Compare commits
1133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b9daa2702 | |||
| 304350ffd4 | |||
| 8988094df5 | |||
| f93df8cb9e | |||
| ced8c4c821 | |||
| f45098a61d | |||
| d146ba869a | |||
| 3ffeef55a6 | |||
| cb7814b362 | |||
| 2ecd54a3b2 | |||
| b34a2cdfc9 | |||
| 304c225c8f | |||
| 72adaf4119 | |||
| c656ad4068 | |||
| c33a7b77e1 | |||
| 7f64580ce2 | |||
| 08d7bd0cee | |||
| 6ecefd6bc7 | |||
| b21b8c4a53 | |||
| 771ddcb91e | |||
| 408c12fc66 | |||
| adff893835 | |||
| 81e328d090 | |||
| b1c09ed61b | |||
| 9ccf94dc3c | |||
| 15ab77629f | |||
| 5178cbbfc7 | |||
| a7ff0587a6 | |||
| edec6e7558 | |||
| 868e821427 | |||
| de5d80547b | |||
| 5e755c5d51 | |||
| d46e65805a | |||
| 9b05d0d822 | |||
| 01a36c90c3 | |||
| 6da17b3f02 | |||
| 15ed91e047 | |||
| 6d1289482a | |||
| 8021d72389 | |||
| f770248b24 | |||
| 2a79956caa | |||
| 871116eee9 | |||
| 03c16cb9f6 | |||
| 34a9289978 | |||
| ad63d28cfa | |||
| ac0992953e | |||
| d035eb1d90 | |||
| 0ec90c23da | |||
| c72384f7d4 | |||
| 5a5a5797e9 | |||
| e22162a23a | |||
| 73f977986e | |||
| 2d444284cc | |||
| c0c9d16412 | |||
| 78a1634341 | |||
| 78e2ab9d99 | |||
| f1553ae7f8 | |||
| 9a1a3e67c3 | |||
| 82ae74fee9 | |||
| 8d66d1bab0 | |||
| 66d43c1088 | |||
| ad79f9c0ac | |||
| 1510f87fa0 | |||
| a2f868d1a7 | |||
| 17befab414 | |||
| abda959796 | |||
| 1a343d93e7 | |||
| 2a70587dda | |||
| d403c12464 | |||
| e5487da160 | |||
| 9c1d62a039 | |||
| 1c9171d917 | |||
| 931c564942 | |||
| 632de475a1 | |||
| a21ea06bed | |||
| 66330a20b3 | |||
| 3f272a72c4 | |||
| 81b55a4ca8 | |||
| d43812ac04 | |||
| 2c87333106 | |||
| 1aacbaf681 | |||
| 5093af87b1 | |||
| 23cc1e31c1 | |||
| 96824055f8 | |||
| cb9e2bda92 | |||
| 04befa3ab5 | |||
| fd15333ba2 | |||
| 68c61ef8e1 | |||
| fd9266e960 | |||
| 5ea7ef01e5 | |||
| 3693c582c3 | |||
| 8101879799 | |||
| ec95ca6ed1 | |||
| b88381a289 | |||
| 3917ba67c9 | |||
| c1030562c3 | |||
| e6185b3827 | |||
| 13df762dc1 | |||
| ec5f94f0b4 | |||
| 420a69f9de | |||
| ec5f5bc104 | |||
| 9d46da9f11 | |||
| 13f2c89e06 | |||
| d4df2624ff | |||
| 37098bbb43 | |||
| 1913d012ee | |||
| 80ccbd045f | |||
| b73dbb4a55 | |||
| 02185f7034 | |||
| dadca42776 | |||
| 5d549f1714 | |||
| 77f0b6a4e2 | |||
| bffcb26e7d | |||
| 78608a1b9d | |||
| 75aba1d17f | |||
| c5e8cb71b5 | |||
| a49b3a1b61 | |||
| 0e8a707311 | |||
| 2cf36c7a5e | |||
| 001e88a60d | |||
| 4ec34ad25d | |||
| 03b58ea1df | |||
| 59924ea03e | |||
| b85c819414 | |||
| 6012c7d98b | |||
| b029519248 | |||
| 61440c3561 | |||
| 9c4e547f5a | |||
| fa3db13cec | |||
| eff57ad483 | |||
| 2c964b07ab | |||
| 8c5d60e539 | |||
| 63d4b758f8 | |||
| 7876c37715 | |||
| 29f29e981e | |||
| 2163b282b5 | |||
| 40dc706270 | |||
| 0c97c85aea | |||
| 0db486b511 | |||
| 7551d87384 | |||
| 3ac598666a | |||
| 1380af4f00 | |||
| ab08f5e095 | |||
| 59d1f349b5 | |||
| 3dbf8a07bd | |||
| 392fd004d9 | |||
| 72314f1014 | |||
| 9a1de1d9e8 | |||
| 7dd08c3b81 | |||
| e6dc24a914 | |||
| 05f9f85d2d | |||
| 96c5515b04 | |||
| e94b40a75f | |||
| dc60c1ee10 | |||
| 3e6347cd48 | |||
| 7377971da8 | |||
| 13df5d9db8 | |||
| 664a64b3f3 | |||
| 6c90f7aaff | |||
| eddaf961a1 | |||
| 0e456cded2 | |||
| a042b3d409 | |||
| 16891ff937 | |||
| 70403b5d9a | |||
| 6a8dd30f26 | |||
| 75d6a048b9 | |||
| d186e735d3 | |||
| 75c4cebdb6 | |||
| 56be9d5461 | |||
| 2851a42fed | |||
| 58cbc0ffaa | |||
| 93fa01f7f5 | |||
| 4892fce37c | |||
| cedcda5372 | |||
| 78433d3dc7 | |||
| db9e6518ed | |||
| 4c9398f6bd | |||
| 3ef8cb2f92 | |||
| bbe9d75bd6 | |||
| 8d6f88f612 | |||
| 8cf52972bb | |||
| e99b67c281 | |||
| 1985eede8f | |||
| 0f5f71f07a | |||
| 1dc370942b | |||
| e0d7f4d9c6 | |||
| aebfaf58a3 | |||
| 0d4c5c7f49 | |||
| 2de2bc658c | |||
| 15c4ba2fb9 | |||
| bb05b904cf | |||
| 110d67cc62 | |||
| 03069f7acf | |||
| f7aee0ee36 | |||
| e9dc02ae17 | |||
| 11d66979d6 | |||
| 7e44fbca79 | |||
| dcc32da55a | |||
| 52c0614199 | |||
| 0777aa6ece | |||
| 3945071446 | |||
| 05e4d7a502 | |||
| ba38d0c1de | |||
| f7cc17e1e0 | |||
| f77d2cd83f | |||
| 4cd619ccdd | |||
| 66aae2a0b5 | |||
| 7fb57ea271 | |||
| 4d9e41871e | |||
| f672f17fcf | |||
| 39a6a2eb8c | |||
| 13ceb3e745 | |||
| 6250677493 | |||
| f1dcfd6332 | |||
| c40b3e768e | |||
| 3eaac27789 | |||
| affb5d195e | |||
| 7995c046b4 | |||
| 612ed3b603 | |||
| ba1c797db4 | |||
| e3fdf19843 | |||
| c1061150a9 | |||
| c5f4168dd6 | |||
| 7ea804bd2a | |||
| 748c3d514a | |||
| 818778cdf3 | |||
| 2a605deaa5 | |||
| a154cd4640 | |||
| eee0dc4e87 | |||
| 28a8ce1732 | |||
| 3d3639a923 | |||
| 57d454c557 | |||
| 94fe6a9876 | |||
| c9086a436b | |||
| 87370abea4 | |||
| fa554455b7 | |||
| b430d95c0a | |||
| b4b2ee4716 | |||
| ea6e15086e | |||
| d7a887766c | |||
| ee040aba60 | |||
| f7e06d0c20 | |||
| d2ecf461d0 | |||
| a1bbe9d810 | |||
| 20ada4014d | |||
| 604190d3ca | |||
| 6ad26fff5c | |||
| de989b2c51 | |||
| ea4737269f | |||
| bf39976d21 | |||
| ad9c2acef1 | |||
| 5ac5e04fb8 | |||
| 794b11a0ce | |||
| dba7cf39e6 | |||
| 1a872ac383 | |||
| 7bce12ca70 | |||
| 2652b77a1b | |||
| f310db44a3 | |||
| 5df53d7338 | |||
| 9575ee92d4 | |||
| e49d62988b | |||
| 8867a03c07 | |||
| 6c18bcf748 | |||
| 4cc67e9002 | |||
| bac0654a63 | |||
| 6111beded7 | |||
| b0558a002b | |||
| c6f4ca09f5 | |||
| 532f3a3ee8 | |||
| 3914e31461 | |||
| a48d604b3f | |||
| e486da4b08 | |||
| 67e5820643 | |||
| 7ae0bcecfd | |||
| 1fd29f7b6d | |||
| c8f6fe4f6d | |||
| 21804ab313 | |||
| 14252807a2 | |||
| d1fad85c5f | |||
| 38932e7e03 | |||
| 397a970636 | |||
| 8d2db9d35a | |||
| 8ee0bbb22e | |||
| 672bb0229b | |||
| b894b700c7 | |||
| 9af57c7a60 | |||
| 725d7bbc97 | |||
| 2d10ea4dc9 | |||
| e50b574f12 | |||
| 35173347fd | |||
| 64c5a83e33 | |||
| 8da3922ff0 | |||
| eba5efc829 | |||
| 7ed6db9758 | |||
| 32c7a1ab78 | |||
| cbb92cacce | |||
| 402d620608 | |||
| b57dbd8d1a | |||
| aa6bbc1a0f | |||
| 6672302684 | |||
| 9e4125bb39 | |||
| cb452802bb | |||
| da249bbc51 | |||
| c16bfd3234 | |||
| 83427b325c | |||
| b99e103704 | |||
| 6d6f470e11 | |||
| 94d3758a0d | |||
| 4b1d03f229 | |||
| 06825c6385 | |||
| 2987bd1f6d | |||
| 099cd8d6a0 | |||
| f669b3a530 | |||
| da8588787f | |||
| e21e412aae | |||
| 749ed2c3e0 | |||
| e709cca2f5 | |||
| f661b0d728 | |||
| cef4409dda | |||
| ac6fa7abb1 | |||
| 9ef465fdf4 | |||
| 11de0449fa | |||
| 964553952d | |||
| 5c52014c4b | |||
| 85d09c3c56 | |||
| c0924c87af | |||
| 733665ddcc | |||
| 2c57557a3a | |||
| d5df58496b | |||
| df5e8e724e | |||
| a7cc3777d1 | |||
| 4724115e8d | |||
| e355a6aa39 | |||
| 99ae06be68 | |||
| 787b2d31cd | |||
| 23f17614e5 | |||
| eebbb74be7 | |||
| fac5ba5bae | |||
| c57d2c68a1 | |||
| 6b1ef484f2 | |||
| aeb85ba836 | |||
| afa3808752 | |||
| 1f26822a64 | |||
| 44a78ba9f6 | |||
| 1493dc2c6a | |||
| 9531a4041e | |||
| 1ce27d6eec | |||
| 73ae1cc6da | |||
| 74b79d8212 | |||
| d0776819c7 | |||
| 4e9edcb0db | |||
| e3dc094be4 | |||
| f4772c9b0e | |||
| 76210686c4 | |||
| 3a00271af0 | |||
| d6915793c1 | |||
| 121dedee4e | |||
| 8eebb9bb39 | |||
| 9e2e28d57a | |||
| 2255cd5a43 | |||
| 12a231675c | |||
| 60017241b2 | |||
| 03fe9feb69 | |||
| 6f2d8e0e50 | |||
| 56aa86da8b | |||
| 67e2842f84 | |||
| 443647a1ba | |||
| b9ea6ff300 | |||
| c219c727bb | |||
| db0f9b19be | |||
| 343416653d | |||
| 17b3cb6b31 | |||
| f8a6f90664 | |||
| 8ac51c19f5 | |||
| ad2e8336f6 | |||
| a3f6e0fb5a | |||
| f997256c73 | |||
| e56d092b4a | |||
| dedfc2649a | |||
| 90bed18415 | |||
| a3061eb39a | |||
| f69db1d169 | |||
| 0ba4e42161 | |||
| 7cf36ee9f6 | |||
| ff40ef0176 | |||
| d02125ba21 | |||
| 76200c1007 | |||
| b4c3ab38b8 | |||
| f4b0ebdb95 | |||
| 6488be671e | |||
| 5eaf10e9f8 | |||
| 177e31cf9a | |||
| dc2b9ed89c | |||
| fb9982fb48 | |||
| 856dd01009 | |||
| ea1a01000f | |||
| 0ebedfe286 | |||
| e0b4e2c35d | |||
| 1f2459478f | |||
| 0c0342b994 | |||
| 3e261188d3 | |||
| 16eb449c6b | |||
| 4618d7ca7d | |||
| 5e2a2465c9 | |||
| 0e0a406cb1 | |||
| 04e5643422 | |||
| d2bb17acd5 | |||
| dddc607e07 | |||
| 25f893b0bb | |||
| 415617080a | |||
| 2c7afc201f | |||
| 89abb75d4d | |||
| fd5666f55f | |||
| 397a26e00b | |||
| a6c42404a6 | |||
| 7856dab56d | |||
| 149606e148 | |||
| ebcf1c434c | |||
| b5e2eb6831 | |||
| 9d20a02a12 | |||
| 8f5e1f3dfc | |||
| d9864373f3 | |||
| e4331ac9b8 | |||
| 9547e3cee6 | |||
| ec45ce3aa6 | |||
| 5916192fb6 | |||
| 5e347ce135 | |||
| 97ce5742e1 | |||
| 13cc7962e7 | |||
| c471ee42ab | |||
| 8e2fdd9200 | |||
| 289e2ac92b | |||
| 90c35b6b34 | |||
| fd96360228 | |||
| bd65c77534 | |||
| 4398052d94 | |||
| b03ae97580 | |||
| a157f2eb97 | |||
| db5151d612 | |||
| 621b47d763 | |||
| 650281b534 | |||
| 25ee247fe9 | |||
| 1f524f2dec | |||
| a3e6a070d5 | |||
| 381c02d21e | |||
| 558d133e14 | |||
| 3a99d52e84 | |||
| 4325812b05 | |||
| 845f65400a | |||
| d5cbf77b84 | |||
| f322dcd200 | |||
| 8cb778e9d9 | |||
| 09955cf0db | |||
| f7329c71bb | |||
| b977a239c3 | |||
| 3aa62a265d | |||
| fc4cd530fb | |||
| 8a313df6a4 | |||
| 7c68096237 | |||
| e12f917559 | |||
| 20f0346733 | |||
| 8e98252be3 | |||
| 1053bc9148 | |||
| 1dd497e9de | |||
| 81f1292660 | |||
| 5671121b21 | |||
| 88c4dec35f | |||
| 6d9aa14ccd | |||
| 16551feea3 | |||
| 1e7182e820 | |||
| a843125fa4 | |||
| fcfdaadb25 | |||
| 222f969e2f | |||
| 406fd011ff | |||
| b903ee4b42 | |||
| b9b5286f8a | |||
| 2f5d8c212a | |||
| 797532815a | |||
| f11aeafd58 | |||
| 327e0aef99 | |||
| 742700b7fa | |||
| 03f4a56bff | |||
| d8b0b9e2f4 | |||
| dbb9c60d09 | |||
| b6db3af882 | |||
| 8a8ad22961 | |||
| b78bbc01a0 | |||
| f51865e1ae | |||
| 682c17c9d8 | |||
| 4da1c01963 | |||
| 96a4b06ca6 | |||
| ef5b12035d | |||
| 38978dacd7 | |||
| 74ea661438 | |||
| da2abccc0d | |||
| 6731c52b12 | |||
| a3ca28f1a5 | |||
| 450ceaa241 | |||
| 6cd655ba7c | |||
| f9ab073adf | |||
| b5d7c40029 | |||
| 29f7f88a2c | |||
| db37a1feb5 | |||
| 901024bccc | |||
| 5d1fa986f3 | |||
| aa99c7bd38 | |||
| 76d484541f | |||
| 8482d181b5 | |||
| 650f210da7 | |||
| 191350a290 | |||
| 95baf17c36 | |||
| 1c1053afe6 | |||
| 7ac153fd67 | |||
| 31131146a6 | |||
| ac0bc95c25 | |||
| 11c3799fa2 | |||
| f11cf87326 | |||
| 7b092fd174 | |||
| 1fa4bb4cfa | |||
| dabc7c512c | |||
| 8fe27ab582 | |||
| 8c1d3f4f60 | |||
| a623215257 | |||
| f61de718b8 | |||
| fa5bbadf57 | |||
| 9319f4fcff | |||
| 25fb9ee47d | |||
| 85538dc3ed | |||
| 5bb2511914 | |||
| 667ada88e6 | |||
| 022b8a0f38 | |||
| 29caa02ef0 | |||
| 9102a9c841 | |||
| 99e47ed5d7 | |||
| 54729ce32b | |||
| 36e199c31e | |||
| ab9e4f73b1 | |||
| ce7143b833 | |||
| 9480450410 | |||
| 4744a994b4 | |||
| 1fd5b34fd0 | |||
| 1255027d6e | |||
| baac38fec5 | |||
| 94c0322fbe | |||
| e9a4389a12 | |||
| 67615fec3a | |||
| 95a471b0d2 | |||
| 56f4b3e70e | |||
| 6aee1f62bd | |||
| 8d968604e9 | |||
| 3a7b8fc6a5 | |||
| 7de5d295b6 | |||
| 6b394d96bd | |||
| 97959bbcd0 | |||
| 2883685bcc | |||
| eef61f87c1 | |||
| 2c9a088a36 | |||
| 8912761eb7 | |||
| 3323f37efc | |||
| 515aaf0a8a | |||
| c172ad9191 | |||
| 601dce76ef | |||
| f704066fbe | |||
| b8174c437f | |||
| a7cda30f6a | |||
| 88cd2557f3 | |||
| bd33c336e7 | |||
| 82bcf48c88 | |||
| daaf17198c | |||
| fe39ca47d6 | |||
| f42c8937da | |||
| 199275ff89 | |||
| 7df31406dc | |||
| 1edfc6cb5e | |||
| d4c1b9b8ad | |||
| d447f63e33 | |||
| c13fb7e19f | |||
| 17805cbcd8 | |||
| d2c9ca455d | |||
| 36c39b837a | |||
| b83a644260 | |||
| 7c4d180297 | |||
| b2e7ae4310 | |||
| 5d5a3044c8 | |||
| ae170362a5 | |||
| 01c5412951 | |||
| ab2b5bfa23 | |||
| c120da79d1 | |||
| 962c0bf4fd | |||
| 57b68614af | |||
| 8eafaa58fb | |||
| 96c7b3fc52 | |||
| 0a02a41a14 | |||
| a248ec75e2 | |||
| 3aa0a905b2 | |||
| 44443d3b46 | |||
| 40d96dbf27 | |||
| b2dc21d7d0 | |||
| fa5ce1d462 | |||
| e264482954 | |||
| a1c1b0e157 | |||
| 3ac123db29 | |||
| 9caec95c5e | |||
| 4bdcedbc66 | |||
| 3ccd2e9f8f | |||
| 099c855049 | |||
| 1e35188aec | |||
| c6da40cf55 | |||
| 0ff9e066fb | |||
| c59465c54c | |||
| 0c4b62f664 | |||
| 8c2831a5da | |||
| 4e8cee162a | |||
| 1e11ac406f | |||
| 10069fbead | |||
| 56da4a31a4 | |||
| ffacbe8666 | |||
| ca13be020c | |||
| baf97c69b1 | |||
| 628374b8d8 | |||
| 454d49aa64 | |||
| 06e212c11d | |||
| 213dac2d30 | |||
| 6b754acd81 | |||
| 88f75a85bb | |||
| a8e522c164 | |||
| 9dcab4ed30 | |||
| 4774cc8e65 | |||
| 9c4318d191 | |||
| 2bb07d6a4e | |||
| 44029009e4 | |||
| b2c96b72b0 | |||
| 9d281937d5 | |||
| 818a435f9e | |||
| 486b6d6e2b | |||
| 32edfb1a9f | |||
| 9159a5983b | |||
| 69ac7e07e6 | |||
| ee23839259 | |||
| 555dfe0e77 | |||
| 7f7d9b8175 | |||
| 57f6715784 | |||
| b587c064d7 | |||
| 52dc64e0db | |||
| 7b7ee980e8 | |||
| 5591be9a8e | |||
| 876d3237eb | |||
| a308d34d09 | |||
| cabab289c9 | |||
| a98779dfbb | |||
| 182e84cd3d | |||
| 5c049d6e2e | |||
| 057bca070c | |||
| 75871216d2 | |||
| 4661ca810a | |||
| f3687dc4c7 | |||
| 694fb57c17 | |||
| e1b9fe266d | |||
| 35a1596755 | |||
| 117307eaff | |||
| 52a5a86cf9 | |||
| 9faa839f56 | |||
| 5f960d889e | |||
| 655ac0725c | |||
| 229105536b | |||
| 4b85a81347 | |||
| a328d8787a | |||
| d1e92ece42 | |||
| ff4a8f0ba5 | |||
| 0a7e28f681 | |||
| e8cf6dcde6 | |||
| 505fb682af | |||
| 2520804a60 | |||
| 73684ab57c | |||
| 7718f90428 | |||
| 5e692931dd | |||
| 5e10ccc248 | |||
| 73b01743a5 | |||
| 2f9b9942c3 | |||
| a52a2329a1 | |||
| 6f9147de86 | |||
| 6a67ff9acf | |||
| 4eb3da6be7 | |||
| 78889aec8c | |||
| b0880996fc | |||
| 88a8a7007c | |||
| ff1555ed40 | |||
| 5c29c08941 | |||
| 3fb8a46c95 | |||
| 31d985813a | |||
| eea475854c | |||
| a6c2719976 | |||
| 2f58cb1620 | |||
| 0947349ae0 | |||
| 2f3b85d244 | |||
| 919d58f94b | |||
| 16dcfb2e84 | |||
| 9ef78a484c | |||
| 3edaff1364 | |||
| 45055d80cd | |||
| 1ea163271b | |||
| 899e4db8d0 | |||
| e9cca7f68d | |||
| fd709b9d52 | |||
| 4ad79d6d44 | |||
| e57a02fd91 | |||
| c41f7975b3 | |||
| cb6b420ad0 | |||
| 724d133cce | |||
| b7d6fd08f1 | |||
| 0469c27b91 | |||
| f14c00db82 | |||
| a204b2994d | |||
| 3f627f4125 | |||
| 98a68632df | |||
| 0c98e26a05 | |||
| 4b56ca1841 | |||
| 8d2e790bca | |||
| e4be216731 | |||
| 74727e5f84 | |||
| 4b711e4b37 | |||
| 4db69647ce | |||
| e922a58cc3 | |||
| eb0ddbc063 | |||
| 9fbc2ab07c | |||
| 2f7b2f0451 | |||
| ad1623da58 | |||
| 2abe3aba4a | |||
| 82684d64d4 | |||
| 7b40daa3cf | |||
| 5ee3897f7e | |||
| 370f4735f7 | |||
| 77ba3010cc | |||
| b2b9b5fa12 | |||
| c251f16292 | |||
| 8890bf3cee | |||
| 2e3ced1fb2 | |||
| 2163ab03ec | |||
| 4b1eefca80 | |||
| 4dd7c3093c | |||
| 70466aafb4 | |||
| 3e35f163b7 | |||
| 690ed4611d | |||
| 8392ef07cd | |||
| 9c33540af8 | |||
| 0401b995b7 | |||
| 0521d23e94 | |||
| 37d95571e9 | |||
| caa9a7d8be | |||
| d9231be1ba | |||
| 2068e7f266 | |||
| 26b0b32e55 | |||
| c7f3e2ad1d | |||
| a79c5286d7 | |||
| 8ac6845607 | |||
| 9f75552c9b | |||
| 3ef5214587 | |||
| 30640ebb65 | |||
| 4f3cdfacaa | |||
| b68bcf9cff | |||
| 8e64341176 | |||
| 40ba98b95e | |||
| 3541d205e0 | |||
| 2a201d4218 | |||
| 43129441db | |||
| 5ae2d83457 | |||
| 193f3331e8 | |||
| 65fe3c8b5b | |||
| 85e8771b5a | |||
| d27bfca5e4 | |||
| 77bf972b3f | |||
| 307063e571 | |||
| 18c155beb5 | |||
| 0c1b6e45d5 | |||
| b5bda577dd | |||
| 6593e32582 | |||
| d3612ce35b | |||
| bd6d0e959a | |||
| cced512ad4 | |||
| 06359b1166 | |||
| 10098d20c5 | |||
| e4c8d6b708 | |||
| 6c6a8e2e77 | |||
| dcf0069753 | |||
| f0354d1fc5 | |||
| 6634735065 | |||
| 59c468c758 | |||
| c6e93b06a3 | |||
| 89033cd13a | |||
| 88a70f472f | |||
| fafd1a403c | |||
| 371cc24031 | |||
| 2538ba68c5 | |||
| 14e93e8c0c | |||
| 54bdb7791c | |||
| 2f97bc2bae | |||
| 7e3e8fff55 | |||
| 8d878b6785 | |||
| add06bf897 | |||
| e5b07aa827 | |||
| 814d78708e | |||
| 88d7a2fe28 | |||
| 5386e9e838 | |||
| 9e6252cb2d | |||
| 12d5f51051 | |||
| c7d34bd65e | |||
| 900a6d1382 | |||
| 9228ad2f59 | |||
| 7d9ee71245 | |||
| c1c8bfda4e | |||
| 1fb717968e | |||
| e69591dad3 | |||
| ee8e9ef528 | |||
| 7cbc3e587d | |||
| aeba46a0eb | |||
| 11074d8f4d | |||
| a6c133369f | |||
| 6a81ceced0 | |||
| ffc7648ce6 | |||
| fd395a82c5 | |||
| 15afd1f690 | |||
| 1e24fbc72d | |||
| 32afc56005 | |||
| 7e61a6dd31 | |||
| cae3b38c35 | |||
| c36cb1b424 | |||
| 315a29f568 | |||
| 74931768e5 | |||
| 7eb3c30a3c | |||
| 7e99e812dd | |||
| e97b7838c5 | |||
| 8eb3fdc9e4 | |||
| 8536e2b2a3 | |||
| 060eaeffc0 | |||
| b177bd9783 | |||
| 6fa487fad8 | |||
| 12444d3dc1 | |||
| ed5d97e052 | |||
| cff844ac74 | |||
| 70e38755fa | |||
| c20e6aeca7 | |||
| 53d723d149 | |||
| 5cb587a60b | |||
| 354f9de257 | |||
| 9d04b23f45 | |||
| 73edf6b734 | |||
| 5114474829 | |||
| 91331bea51 | |||
| 79f97504f8 | |||
| 50ed681a4e | |||
| 910887fbc6 | |||
| 304bd910f0 | |||
| e87a7954a0 | |||
| d2b02ec2e8 | |||
| d84387d12e | |||
| 368b585124 | |||
| 7bbd07cc77 | |||
| 008330a744 | |||
| 1052c8db15 | |||
| 3e04590ded | |||
| ce2d8212eb | |||
| 5ad9090dc8 | |||
| b15829a9fd | |||
| bebb733607 | |||
| e872babd40 | |||
| bcb125a09b | |||
| 65774aa90b | |||
| 868bb9a8d9 | |||
| 61c7a96e36 | |||
| e6dadf2b0d | |||
| 75454de284 | |||
| 8b298dfd2f | |||
| 31ba7b82d8 | |||
| 71f4af9cdd | |||
| fd716bcd81 | |||
| 385e6933d2 | |||
| fab1c1c299 | |||
| 9bf48ef041 | |||
| 2e9f362ae4 | |||
| fce1140ad1 | |||
| 0c1d90d901 | |||
| 717dc1184b | |||
| 99e0593f90 | |||
| f6f6cfd844 | |||
| d905dcc476 | |||
| 790fe5f6db | |||
| f2652ee9f2 | |||
| 099bf9c929 | |||
| 5957d9603b | |||
| 3d60ac36d2 | |||
| 787d04190e | |||
| e33c44266e | |||
| 17e8109ab6 | |||
| 14246c7094 | |||
| 6a34f54753 | |||
| 78e3350b17 | |||
| e3c7e9db9b | |||
| dc89c00a8c | |||
| 5baf078c4b | |||
| f950e67b39 | |||
| 2c688fd40f | |||
| e94fd2a7df | |||
| d699b2fa70 | |||
| 61d3f8d1d9 | |||
| 435d74a67a | |||
| fe2ca34179 | |||
| 3b068b592d | |||
| 344a96a80f | |||
| 4e8c63e4e0 | |||
| 452cf0f269 | |||
| e78c4b89b4 | |||
| bcf8cc1c57 | |||
| f47e7b3427 | |||
| 8252379f76 | |||
| 2ef1e32f9a | |||
| 47a4f473a6 | |||
| 9d6d2c3b3c | |||
| bd38dc971b | |||
| 4712c318cc | |||
| a92140f3ac | |||
| 88d10210ad | |||
| de9adc8d3d | |||
| 51704fa093 | |||
| 0e91c085f9 | |||
| 49630fdd9f | |||
| a1f1992d7e | |||
| d64a5e4dcc | |||
| f0c97d94b2 | |||
| 5d0c6a608a | |||
| 3dbfdcddf4 | |||
| e9b11408bb | |||
| d26b8cca1c | |||
| 6c5400896a | |||
| 8b7ebece49 | |||
| 4def306eb8 | |||
| 3d4db7e220 | |||
| 6fe98c3f43 | |||
| 375c6c33c6 | |||
| edf23dbd2e | |||
| 8d87e32f8b | |||
| 2f6d0450a7 | |||
| e07ed14d20 | |||
| ae97772909 | |||
| 8975e6ab5d | |||
| 32aa784c5f | |||
| 56e155300c | |||
| df61037ac5 | |||
| 471b470c01 | |||
| 56d9f9d9a8 | |||
| 6e685e2e09 | |||
| ad5c1202b5 | |||
| 87a07d9ee3 | |||
| e0bda80e7b | |||
| b9ef5d6270 | |||
| fa26499a39 | |||
| 916e85f79e | |||
| f2b61eb5e2 | |||
| 40e006658c | |||
| ac2fce8220 | |||
| 296d613138 | |||
| 43bed281b1 | |||
| b951d0d785 | |||
| a802b733f8 | |||
| 6acd68cf00 | |||
| 75365a7774 | |||
| 61a5f1b3cf | |||
| 44e438b21b | |||
| 7b49283394 | |||
| e0384494b6 | |||
| cc236e39d5 | |||
| c4d5657162 | |||
| 59f9f43f15 | |||
| 261eb99614 | |||
| 5ae638047e | |||
| 8ee0021641 | |||
| 39241c5e18 | |||
| 9ef9251936 | |||
| 90f1a34855 | |||
| e00532f5d2 | |||
| 8cef7a54d9 | |||
| f64af126f1 | |||
| e3da325f29 | |||
| 860fe4afe0 | |||
| e3ba3366f1 | |||
| e604277e4c | |||
| a356a20c3d | |||
| 1c1ecf0ab0 | |||
| fd26cbcfcb | |||
| 3a543f188b | |||
| 06fe8a8f32 | |||
| 6ef33619bf | |||
| cde6a559ad | |||
| 3b20cc4444 | |||
| 3599e578e2 | |||
| bc6c458371 | |||
| f0d722099f | |||
| bcf1ee408b | |||
| ef322cc39b | |||
| f5f8f47667 | |||
| c418f0e6ba | |||
| cde7dd2ea8 | |||
| debc28fa47 | |||
| 6e3892528c | |||
| fca455c0bf | |||
| cd90265eab | |||
| 25b2b191bb | |||
| 3ade16a07d | |||
| 411dd29244 | |||
| 1e74be2b2d | |||
| a77b67eecd | |||
| 18eefcffdb | |||
| 2a5dcd562c | |||
| a3b8ec3640 | |||
| 1fa488ec04 | |||
| 25ecf089aa | |||
| 8573417537 | |||
| f20c92a323 | |||
| be8be9ef04 | |||
| eccfab8b27 | |||
| 25e4c8d722 | |||
| a9d6ab7313 | |||
| 4d2c3e33fc | |||
| 0be2747ccf | |||
| 0b01a2a53a | |||
| d85c7b213f | |||
| 52b10e776c | |||
| d88970155d | |||
| 1139538c76 | |||
| a1a93afbe2 | |||
| 7a107d8fa2 | |||
| 5117325ef4 | |||
| 1085b85dfc | |||
| 7a7e624e7b | |||
| 384d8b0e19 | |||
| 68c9e5e98d | |||
| ff89315b6e | |||
| eec52d7977 | |||
| 8515def61b | |||
| 75ea0ad283 | |||
| ac24f62d8c | |||
| 76cd7ab649 | |||
| eab35c1289 | |||
| 1d6c01fb34 | |||
| b680c50035 | |||
| 20b8c41727 | |||
| e9d2b1f27a | |||
| f90d1678e2 | |||
| 92b7598e4b | |||
| 10779481b7 | |||
| 2314a74cdb | |||
| 5e0e752fcd | |||
| b4b460ad27 | |||
| 6d5f617355 | |||
| 9f0a1e0ce7 | |||
| d6484a012c | |||
| f7cb253b1c | |||
| 2dbd4a28a1 | |||
| 5c3edfcf6a | |||
| 20b97fc716 | |||
| b3f4e658c5 | |||
| a3fe9c357b | |||
| 2fcd28ab0c | |||
| 5e029aa7a0 | |||
| 95edb9f864 | |||
| 7116ed4848 | |||
| c00f9e9312 | |||
| 26896599de | |||
| 3b5238135d | |||
| 0e5cc44ffa | |||
| ca47e6d5f9 | |||
| 75f1aaced6 | |||
| de316acc11 | |||
| 706e477e02 | |||
| 6716ad6374 | |||
| 92df885474 | |||
| ddb44d35cd | |||
| 8de6345d0f | |||
| e6d33eaf90 | |||
| fa74602c65 | |||
| 5cbc34811f | |||
| 2d60611a44 | |||
| ff387609c3 | |||
| b05a0ecb3b | |||
| 784c7459fe | |||
| 18065cb42e | |||
| 01d11888f4 | |||
| 8aef687dcb | |||
| 79bb716298 | |||
| 8ef09b4044 | |||
| 5f435a86ee | |||
| 58be7b0f3d | |||
| 7034104ef1 | |||
| c4113060b2 | |||
| 3d1de1f583 | |||
| 147f1d3f6c | |||
| e3af74739d | |||
| 8d0867a1a8 | |||
| cfe3bb7cef | |||
| 95b95aae5f | |||
| 83d7cd5430 | |||
| d0e5d84bff | |||
| fb4b5ea48f | |||
| 985f9f9b07 | |||
| c7f7828368 | |||
| 3fea9ae51a | |||
| 3c4b892129 | |||
| a7d2ff327d | |||
| 90c1858389 | |||
| 289268a60b | |||
| 04d1fd4b8a | |||
| 97b2a20338 | |||
| 62641adfeb | |||
| 6e407989e8 | |||
| 1521ec6cb2 | |||
| b3ada6cb37 | |||
| 21b7e54a59 | |||
| 463b0dfb91 | |||
| f48695cc95 | |||
| 395a39e039 | |||
| e37f5e5ac3 | |||
| 1bc921f373 | |||
| 36e69f30ec | |||
| 82c60116ef | |||
| 840cb97c21 | |||
| ae15595af1 | |||
| f89ed01182 | |||
| 2e803c3a3d | |||
| 7a3a3edc9d | |||
| 13718c6d49 | |||
| 767b45dcf7 | |||
| b690db1e05 | |||
| 5ab397e8d9 | |||
| 0d938d94aa |
@@ -38,6 +38,7 @@ rustflags = [
|
||||
"-Wclippy::nonstandard_macro_braces",
|
||||
"-Wclippy::str_to_string",
|
||||
"-Wclippy::todo",
|
||||
"-Wclippy::unused_async",
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")']
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/audit-check@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/audit@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -12,12 +12,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-05-19
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
@@ -108,4 +108,50 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --only-target=aarch64-apple-ios
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
||||
with:
|
||||
use_rust_sdk: "." # use local checkout
|
||||
use_complement_crypto: "MATCHING_BRANCH"
|
||||
|
||||
test-crypto-apple-framework-generation:
|
||||
name: Generate Crypto FFI Apple XCFramework
|
||||
runs-on: macos-12
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add rust targets
|
||||
run: |
|
||||
rustup target add aarch64-apple-ios
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
- name: Delete cargo config
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Run the Build Framework script
|
||||
run: ./bindings/apple/build_crypto_xcframework.sh -i
|
||||
|
||||
- name: Is XCFramework generated?
|
||||
if: ${{ hashFiles('generated/MatrixSDKCryptoFFI.zip') != '' }}
|
||||
run: echo "XCFramework exists"
|
||||
|
||||
+23
-25
@@ -41,10 +41,11 @@ jobs:
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
- image-proc
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -64,7 +65,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -82,7 +83,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -96,7 +97,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -114,7 +115,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -128,7 +129,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -161,7 +162,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -210,11 +211,8 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m]-indexeddb, no crypto'
|
||||
cmd: indexeddb-no-crypto
|
||||
|
||||
- name: '[m]-indexeddb, with crypto'
|
||||
cmd: indexeddb-with-crypto
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
|
||||
- name: '[m], no-default, wasm-flags'
|
||||
cmd: matrix-sdk-no-default
|
||||
@@ -227,7 +225,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -236,7 +234,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: jetli/wasm-pack-action@v0.4.0
|
||||
uses: qmaru/wasm-pack-action@v0.5.0
|
||||
with:
|
||||
version: v0.10.3
|
||||
|
||||
@@ -255,7 +253,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -276,12 +274,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-05-19
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
@@ -295,10 +293,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.17.0
|
||||
uses: crate-ci/typos@v1.20.10
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
@@ -308,7 +306,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -318,7 +316,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-05-19
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
@@ -327,7 +325,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -378,7 +376,7 @@ jobs:
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.94.0 # keep in sync with ./coverage.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./coverage.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
@@ -387,7 +385,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -402,7 +400,7 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
RUST_LOG: "hyper=trace"
|
||||
RUST_LOG: "info,matrix_sdk=trace"
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Code coverage
|
||||
name: Code Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.94.0 # keep in sync with ./ci.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./ci.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
tool: cargo-tarpaulin
|
||||
|
||||
# set up backend for integration tests
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -102,20 +102,37 @@ jobs:
|
||||
run: |
|
||||
rustup run stable cargo tarpaulin \
|
||||
--skip-clean --profile cov --out xml \
|
||||
--features experimental-widgets,testing
|
||||
--features experimental-widgets,testing,image-proc
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 'false'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
|
||||
- name: Get PR number and commit SHA
|
||||
run: |
|
||||
echo "Storing PR number ${{ github.event.number }}"
|
||||
echo "${{ github.event.number }}" > pr_number.txt
|
||||
|
||||
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
|
||||
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
|
||||
|
||||
# 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.
|
||||
- name: Store coverage report in artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Work around frequent upload errors, for runs inside the main repo (not PRs from forks).
|
||||
# Otherwise not required for public repos.
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
# The upload sometimes fails due to https://github.com/codecov/codecov-action/issues/837.
|
||||
# To make sure that the failure gets flagged clearly in the UI, fail the action.
|
||||
fail_ci_if_error: true
|
||||
name: codecov_report
|
||||
path: |
|
||||
cobertura.xml
|
||||
pr_number.txt
|
||||
commit_sha.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- run: |
|
||||
echo 'The coverage report was stored in Github artifacts.'
|
||||
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -37,10 +37,10 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-05-19
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -60,11 +60,11 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Rust version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
@@ -0,0 +1,79 @@
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
|
||||
name: Codecov
|
||||
|
||||
on:
|
||||
# This workflow is triggered after every successfull execution
|
||||
# of `coverage` workflow.
|
||||
workflow_run:
|
||||
workflows: ["Code Coverage"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Upload coverage report
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: 'Fetch coverage report from artifacts'
|
||||
id: prepare_report
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
|
||||
// List artifacts of the workflow run that triggered this workflow
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "codecov_report";
|
||||
});
|
||||
|
||||
if (codecovReport.length != 1) {
|
||||
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
|
||||
}
|
||||
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: codecovReport[0].id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
|
||||
|
||||
- id: parse_previous_artifacts
|
||||
run: |
|
||||
unzip codecov_report.zip
|
||||
|
||||
echo "Detected PR is: $(<pr_number.txt)"
|
||||
echo "Detected commit_sha is: $(<commit_sha.txt)"
|
||||
|
||||
# Make the params available as step output
|
||||
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
path: repo_root
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
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@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check xtask cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
|
||||
@@ -7,6 +7,8 @@ emsdk-*
|
||||
.build
|
||||
.swiftpm
|
||||
/Package.swift
|
||||
# code coverage report
|
||||
cobertura.xml
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
+9
-3
@@ -21,13 +21,19 @@ WeeChat = "WeeChat"
|
||||
sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
ratatui = "ratatui"
|
||||
# base64 false positives
|
||||
Nd = "Nd"
|
||||
Abl = "Abl"
|
||||
Som = "Som"
|
||||
Ba = "Ba"
|
||||
|
||||
[files]
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
|
||||
# automatically ignored, we ignore them here.
|
||||
extend-exclude = [
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys.
|
||||
"*.json",
|
||||
# We are using some fuzzy match patterns that can be understood as typos confusingly.
|
||||
# Fuzzy match patterns that can be understood as typos confusingly.
|
||||
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
|
||||
# Hand-crafted base64 session keys that are understood as typos.
|
||||
"crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs",
|
||||
]
|
||||
|
||||
@@ -29,6 +29,44 @@ integration tests that need a running synapse instance. These tests reside in
|
||||
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
|
||||
synapse for testing purposes.
|
||||
|
||||
## Commit messages and PR title guidelines
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and each commit
|
||||
should have a *good commit message*.
|
||||
|
||||
An *atomic logical commit* is one that is ideally small, can be compiled in isolation, and passes
|
||||
tests. This is useful to make the review process easier (help your reviewer), but also when running
|
||||
bisections, helping identifying which commit introduced a regression.
|
||||
|
||||
A *good commit message* should be composed of:
|
||||
|
||||
- a prefix to indicate which area/feature is related by the commit
|
||||
- a short description that would give sufficient context for a reviewer to guess what the commit is
|
||||
about.
|
||||
|
||||
Examples of commit messages that aren't so useful:
|
||||
|
||||
- “add new method“
|
||||
- “enhance performance“
|
||||
- “fix receipts“
|
||||
|
||||
Examples of good commit messages:
|
||||
|
||||
- “ffi: Add new method `frobnicate_the_foos`”
|
||||
- “indexeddb: Break up the request inside `get_inbound_group_sessions`”
|
||||
- “read_receipts: Store receipts locally, fixing #12345”
|
||||
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR, following the
|
||||
same guidelines of a good commit message, including the area/feature prefix. Something like
|
||||
`FFI: Allow logs files to be pruned.` would be a good PR title.
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
|
||||
branch name.)
|
||||
|
||||
Having good commit messages and PR titles also helps with reviews, scanning the `git log` of
|
||||
the project, and writing the [*This week in
|
||||
Matrix*](https://matrix.org/category/this-week-in-matrix/) updates for the SDK.
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
|
||||
Generated
+1738
-1303
File diff suppressed because it is too large
Load Diff
+36
-10
@@ -6,15 +6,16 @@ members = [
|
||||
"crates/*",
|
||||
"testing/*",
|
||||
"examples/*",
|
||||
"labs/*",
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*"]
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.70"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.68"
|
||||
@@ -25,20 +26,36 @@ async-rx = "0.1.3"
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.60"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.21.0"
|
||||
base64 = "0.22.0"
|
||||
byteorder = "1.4.3"
|
||||
eyeball = { version = "0.8.7", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.4.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.5.1"
|
||||
futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.6"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
growable-bloom-filter = "2.1.0"
|
||||
http = "1.1.0"
|
||||
imbl = "2.0.0"
|
||||
itertools = "0.12.0"
|
||||
ruma = { version = "0.9.3", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = "0.12.0"
|
||||
once_cell = "1.16.0"
|
||||
pin-project-lite = "0.2.9"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "c21817436979acbe66d43064498920a6d289b562", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
"compat-encrypted-stickers",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3266",
|
||||
"unstable-msc4075"
|
||||
] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "c21817436979acbe66d43064498920a6d289b562" }
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
@@ -49,9 +66,11 @@ tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
uniffi = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
uniffi_bindgen = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
vodozemac = "0.5.0"
|
||||
uniffi = { version = "0.27.1" }
|
||||
uniffi_bindgen = { version = "0.27.1" }
|
||||
url = "2.5.0"
|
||||
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "4ef989c6a8eba0bc809e285a081c56320a9bbf1e" }
|
||||
wiremock = "0.6.0"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
|
||||
@@ -95,3 +114,10 @@ opt-level = 3
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Releasing `matrix-rust-sdk`
|
||||
|
||||
- Make sure to bump all the crates to *the same version number*, and commit that (along with the
|
||||
changes to the `Cargo.lock` file).
|
||||
- Create a `git tag` for the current version, following the format `major.minor.patch`, e.g. `0.7.0`.
|
||||
- Push the tag: `git push origin 0.7.0`
|
||||
- Publish all the crates, in topological order of the dependency tree:
|
||||
|
||||
```
|
||||
cargo publish -p matrix-sdk-test-macros
|
||||
cargo publish -p matrix-sdk-test
|
||||
cargo publish -p matrix-sdk-common
|
||||
cargo publish -p matrix-sdk-qrcode
|
||||
cargo publish -p matrix-sdk-store-encryption
|
||||
cargo publish -p matrix-sdk-crypto
|
||||
cargo publish -p matrix-sdk-base
|
||||
cargo publish -p matrix-sdk-sqlite
|
||||
cargo publish -p matrix-sdk-indexeddb
|
||||
cargo publish -p matrix-sdk
|
||||
cargo publish -p matrix-sdk-ui
|
||||
```
|
||||
@@ -15,6 +15,7 @@ matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
|
||||
@@ -29,3 +30,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
@@ -76,8 +76,9 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
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)).unwrap();
|
||||
let machine = runtime
|
||||
.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)
|
||||
@@ -134,7 +135,7 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
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))
|
||||
.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))
|
||||
@@ -194,7 +195,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -203,8 +204,9 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
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)).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();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
@@ -225,7 +227,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -267,8 +269,9 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
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)).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, &response)).unwrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::utils::IntoRawStateEventContent;
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::EventBuilder;
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
|
||||
owned_room_id,
|
||||
serde::Raw,
|
||||
user_id, OwnedUserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let ev_builder = EventBuilder::new();
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
let member_content_json = json!({
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support",
|
||||
});
|
||||
let member_content: Raw<RoomMemberEventContent> =
|
||||
member_content_json.into_raw_state_event_content().cast();
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
|
||||
let state_key = user_id.to_string();
|
||||
let event: Raw<RoomMemberEvent> = ev_builder
|
||||
.make_state_event(
|
||||
&user_id,
|
||||
&room_id,
|
||||
&state_key,
|
||||
member_content.deserialize().unwrap(),
|
||||
None,
|
||||
)
|
||||
.cast();
|
||||
member_events.push(event);
|
||||
}
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
for member_event in member_events.iter() {
|
||||
let event = member_event.clone().cast();
|
||||
changes.add_state_event(&room_id, event.deserialize().unwrap(), event);
|
||||
}
|
||||
|
||||
// Sqlite
|
||||
let sqlite_dir = tempfile::tempdir().unwrap();
|
||||
let sqlite_store = runtime.block_on(SqliteStateStore::open(sqlite_dir.path(), None)).unwrap();
|
||||
runtime
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
|
||||
|
||||
runtime
|
||||
.block_on(base_client.set_session_meta(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
None,
|
||||
))
|
||||
.expect("Could not set session meta");
|
||||
|
||||
base_client.get_or_create_room(&room_id, RoomState::Joined);
|
||||
|
||||
let request = get_member_events::v3::Request::new(room_id.clone());
|
||||
let response = get_member_events::v3::Response::new(member_events);
|
||||
|
||||
let count = MEMBERS_IN_ROOM;
|
||||
let name = format!("{count} members");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(50);
|
||||
|
||||
group.bench_function(BenchmarkId::new("receive_members", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(base_client);
|
||||
}
|
||||
|
||||
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 = room;
|
||||
config = criterion();
|
||||
targets = receive_all_members_benchmark,
|
||||
}
|
||||
criterion_main!(room);
|
||||
+2
-2
@@ -11,7 +11,7 @@ maintained by the owners of the Matrix Rust SDK project.
|
||||
|
||||
There are also external bindings in other repositories:
|
||||
|
||||
* [`matrix-sdk-crypto-js`], JavaScript bindings of the
|
||||
* [`matrix-sdk-crypto-wasm`], JavaScript / WebAssembly bindings of the
|
||||
[`matrix-sdk-crypto`] crate,
|
||||
* [`matrix-sdk-crypto-nodejs`], Node.js bindings of the
|
||||
[`matrix-sdk-crypto`] crate
|
||||
@@ -22,7 +22,7 @@ There are also external bindings in other repositories:
|
||||
[`matrix-sdk-ffi`]: ./matrix-sdk-ffi
|
||||
[`matrix-sdk`]: ../crates/matrix-sdk
|
||||
|
||||
[`matrix-sdk-crypto-js`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-web
|
||||
[`matrix-sdk-crypto-wasm`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm
|
||||
[`matrix-sdk-crypto-nodejs`]: https://github.com/matrix-org/matrix-rust-sdk-crypto-nodejs
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.0.1"
|
||||
s.version = "0.4.1"
|
||||
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
|
||||
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
|
||||
s.author = { "matrix.org" => "support@matrix.org" }
|
||||
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
|
||||
s.ios.deployment_target = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Pod::Spec.new do |s|
|
||||
|
||||
s.name = "MatrixSDKCrypto"
|
||||
s.version = "0.0.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
s.version = "0.4.1" # Version is only incremented manually and locally before pushing to CocoaPods
|
||||
s.summary = "Uniffi based bindings for the Rust SDK crypto crate."
|
||||
s.homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
|
||||
s.author = { "matrix.org" => "support@matrix.org" }
|
||||
|
||||
s.ios.deployment_target = "11.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
s.ios.deployment_target = "13.0"
|
||||
s.osx.deployment_target = "10.15"
|
||||
|
||||
s.swift_versions = ['5.0']
|
||||
s.swift_versions = ['5.1', '5.2']
|
||||
|
||||
s.source = { :http => "https://github.com/matrix-org/matrix-rust-sdk/releases/download/matrix-sdk-crypto-ffi-#{s.version}/MatrixSDKCryptoFFI.zip" }
|
||||
s.vendored_frameworks = "MatrixSDKCryptoFFI.xcframework"
|
||||
|
||||
@@ -2,9 +2,9 @@ import XCTest
|
||||
@testable import MatrixRustSDK
|
||||
|
||||
final class ClientTests: XCTestCase {
|
||||
func testBuildingWithHomeserverURL() {
|
||||
func testBuildingWithHomeserverURL() async {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
_ = try await ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.build()
|
||||
} catch {
|
||||
@@ -12,9 +12,9 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithHomeserverURLAndUserAgent() {
|
||||
func testBuildingWithHomeserverURLAndUserAgent() async {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
_ = try await ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.userAgent(userAgent: "golden-eye/007")
|
||||
.build()
|
||||
@@ -23,24 +23,14 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithUsername() {
|
||||
func testBuildingWithInvalidUsername() async {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:matrix.org")
|
||||
.build()
|
||||
} catch {
|
||||
XCTFail("The client should build successfully when given a username.")
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
_ = try await ClientBuilder()
|
||||
.username(username: "@test:invalid")
|
||||
.build()
|
||||
|
||||
XCTFail("The client should not build when given an invalid username.")
|
||||
} catch ClientError.Generic(let message) {
|
||||
} catch ClientBuildError.Sdk(let message) {
|
||||
XCTAssertTrue(message.contains(".well-known"), "The client should fail to do the well-known lookup.")
|
||||
} catch {
|
||||
XCTFail("Not expecting any other kind of exception")
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eEu
|
||||
|
||||
helpFunction() {
|
||||
echo ""
|
||||
echo "Usage: $0 -only_ios"
|
||||
echo -e "\t-i Option to build only for iOS. Default will build for all targets."
|
||||
exit 1
|
||||
}
|
||||
|
||||
only_ios='false'
|
||||
|
||||
while getopts ':i' 'opt'; do
|
||||
case ${opt} in
|
||||
'i') only_ios='true' ;;
|
||||
?) helpFunction ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Path to the repo root
|
||||
@@ -22,52 +38,63 @@ TARGET_CRATE=matrix-sdk-crypto-ffi
|
||||
# Required by olm-sys crate
|
||||
export IOS_SDK_PATH=`xcrun --show-sdk-path --sdk iphoneos`
|
||||
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
if ${only_ios}; then
|
||||
# iOS
|
||||
echo -e "Building only for iOS"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
else
|
||||
# iOS
|
||||
echo -e "Building for iOS [1/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
# MacOS
|
||||
echo -e "\nBuilding for macOS (Apple Silicon) [2/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
echo -e "\nBuilding for macOS (Intel) [3/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
# iOS Simulator
|
||||
echo -e "\nBuilding for iOS Simulator (Apple Silicon) [4/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
echo -e "\nBuilding for iOS Simulator (Intel) [5/5]"
|
||||
cargo build -p ${TARGET_CRATE} ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
fi
|
||||
|
||||
echo -e "\nCreating XCFramework"
|
||||
# Lipo together the libraries for the same platform
|
||||
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
if ! ${only_ios}; then
|
||||
echo "Lipo together the libraries for the same platform"
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a"
|
||||
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-output "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a"
|
||||
fi
|
||||
|
||||
# Generate uniffi files
|
||||
cargo uniffi-bindgen generate \
|
||||
cd ../matrix-sdk-crypto-ffi && cargo run --bin matrix_sdk_crypto_ffi generate \
|
||||
--language swift \
|
||||
--lib-file "${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--config "${SRC_ROOT}/bindings/${TARGET_CRATE}/uniffi.toml" \
|
||||
--out-dir ${GENERATED_DIR} \
|
||||
"${SRC_ROOT}/bindings/${TARGET_CRATE}/src/olm.udl"
|
||||
--library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
--out-dir ${GENERATED_DIR}
|
||||
|
||||
# Move headers to the right place
|
||||
HEADERS_DIR=${GENERATED_DIR}/headers
|
||||
mkdir -p ${HEADERS_DIR}
|
||||
mv ${GENERATED_DIR}/*.h ${HEADERS_DIR}
|
||||
|
||||
# Rename and move modulemap to the right place
|
||||
mv ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}/module.modulemap
|
||||
# Rename and merge the modulemap files into a single file to the right place
|
||||
for f in ${GENERATED_DIR}/*.modulemap
|
||||
do
|
||||
cat $f; echo;
|
||||
done > ${HEADERS_DIR}/module.modulemap
|
||||
rm ${GENERATED_DIR}/*.modulemap
|
||||
|
||||
# Move source files to the right place
|
||||
SWIFT_DIR="${GENERATED_DIR}/Sources"
|
||||
@@ -77,15 +104,21 @@ mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
|
||||
# Build the xcframework
|
||||
|
||||
if [ -d "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"; fi
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
if ${only_ios}; then
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
else
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/macos/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/simulator/libmatrix_sdk_crypto_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKCryptoFFI.xcframework"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
if [ -d "${GENERATED_DIR}/macos" ]; then rm -rf "${GENERATED_DIR}/macos"; fi
|
||||
|
||||
@@ -12,6 +12,10 @@ publish = false
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "matrix_sdk_crypto_ffi"
|
||||
path = "uniffi-bindgen.rs"
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
@@ -31,7 +35,7 @@ sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
|
||||
uniffi = { workspace = true }
|
||||
uniffi = { workspace = true , features = ["cli"]}
|
||||
vodozemac = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
@@ -41,7 +45,7 @@ features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
workspace = true
|
||||
features = ["qrcode", "automatic-room-key-forwarding"]
|
||||
features = ["qrcode", "automatic-room-key-forwarding", "uniffi"]
|
||||
|
||||
[dependencies.matrix-sdk-sqlite]
|
||||
workspace = true
|
||||
@@ -49,13 +53,16 @@ features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.33.0"
|
||||
default_features = false
|
||||
default-features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.8.0"
|
||||
assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -31,7 +31,6 @@ fn setup_x86_64_android_workaround() {
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
uniffi::generate_scaffolding("./src/olm.udl")?;
|
||||
|
||||
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
|
||||
|
||||
|
||||
@@ -25,9 +25,11 @@ pub struct Device {
|
||||
/// Is our cross signing identity trusted and does the identity trust the
|
||||
/// device.
|
||||
pub cross_signing_trusted: bool,
|
||||
/// The first time this device was seen in local timestamp, seconds since
|
||||
/// epoch.
|
||||
/// The first time this device was seen in local timestamp, milliseconds
|
||||
/// since epoch.
|
||||
pub first_time_seen_ts: u64,
|
||||
/// Whether or not the device is a dehydrated device.
|
||||
pub dehydrated: bool,
|
||||
}
|
||||
|
||||
impl From<InnerDevice> for Device {
|
||||
@@ -42,6 +44,7 @@ impl From<InnerDevice> for Device {
|
||||
locally_trusted: d.is_locally_trusted(),
|
||||
cross_signing_trusted: d.is_cross_signing_trusted(),
|
||||
first_time_seen_ts: d.first_time_seen_ts().0.into(),
|
||||
dehydrated: d.is_dehydrated(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ mod responses;
|
||||
mod users;
|
||||
mod verification;
|
||||
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Context as _;
|
||||
pub use backup_recovery_key::{
|
||||
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
|
||||
};
|
||||
@@ -31,11 +31,10 @@ pub use logger::{set_logger, Logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::SignatureState,
|
||||
olm::{IdentityKeys, InboundGroupSession, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
|
||||
EncryptionSettings as RustEncryptionSettings, LocalTrust,
|
||||
EncryptionSettings as RustEncryptionSettings,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
pub use responses::{
|
||||
@@ -129,10 +128,10 @@ pub struct PickledSession {
|
||||
pub sender_key: String,
|
||||
/// Was the session created using a fallback key.
|
||||
pub created_using_fallback_key: bool,
|
||||
/// The Unix timestamp when the session was created.
|
||||
pub creation_time: String,
|
||||
/// The Unix timestamp when the session was last used.
|
||||
pub last_use_time: String,
|
||||
/// Unix timestamp (in seconds) when the session was created.
|
||||
pub creation_time: u64,
|
||||
/// Unix timestamp (in seconds) when the session was last used.
|
||||
pub last_use_time: u64,
|
||||
}
|
||||
|
||||
/// A pickled version of an `InboundGroupSession`.
|
||||
@@ -244,9 +243,11 @@ async fn migrate_data(
|
||||
user_id: parse_user_id(&data.account.user_id)?,
|
||||
device_id: device_id.clone(),
|
||||
pickle,
|
||||
dehydrated: false, // dehydrated devices are never involved in migration
|
||||
shared: data.account.shared,
|
||||
uploaded_signed_key_count: data.account.uploaded_signed_key_count as u64,
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch(UInt::default()),
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
|
||||
fallback_key_creation_timestamp: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
};
|
||||
let account = matrix_sdk_crypto::olm::Account::from_pickle(pickled_account)?;
|
||||
|
||||
@@ -423,8 +424,12 @@ fn collect_sessions(
|
||||
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
|
||||
.pickle();
|
||||
|
||||
let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?);
|
||||
let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?);
|
||||
let creation_time = SecondsSinceUnixEpoch(
|
||||
UInt::new(session_pickle.creation_time).context("invalid creation timestamp")?,
|
||||
);
|
||||
let last_use_time = SecondsSinceUnixEpoch(
|
||||
UInt::new(session_pickle.last_use_time).context("invalid last use timestamp")?,
|
||||
);
|
||||
|
||||
let pickle = matrix_sdk_crypto::olm::PickledSession {
|
||||
pickle,
|
||||
@@ -839,6 +844,7 @@ impl From<RoomSettings> for RustRoomSettings {
|
||||
Self {
|
||||
algorithm: value.algorithm.into(),
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
..RustRoomSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -881,7 +887,7 @@ fn vodozemac_version() -> String {
|
||||
vodozemac::VERSION.to_owned()
|
||||
}
|
||||
|
||||
uniffi::include_scaffolding!("olm");
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -901,36 +907,35 @@ mod tests {
|
||||
"pickle":"FFGTGho89T3Xgd56l+EedOPV37s09RR8aYnS9305qPKF66LG+ly29YpCibjJOvkwm0dZwN9A2bOH/z7WscriqwZn/p0GE6YSNwLzffCy5iROzYzpYzFe0HtiyJmCQWCezvLc5lHV8YsfD00C1pKGX2R9M1wwp3/n4/3VjtTyPsdnmtwAPu4WdcPSkisCaQ3a6JaSKqv8zYzUjnpzgcpXHvPUR5d5+TzXgrVz3BeCOe8NEOWIW6xYUxFtGteYP0BczOkkJ22t7Css0tSMSrYgCll4zZUGNrd6D9b/z7KwcDnb978epsZ16DcZ/aaTxPdM5uDIkHgF/qHWerfxcaqsqs4EQfJdSgOTeqhjHBw1k0uWF2bByJLK+n7sGkYXEAuTzc4+0XvSFvu3Qp+1bHZuT7QejngRZzyxznORyBxd8la3/JjeJlehSK80OL7zSmohoYZD59S6i3tFWfopjQThJ0/eIyVOhEN/c3tfIcVr3lFEQeokgpCRNOVldhPcQWq994NHaL7jtb6yhUqT1gShY4zYayFL/VRz6nBSXXYwzrC9jho67knqXSri3lIKYevP9aOi384IvzbkinQdumc804dYwiCbs5hZppfEnfhfgiDDm+kVrJ9WaPRF4SySCTlS8jdGmBeL2CfCQ5IcZ5nK6X7tZM3tmtYwva0RuQiTNltp3XTfbMa0EoaEBximv25165hFTpzrWgoszBTpZPfgsMuWENWBcIX4AcLSk0CJ0qzPDeUwvmRcFStstGYV4drs5u5HEqovFSI48CoHPSEZfwwERCI4c/0efZ0CVEfnm8VcMv3AbnAfedD7v3QNdVwWOEhz/fGR76BQi2WjZP4MWvYRJ/vsLO5hcVWUvaJGQs5kANUFZMWuJQeJv3DmkV9kKKXnyfFUerlQ4Uk/5tp2mXiG+adHjuRp/Eeh5V/biCcIaX3rNuIY6MJaPz6SOwlFe79MMBaNwaS3j4Kh/Aq9BRw0QXdjO4CqMI4p2xCE1N5QTPdeaRTHTZ3r7mLkHX3FpZMxitc8vDl9L2FRoSOMMh/sRD1boBCkjrsty9rvTUGYY3li05jBuTXnYMjA4zj79dC9TGo4g+/wi+h537EhtP5+170LwqnIzfHt8yfjbsMMC7iwLpC1C57sTwxpMkNo3nQEvZOfqCxjq+ihiGuL9iN5lSstu9/C4qP2tQll86ASXf1axxRZQlUB0hlLHbEW6/7O7xOU6FTs4yXAZC04souRkggmfhDzZ9kQmN/zRTbqlATFI7l9/0VGxwLOVnCMUhgiDX5yL8CYK9I4ENMLf5zOuO6P3GbYISjEoHC7fUOzQ6OwGgLyI0wCEVdSJzQcdKh+W15VV+eDjhE/qEJHQWx024hTQFTKYHlDn95+lMmRI9BJLP1HU2JW6onVWsTsE5zSYu9jLj739EKfV4gS/pWzoQDRa7a9ZG6+m+RrwyJhCso3gkUekDNobhFlDX6YeH+Btj91N0uS3F9qr8lbo491s/z2fNV42zT4NYObzgrAYDQAV/2WYF8tXtxLV/Jzk8AMmyr/cfNaT2dXxVJKWq+nN2BYHBmg9CCWPJ2aB/1WWIcHfcDOlngtH991gP6246f/DEaVC/Ayxz7bPtSH5tlZ4Xbpc2P4BYxaRp/yxhhQ2C9H2I/PTt3mnNNgky/t8PZrN3W5+eiSVE9sONF8G3mYsa4XFqM+KxfbPUqsrEnrRBmvmJ250hpTPkFcIF775RvvRRKALXdlTKs+S4HKDW7KoP0Dm9+r4RlO0UHpWND9w0WSMItvWQyo0VViXJgZfBjYtWDoO0Ud+Kc7PLWNX6RUKY7RlDjXadJTC4adH6CN3UBC/ouqqfTrYvPOkyd2oKf4RLjEVcFAUIftFbLy+WBcWv8072nnAFJIlm3CxGq++80TyjqFR45P+qfIJavxQNIt5zhHPfMgHjX27OA3+l7rHDxqfMLBPxhtARwlyF+qx1IJiSWbmlHkdz2ylD9unoLSpf+DmmFvvgTj+3EEP4bY2jA/t91XFeG3uaTQSy3ryDvhbX21U7G2HGOEl9rCkmz+hG0YRB/6KxZZ0eMIDr7OWfpPEuHV8oYwDNYbsT9zCGsR1hHxBJtdo60b36mjMemtf761DhJ/oQZ4eU738yzx1hvVS3aCJsfyp70H5u+pUjgrA565uG2lEMNLu4T4NFVw0UdrVudyrhmT8P7vF4v+mR4pp+OzRbLf8AtZrKmHlMqRst+/wOHUHug/Tpz6EwZPDWGiQyFyPUkjHWW7ACouegBFOWFabsk+zCDhyxoSNrSMCtdB1L+qK72jRPGOvXk8p/1kBOIJfAjaK1ZWz8hTc30hOSWYxkRP296zPHiQF0ibNYSPNZ9tNxgq9nV/cEQ68TsNr3SULfDr0TSjCPf4AfmJ0k1k5xphSYv/TtGIbjg/9yGVFqclg4Y/6rrfkApbx36PQEBNxLiRsZ4hGpCfVU6h0jOekk8TV6CAguXVX/G31UqsAEa4sOD2g10Ir+5JD7bdd3JE/999kHGdiCqc0DNcgSqWYbq2QYwrN/mb+mMUbiQSNMcc34kK1n+7dGxppnt7YN7UsJqBWJdH0Lw1Epxi11ViTeVma9bqioJYXi6N5exdpZTT7KmcGYFsoTqO958EX6AppgcML7N9oP3TO8qSgCpV3Bbbemq4bvjV43aM6Rdx17pC4GZo0jjU97p4K8jE4PvgoHlYkuPwSJDOSAdnYPh+Inq/vCk48UfIlup0ATJFVUXD7uf84v9roZSwZPXZ5j/88+MkHBIJwPv8cugmz5uN2EuBW5IScMuEqG7Cmk72SU3/QA39G79S0Gpw7iPhTos5LXxhfvohGcnSaNEvfNeecQf7fpVciTdHwuvcgqJizUKpSFg2P+LDBiO44mJD15RNAaT37Rrj5P06YITO4PDj+FMdc6gx+JQUFbcSRhScE/0gfsVm0P1BYIH5q0k/QDgEVoerf/n19lITTzPib1F2OHP4hyF3BEq1pd9NwuPhhsVVqTVTK5MzFwFIOH7cwJyY7aBykmsWBavdb2J7UA5wjKqMHl1auUGPlNL+lZjqG4tw05bchtFAF+PGWQXJhJCtRSkkzTOCrLRyYyyI9mWYEjoc23cGLanlIs7WA1Nd0Jz+5RSNlf9Gtnd65yQp/W1eqY6yzURPHUUa7FrynyORmjaR9adT9utSQkXy8++IeDNzhMtFr+SqQ/gKECLe0GeuyTs6E5bImUtqpN+xopBXnEeq8wp+bvLf76d98qPE5ibTRwlsSyCE4c1Y7vrJrlc15Yc2R9ciIuKUS8rUKLSdGBFe/TD4R3cPhCKAnnRLGWnJiPPgxoTVwHVZMISdsAjNaWblBmiAOzFcu7443d3PCLyXVcfR9xgvW51HTumo91t5Qyx4HIXGoZxayZYFm2hrhSlieUqLnDL2j2gYgGU5NGoQl4OnEY2QqobpRUF4xJ4HhLzYbLrBeXmTDPvj0MasC3kKsRlm/HrsRRWZ2iPSMw9601tLvDfyjG53ddPISiVNnkdXcaAN5np7dwipdBOC1s4a0sEmKakNbkkDb8LsGBNte/g6UYs5yYaKr0bnXlDjMCznHQa7pypBjE7S55T3UeRpwo3IvZ1tfIGdb+z9RIA/PDvUksxJ3Xq3lqtZzkZJF5aeedfIOekGS/G0LiCSYsELgRceH5veknHqoGoL6xi4Q6/VjmfpZVXT19bDcTNtaR9Dlaq4LDjpQl9rl5C3O/X1hgADvJUuINCiLrD114sLY1DG/TDXE0sp+TK7utnjLAoHuAuj+6anY5vN66CSbwyUNmvo+m8li/AMkRYdtSDoPWkV7Y1ixMBPcua0Llwn2HSKKwnCjvhDIDIIVwbWwb1s6b9cztH81WF5RWUgFujewPvTElM1Sy10y7BcZohKw28uLRFVsKunc9yX2PiQoTSB4PHBHRA4U5dEQV3GHQJ93nee7VT3oeQPMVebWhuhOhi34Z33LQajzpCF3OjIbJb0tOPP6L6N/ODqkNsYViI3kgCnkNhexadOuGFWIqen2Q8iv2uOZWbPirt0YEeKZIk2dpND07L8Q3OsoQCk2rjpnw9LuFrjgu7gN9gFyPq25HJRBn7PM/lS60DF+xVkJq94PwN+CiZWC43SVcBGx65DFZIs/N78MZCUzZbFlsS7FsIrDJt878cp9eZdq/Ai4LZhL8QYHpVUrQxRxZGSqooA755N6nOxw66JkA1VPnjECCMgoNNtWox0JzhMe8PBdh2ZliXf8yQ6/eTvsG6FD84F+49pc7m0L99pfWHb9ClyO3KRHscp/MOIC1MJmqoB4dNxV20U+z8/lSTIvcmM8DiaAZj/yxlst90drlGydlyPjQzYd/XtIYcO5gHoeD1KUCZRapE5dkyk5vh97WZJn/JkR8hsslU3D6x3rNGwJbQVRu0IiA3PpeAQNZBNAJHHfv8IzIYxPhMJdYq0YqLIGSUYu87D04cDOxJY7hgawYs+ExOWb7XkbpuRoITQd8zpwVDFlSCS+wFO+qah3Vn8RBTc6cXHO5xRWfUNj+NrEtPdVmax+9EXqXtHQyFpxaauvL96RH+mGwpKHOk3aisXbZ6gLE2mF4egGjjJOIJdHyb2ZR+kj+4GIvkoBwipDgUfr4UBXY8pvFxQOxRgtI4LgOY9Z1Aco7Mwp6qi1KoMFJW8d+gJwsgM3cMsyEeYH1n/mdpJW6VDbIWzOHkP5n+OKKNm2vJTkQFFwF9eOtGy9fNBtS4qo4jvOUJnnAPsrPbGMbBYd1dMC3daHLEwvIKCAVBn7q1Z2c4zAD5eEoY0EwZj/j8x8lGQ8TswFT81ZotW7ZBDai/YtV8mkGfuaWJRI5yHc/bV7GWLF+yrMji/jicBF5jy2UoqwxseqjgTut49FRgBH3h1qwnfYbXD3FvQljyAAgBCiZV726pFRG+sZv0FjDbq0iCKILVSEUDZgmQ",
|
||||
"shared":true,
|
||||
"uploaded_signed_key_count":50
|
||||
|
||||
},
|
||||
"sessions":[
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4s+SdrKUYAMUdGcYD7QukrPklEOy7fJho9YGK/jV04QdA8JABiOfD+ngJTR4V8eZdmDuG08+Q5EL79V81hQwU2fKndP0y/9nAXPUIADYq0Zrg4EsOnXz7aE+hAeBAm0IBog1s8RYUvynZ15uwjbd/OTLP+gpqpX33DwVg2leiBkQetiUSpOpZCuQ8CcZwIA0MoGCqvaT7h76VHX9JxJx+2fCMhsJMx1nhd99WJH1W9ge5CtdbC4KUP92OSxIrPOnMrNcOPJPp/paZP+HFNQ3PDL+z8pGKXmCnrXGSbd7iPHurPYESrVkBzr",
|
||||
"sender_key":"WJ6Ce7U67a6jqkHYHd8o0+5H4bqdi9hInZdk0+swuXs",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649425011424",
|
||||
"last_use_time":"1649425011424"
|
||||
"creation_time": 1649425011424u64,
|
||||
"last_use_time": 1649425011424u64
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4t2W/lowyrV6SXVZp+uG59im0AAfNSKjhjZuiOpQlX7MS+AOJkCNvyujJ2g3KSjLZ94IkoHxkBDHLWSjwaLPu40rfOzJPDpm0XZsR6bQrsxKOmXLGEw2qw5jOTouzMVL2gvuuTix97nSYSU8j3XvTMRUoh0AF/tUpRLcvEFZeGrdUYmTMlyTv4na+FVUalUZ+jrk8t1/sM99JNq3SY1IBSjrBq/0rCOHieiippz0sw2fe2b87id4rqj1g3R9w2MWTWEdOz3ugjMGYF1YDBQZA1tJZ/hmgppk2AU2xKQXE2X3DgSC6fC66D4",
|
||||
"sender_key":"RzRROfmHNlBfzxnNCUYBfn/5oZNQ11XYjDg59hS+mV0",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649425011503",
|
||||
"last_use_time":"1649425011503"
|
||||
"creation_time": 1649425011503u64,
|
||||
"last_use_time": 1649425011503u64
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4titbL3SS12PYHpcBPJc6hXnOnZXqrjtjYOD545fck+3utEo8cqqwWubc9tsvxGW3tOWPttLBdAW30Vn8V1M8ebqVCNVWEAb1GKjV4ni8xG7G9SlEcCjLjnF4lJpddSZkqVMFoN0ITr9aSz/eJwXpc3HLreUFXwc8LuQp7krQ4Vt1e5EE/klduqsdurZf5V14RHsmWz2lKjt7nVgtIz/dhtF5F/sGJdg8kCGaHIMSbGAPuPPpa4/Laicb/5otrYt4pg4W4KdFpSGJIcvUQNjXaOZMx3cu/RPJIOyNhx7whG1QiYAUBqAJvr",
|
||||
"sender_key":"IXSZugAHig1v8MowE1jxi2wDDDfuZBeJynHlegJVwUc",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649425011566",
|
||||
"last_use_time":"1649425011566"
|
||||
"creation_time": 1649425011566u64,
|
||||
"last_use_time": 1649425011566u64
|
||||
},
|
||||
{
|
||||
"pickle":"SmkDiFZjNukiarQ7XHQo25FILHsuhNOnxy56cMSQU/Y71jaGbJes4YrvN4Dfy4RSONfejEDXDkbW2JudlHHRP/rWEmnfJiGbK6ArbrG2puqIZgOecPnOUgPfCisr49p1Gmf36dPaO5lm/ZSrngfSoxahoeJJE/CcJN98sYM15XytRk2LBwc+CyYDqr4V1qxfsBt6tzJ4+tsAZeRdD0UtipQgysgH56o8N7nKTCkaZz5lfpYCl3FEgwXpLJ0MGQvtQmbORFvOLqR1jZ/EbmNGKiqDDIYsqG0sf78ii1jqfpLDBXLuYDccsg",
|
||||
"sender_key":"EB9SC4jVAydKhM6/GcwMc9biKwVNywqW3TerNTrtb1M",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649542063182",
|
||||
"last_use_time":"1649542063182"
|
||||
"creation_time": 1649542063182u64,
|
||||
"last_use_time": 1649542063182u64
|
||||
}
|
||||
],
|
||||
"inbound_group_sessions":[
|
||||
@@ -1010,7 +1015,8 @@ mod tests {
|
||||
"JGgPQRuYj3ScMdPS+A0P+k/1qS9Hr3qeKXLscI+hS78"
|
||||
);
|
||||
|
||||
let room_keys = machine.runtime.block_on(machine.inner.export_room_keys(|_| true))?;
|
||||
let room_keys =
|
||||
machine.runtime.block_on(machine.inner.store().export_room_keys(|_| true))?;
|
||||
assert_eq!(room_keys.len(), 2);
|
||||
|
||||
let cross_signing_status = machine.cross_signing_status();
|
||||
|
||||
@@ -17,7 +17,7 @@ use matrix_sdk_crypto::{
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
|
||||
LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentities,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
@@ -40,6 +40,7 @@ use ruma::{
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -207,8 +208,12 @@ impl OlmMachine {
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
let inner =
|
||||
runtime.block_on(InnerMachine::with_store(&user_id, device_id, Arc::new(store)))?;
|
||||
let inner = runtime.block_on(InnerMachine::with_store(
|
||||
&user_id,
|
||||
device_id,
|
||||
Arc::new(store),
|
||||
None,
|
||||
))?;
|
||||
|
||||
Ok(Arc::new(OlmMachine { inner: ManuallyDrop::new(inner), runtime }))
|
||||
}
|
||||
@@ -800,6 +805,53 @@ impl OlmMachine {
|
||||
Ok(serde_json::to_string(&encrypted_content)?)
|
||||
}
|
||||
|
||||
/// Encrypt the given event with the given type and content for the given
|
||||
/// device. This method is used to send an event to a specific device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user who owns the target device.
|
||||
/// * `device_id` - The ID of the device to which the message will be sent.
|
||||
/// * `event_type` - The event type.
|
||||
/// * `content` - The serialized content of the event.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result` containing the request to be sent out if the encryption was
|
||||
/// successful. If the device is not found, the result will be `Ok(None)`.
|
||||
///
|
||||
/// The caller should ensure that there is an olm session (see
|
||||
/// `get_missing_sessions`) with the target device before calling this
|
||||
/// method.
|
||||
pub fn create_encrypted_to_device_request(
|
||||
&self,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
event_type: String,
|
||||
content: String,
|
||||
) -> Result<Option<Request>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
let content = serde_json::from_str(&content)?;
|
||||
|
||||
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 request = ToDeviceRequest::new(
|
||||
user_id.as_ref(),
|
||||
DeviceIdOrAllDevices::DeviceId(device_id.to_owned()),
|
||||
"m.room.encrypted",
|
||||
encrypted_content.cast(),
|
||||
);
|
||||
|
||||
Ok(Some(request.into()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt the given event that was sent in the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -917,7 +969,7 @@ impl OlmMachine {
|
||||
passphrase: String,
|
||||
rounds: i32,
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?;
|
||||
let keys = self.runtime.block_on(self.inner.store().export_room_keys(|_| true))?;
|
||||
|
||||
let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32)
|
||||
.map_err(CryptoStoreError::Serialization)?;
|
||||
@@ -943,7 +995,7 @@ impl OlmMachine {
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys = Cursor::new(keys);
|
||||
let keys = decrypt_room_key_export(keys, &passphrase)?;
|
||||
self.import_room_keys_helper(keys, false, progress_listener)
|
||||
self.import_room_keys_helper(keys, None, progress_listener)
|
||||
}
|
||||
|
||||
/// Import room keys from the given serialized unencrypted key export.
|
||||
@@ -953,6 +1005,9 @@ impl OlmMachine {
|
||||
/// should be used if the room keys are coming from the server-side backup,
|
||||
/// the method will mark all imported room keys as backed up.
|
||||
///
|
||||
/// **Note**: This has been deprecated. Use
|
||||
/// [`OlmMachine::import_room_keys_from_backup`] instead.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - The serialized version of the unencrypted key export.
|
||||
@@ -964,11 +1019,38 @@ impl OlmMachine {
|
||||
keys: String,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
// Assume that the keys came from the current backup version.
|
||||
let backup_version = self.runtime.block_on(self.inner.backup_machine().backup_version());
|
||||
let keys: Vec<Value> = serde_json::from_str(&keys)?;
|
||||
|
||||
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
|
||||
self.import_room_keys_helper(keys, backup_version.as_deref(), progress_listener)
|
||||
}
|
||||
|
||||
self.import_room_keys_helper(keys, true, progress_listener)
|
||||
/// Import room keys from the given serialized unencrypted key export.
|
||||
///
|
||||
/// This method is the same as [`OlmMachine::import_room_keys`] but the
|
||||
/// decryption step is skipped and should be performed by the caller. This
|
||||
/// should be used if the room keys are coming from the server-side backup.
|
||||
/// The method will mark all imported room keys as backed up.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - The serialized version of the unencrypted key export.
|
||||
///
|
||||
/// * `backup_version` - The version of the backup that these keys came
|
||||
/// from.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the key import.
|
||||
pub fn import_room_keys_from_backup(
|
||||
&self,
|
||||
keys: String,
|
||||
backup_version: String,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let keys: Vec<Value> = serde_json::from_str(&keys)?;
|
||||
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
|
||||
self.import_room_keys_helper(keys, Some(&backup_version), progress_listener)
|
||||
}
|
||||
|
||||
/// Discard the currently active room key for the given room if there is
|
||||
@@ -976,7 +1058,7 @@ impl OlmMachine {
|
||||
pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?;
|
||||
self.runtime.block_on(self.inner.discard_room_key(&room_id))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1076,8 +1158,7 @@ impl OlmMachine {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
|
||||
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
|
||||
let content =
|
||||
self.runtime.block_on(identity.verification_request_content(Some(methods)));
|
||||
let content = identity.verification_request_content(Some(methods));
|
||||
Some(serde_json::to_string(&content)?)
|
||||
} else {
|
||||
None
|
||||
@@ -1120,11 +1201,7 @@ impl OlmMachine {
|
||||
let methods = methods.into_iter().map(VerificationMethod::from).collect();
|
||||
|
||||
Ok(if let Some(identity) = identity.and_then(|i| i.other()) {
|
||||
let request = self.runtime.block_on(identity.request_verification(
|
||||
&room_id,
|
||||
&event_id,
|
||||
Some(methods),
|
||||
));
|
||||
let request = identity.request_verification(&room_id, &event_id, Some(methods));
|
||||
|
||||
Some(
|
||||
VerificationRequest { inner: request, runtime: self.runtime.handle().to_owned() }
|
||||
@@ -1161,8 +1238,7 @@ impl OlmMachine {
|
||||
if let Some(device) =
|
||||
self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?
|
||||
{
|
||||
let (verification, request) =
|
||||
self.runtime.block_on(device.request_verification_with_methods(methods));
|
||||
let (verification, request) = device.request_verification_with_methods(methods);
|
||||
|
||||
Some(RequestVerificationResult {
|
||||
verification: VerificationRequest {
|
||||
@@ -1452,22 +1528,35 @@ impl OlmMachine {
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Clear any in-memory caches because they may be out of sync with the
|
||||
/// underlying data store.
|
||||
///
|
||||
/// The crypto store layer is caching olm sessions for a given device.
|
||||
/// When used in a multi-process context this cache will get outdated.
|
||||
/// If the machine is used by another process, the cache must be
|
||||
/// invalidating when the main process is resumed.
|
||||
pub async fn clear_crypto_cache(&self) {
|
||||
self.inner.clear_crypto_cache().await
|
||||
}
|
||||
}
|
||||
|
||||
impl OlmMachine {
|
||||
fn import_room_keys_helper(
|
||||
&self,
|
||||
keys: Vec<ExportedRoomKey>,
|
||||
from_backup: bool,
|
||||
from_backup_version: Option<&str>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> Result<KeysImportResult, KeyImportError> {
|
||||
let listener = |progress: usize, total: usize| {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
let result =
|
||||
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
|
||||
let result = self.runtime.block_on(self.inner.store().import_room_keys(
|
||||
keys,
|
||||
from_backup_version,
|
||||
listener,
|
||||
))?;
|
||||
|
||||
Ok(KeysImportResult {
|
||||
imported: result.imported_count as i64,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace matrix_sdk_crypto_ffi {};
|
||||
|
||||
enum LocalTrust {
|
||||
"Verified",
|
||||
"BlackListed",
|
||||
"Ignored",
|
||||
"Unset",
|
||||
};
|
||||
|
||||
enum SignatureState {
|
||||
"Missing",
|
||||
"Invalid",
|
||||
"ValidButNotTrusted",
|
||||
"ValidAndTrusted",
|
||||
};
|
||||
@@ -164,7 +164,6 @@ impl From<OutgoingRequest> for Request {
|
||||
},
|
||||
RoomMessage(r) => Request::from(r),
|
||||
KeysClaim(c) => (r.request_id().to_owned(), c.clone()).into(),
|
||||
KeysBackup(b) => (r.request_id().to_owned(), b.clone()).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,11 @@ pub trait SasListener: Send {
|
||||
/// An Enum describing the state the SAS verification is in.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SasState {
|
||||
/// The verification has been started, the protocols that should be used
|
||||
/// have been proposed and can be accepted.
|
||||
/// The verification has been created, the protocols that should be used
|
||||
/// have been proposed to the other party.
|
||||
Created,
|
||||
/// The verification has been started, the other party proposed the
|
||||
/// protocols that should be used and that can be accepted.
|
||||
Started,
|
||||
/// The verification has been accepted and both sides agreed to a set of
|
||||
/// protocols that will be used for the verification process.
|
||||
@@ -58,6 +61,7 @@ pub enum SasState {
|
||||
impl From<RustSasState> for SasState {
|
||||
fn from(s: RustSasState) -> Self {
|
||||
match s {
|
||||
RustSasState::Created { .. } => Self::Created,
|
||||
RustSasState::Started { .. } => Self::Started,
|
||||
RustSasState::Accepted { .. } => Self::Accepted,
|
||||
RustSasState::KeysExchanged { emojis, decimals } => Self::KeysExchanged {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
@@ -24,17 +24,13 @@ vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
anyhow = { workspace = true }
|
||||
as_variant = { workspace = true }
|
||||
async-compat = "0.2.1"
|
||||
base64 = "0.21"
|
||||
eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
opentelemetry = "0.21.0"
|
||||
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.14.0", features = ["tokio", "reqwest-client", "http-proto"] }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde = { workspace = true }
|
||||
@@ -42,19 +38,16 @@ serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-core = { workspace = true }
|
||||
tracing-opentelemetry = "0.22.0"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = { workspace = true, features = ["time"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url = "2.2.2"
|
||||
url = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
language-tags = "0.3.2"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
paranoid-android = "0.2.1"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies.matrix-sdk]
|
||||
@@ -69,6 +62,7 @@ features = [
|
||||
"rustls-tls", # note: differ from block below
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
@@ -83,4 +77,8 @@ features = [
|
||||
"native-tls", # note: differ from block above
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
|
||||
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.
|
||||
|
||||
# OpenTelemetry support
|
||||
|
||||
The bindings have support for OpenTelemetry, allowing for the upload of traces to
|
||||
any OpenTelemetry collector. This support is provided through the
|
||||
[`opentelemetry`](https://docs.rs/opentelemetry/latest/opentelemetry/)
|
||||
crate, which requires the [`protoc`] binary to be installed during the build
|
||||
process.
|
||||
|
||||
## Platforms
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
path::Path,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::BackupDownloadStrategy,
|
||||
oidc::{
|
||||
registrations::{ClientId, OidcRegistrations, OidcRegistrationsError},
|
||||
types::{
|
||||
client_credentials::ClientCredentials,
|
||||
errors::ClientErrorCode::AccessDenied,
|
||||
@@ -15,30 +18,34 @@ use matrix_sdk::{
|
||||
},
|
||||
AuthorizationResponse, Oidc, OidcError,
|
||||
},
|
||||
AuthSession,
|
||||
};
|
||||
use matrix_sdk_ui::authentication::oidc::{ClientId, OidcRegistrations, OidcRegistrationsError};
|
||||
use ruma::{
|
||||
api::client::discovery::discover_homeserver::AuthenticationServerInfo, IdParseError,
|
||||
OwnedUserId,
|
||||
reqwest::StatusCode,
|
||||
ClientBuildError as MatrixClientBuildError, HttpError, RumaApiError,
|
||||
};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
use url::Url;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::{client::Client, client_builder::ClientBuilder, RUNTIME};
|
||||
use crate::{client::ClientSessionDelegate, client_builder::UrlScheme, error::ClientError};
|
||||
use super::{client::Client, client_builder::ClientBuilder};
|
||||
use crate::{
|
||||
client::ClientSessionDelegate,
|
||||
client_builder::{CertificateBytes, ClientBuildError},
|
||||
error::ClientError,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct AuthenticationService {
|
||||
base_path: String,
|
||||
session_path: String,
|
||||
passphrase: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
client: RwLock<Option<Arc<Client>>>,
|
||||
homeserver_details: RwLock<Option<Arc<HomeserverLoginDetails>>>,
|
||||
client: AsyncRwLock<Option<Client>>,
|
||||
homeserver_details: StdRwLock<Option<Arc<HomeserverLoginDetails>>>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: RwLock<Option<String>>,
|
||||
custom_sliding_sync_proxy: Option<String>,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<CertificateBytes>,
|
||||
proxy: Option<String>,
|
||||
}
|
||||
|
||||
impl Drop for AuthenticationService {
|
||||
@@ -52,14 +59,21 @@ impl Drop for AuthenticationService {
|
||||
pub enum AuthenticationError {
|
||||
#[error("A successful call to configure_homeserver must be made first.")]
|
||||
ClientMissing,
|
||||
#[error("{message}")]
|
||||
InvalidServerName { message: String },
|
||||
|
||||
#[error("The supplied server name is invalid.")]
|
||||
InvalidServerName,
|
||||
#[error(transparent)]
|
||||
ServerUnreachable(HttpError),
|
||||
#[error(transparent)]
|
||||
WellKnownLookupFailed(RumaApiError),
|
||||
#[error(transparent)]
|
||||
WellKnownDeserializationError(DeserializationError),
|
||||
#[error("The homeserver doesn't provide a trusted sliding sync proxy in its well-known configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
|
||||
#[error("Login was successful but is missing a valid Session to configure the file store.")]
|
||||
SessionMissing,
|
||||
#[error("Failed to use the supplied base path.")]
|
||||
InvalidBasePath,
|
||||
|
||||
#[error(
|
||||
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
|
||||
)]
|
||||
@@ -68,12 +82,15 @@ pub enum AuthenticationError {
|
||||
OidcMetadataMissing,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
#[error("Failed to use the supplied registrations file path.")]
|
||||
OidcRegistrationsPathInvalid,
|
||||
#[error("The supplied callback URL used to complete OIDC is invalid.")]
|
||||
OidcCallbackUrlInvalid,
|
||||
#[error("The OIDC login was cancelled by the user.")]
|
||||
OidcCancelled,
|
||||
#[error("An error occurred with OIDC: {message}")]
|
||||
OidcError { message: String },
|
||||
|
||||
#[error("An error occurred: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
@@ -84,16 +101,36 @@ impl From<anyhow::Error> for AuthenticationError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdParseError> for AuthenticationError {
|
||||
fn from(e: IdParseError) -> AuthenticationError {
|
||||
AuthenticationError::InvalidServerName { message: e.to_string() }
|
||||
impl From<ClientBuildError> for AuthenticationError {
|
||||
fn from(e: ClientBuildError) -> AuthenticationError {
|
||||
match e {
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::InvalidServerName) => {
|
||||
AuthenticationError::InvalidServerName
|
||||
}
|
||||
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::Http(e)) => {
|
||||
AuthenticationError::ServerUnreachable(e)
|
||||
}
|
||||
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
|
||||
FromHttpResponseError::Server(e),
|
||||
)) => AuthenticationError::WellKnownLookupFailed(e),
|
||||
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
|
||||
FromHttpResponseError::Deserialization(e),
|
||||
)) => AuthenticationError::WellKnownDeserializationError(e),
|
||||
|
||||
_ => AuthenticationError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcRegistrationsError> for AuthenticationError {
|
||||
fn from(e: OidcRegistrationsError) -> AuthenticationError {
|
||||
match e {
|
||||
OidcRegistrationsError::InvalidBasePath => AuthenticationError::InvalidBasePath,
|
||||
OidcRegistrationsError::InvalidFilePath => {
|
||||
AuthenticationError::OidcRegistrationsPathInvalid
|
||||
}
|
||||
_ => AuthenticationError::OidcError { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -127,6 +164,11 @@ pub struct OidcConfiguration {
|
||||
/// Pre-configured registrations for use with issuers that don't support
|
||||
/// dynamic client registration.
|
||||
pub static_registrations: HashMap<String, String>,
|
||||
|
||||
/// A file path where any dynamic registrations should be stored.
|
||||
///
|
||||
/// Suggested value: `{base_path}/oidc/registrations.json`
|
||||
pub dynamic_registrations_file: String,
|
||||
}
|
||||
|
||||
/// The data required to authenticate against an OIDC server.
|
||||
@@ -150,6 +192,7 @@ impl OidcAuthenticationData {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverLoginDetails {
|
||||
url: String,
|
||||
sliding_sync_proxy: Option<String>,
|
||||
supports_oidc_login: bool,
|
||||
supports_password_login: bool,
|
||||
}
|
||||
@@ -161,6 +204,12 @@ impl HomeserverLoginDetails {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
/// The URL of the discovered or manually set sliding sync proxy,
|
||||
/// if any.
|
||||
pub fn sliding_sync_proxy(&self) -> Option<String> {
|
||||
self.sliding_sync_proxy.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using OIDC.
|
||||
pub fn supports_oidc_login(&self) -> bool {
|
||||
self.supports_oidc_login
|
||||
@@ -172,29 +221,66 @@ impl HomeserverLoginDetails {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl AuthenticationService {
|
||||
/// Creates a new service to authenticate a user with.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `session_path` - A path to the directory where the session data will
|
||||
/// be stored. A new directory **must** be given for each subsequent
|
||||
/// session as the database isn't designed to be shared.
|
||||
///
|
||||
/// * `passphrase` - An optional passphrase to use to encrypt the session
|
||||
/// data.
|
||||
///
|
||||
/// * `user_agent` - An optional user agent to use when making requests.
|
||||
///
|
||||
/// * `additional_root_certificates` - Additional root certificates to trust
|
||||
/// when making requests when built with rustls.
|
||||
///
|
||||
/// * `proxy` - An optional HTTP(S) proxy URL to use when making requests.
|
||||
///
|
||||
/// * `oidc_configuration` - Configuration data about the app to use during
|
||||
/// OIDC authentication. This is required if OIDC authentication is to be
|
||||
/// used.
|
||||
///
|
||||
/// * `custom_sliding_sync_proxy` - An optional sliding sync proxy URL that
|
||||
/// will override the proxy discovered from the homeserver's well-known.
|
||||
///
|
||||
/// * `session_delegate` - A delegate that will handle token refresh etc.
|
||||
/// when the cross-process lock is configured.
|
||||
///
|
||||
/// * `cross_process_refresh_lock_id` - A process ID to use for
|
||||
/// cross-process token refresh locks.
|
||||
#[uniffi::constructor]
|
||||
// TODO: This has too many arguments, even clippy agrees. Many of these methods are the same as
|
||||
// for the `ClientBuilder`. We should let people pass in a `ClientBuilder` and possibly convert
|
||||
// this to a builder pattern as well.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
base_path: String,
|
||||
session_path: String,
|
||||
passphrase: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
proxy: Option<String>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: Option<String>,
|
||||
session_delegate: Option<Box<dyn ClientSessionDelegate>>,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(AuthenticationService {
|
||||
base_path,
|
||||
session_path,
|
||||
passphrase,
|
||||
user_agent,
|
||||
client: RwLock::new(None),
|
||||
homeserver_details: RwLock::new(None),
|
||||
client: AsyncRwLock::new(None),
|
||||
homeserver_details: StdRwLock::new(None),
|
||||
oidc_configuration,
|
||||
custom_sliding_sync_proxy: RwLock::new(custom_sliding_sync_proxy),
|
||||
custom_sliding_sync_proxy,
|
||||
session_delegate: session_delegate.map(Into::into),
|
||||
cross_process_refresh_lock_id,
|
||||
additional_root_certificates,
|
||||
proxy,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,98 +290,100 @@ impl AuthenticationService {
|
||||
|
||||
/// Updates the service to authenticate with the homeserver for the
|
||||
/// specified address.
|
||||
pub fn configure_homeserver(
|
||||
pub async fn configure_homeserver(
|
||||
&self,
|
||||
server_name_or_homeserver_url: String,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
let mut builder = self.new_client_builder();
|
||||
let builder =
|
||||
self.new_client_builder()?.server_name_or_homeserver_url(server_name_or_homeserver_url);
|
||||
|
||||
// Attempt discovery as a server name first.
|
||||
let result = matrix_sdk::sanitize_server_name(&server_name_or_homeserver_url);
|
||||
let client = builder.build_inner().await?;
|
||||
|
||||
match result {
|
||||
Ok(server_name) => {
|
||||
let protocol = if server_name_or_homeserver_url.starts_with("http://") {
|
||||
UrlScheme::Http
|
||||
} else {
|
||||
UrlScheme::Https
|
||||
};
|
||||
builder = builder.server_name_with_protocol(server_name.to_string(), protocol);
|
||||
// Compute homeserver login details.
|
||||
let details = {
|
||||
let supports_oidc_login =
|
||||
client.inner.oidc().fetch_authentication_issuer().await.is_ok();
|
||||
let supports_password_login =
|
||||
client.supports_password_login().await.ok().unwrap_or(false);
|
||||
let sliding_sync_proxy =
|
||||
client.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string());
|
||||
|
||||
HomeserverLoginDetails {
|
||||
url: client.homeserver(),
|
||||
sliding_sync_proxy,
|
||||
supports_oidc_login,
|
||||
supports_password_login,
|
||||
}
|
||||
};
|
||||
|
||||
Err(e) => {
|
||||
// When the input isn't a valid server name check it is a URL.
|
||||
// If this is the case, build the client with a homeserver URL.
|
||||
if Url::parse(&server_name_or_homeserver_url).is_ok() {
|
||||
builder = builder.homeserver_url(server_name_or_homeserver_url.clone());
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client = builder.build_inner().or_else(|e| {
|
||||
if !server_name_or_homeserver_url.starts_with("http://")
|
||||
&& !server_name_or_homeserver_url.starts_with("https://")
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
// When discovery fails, fallback to the homeserver URL if supplied.
|
||||
let mut builder = self.new_client_builder();
|
||||
builder = builder.homeserver_url(server_name_or_homeserver_url);
|
||||
builder.build_inner()
|
||||
})?;
|
||||
|
||||
let details = RUNTIME.block_on(self.details_from_client(&client))?;
|
||||
|
||||
// Now we've verified that it's a valid homeserver, make sure
|
||||
// there's a sliding sync proxy available one way or another.
|
||||
if self.custom_sliding_sync_proxy.read().unwrap().is_none()
|
||||
&& client.discovered_sliding_sync_proxy().is_none()
|
||||
{
|
||||
// Make sure there's a sliding sync proxy available.
|
||||
if self.custom_sliding_sync_proxy.is_none() && details.sliding_sync_proxy().is_none() {
|
||||
return Err(AuthenticationError::SlidingSyncNotAvailable);
|
||||
}
|
||||
|
||||
*self.client.write().unwrap() = Some(client);
|
||||
*self.client.write().await = Some(client);
|
||||
*self.homeserver_details.write().unwrap() = Some(Arc::new(details));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs a password login using the current homeserver.
|
||||
pub fn login(
|
||||
pub async fn login(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
initial_device_name: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
let client_guard = self.client.read().await;
|
||||
let Some(client) = client_guard.as_ref() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
// Login and ask the server for the full user ID as this could be different from
|
||||
// the username that was entered.
|
||||
client.login(username, password, initial_device_name, device_id).map_err(|e| match e {
|
||||
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
|
||||
})?;
|
||||
let whoami = client.whoami()?;
|
||||
let session =
|
||||
client.inner.matrix_auth().session().ok_or(AuthenticationError::SessionMissing)?;
|
||||
client.login(username, password, initial_device_name, device_id).await.map_err(
|
||||
|e| match e {
|
||||
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
|
||||
},
|
||||
)?;
|
||||
|
||||
self.finalize_client(client, session, whoami.user_id)
|
||||
drop(client_guard);
|
||||
|
||||
// Now that the client is logged in we can take ownership away from the service
|
||||
// to ensure there aren't two clients at any point later.
|
||||
let Some(client) = self.client.write().await.take() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
|
||||
/// Requests the URL needed for login in a web view using OIDC. Once the web
|
||||
/// view has succeeded, call `login_with_oidc_callback` with the callback it
|
||||
/// returns.
|
||||
pub fn url_for_oidc_login(&self) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
pub async fn url_for_oidc_login(
|
||||
&self,
|
||||
) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
|
||||
let client_guard = self.client.read().await;
|
||||
let Some(client) = client_guard.as_ref() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
let Some(authentication_server) = client.discovered_authentication_server() else {
|
||||
return Err(AuthenticationError::OidcNotSupported);
|
||||
let oidc = client.inner.oidc();
|
||||
|
||||
let issuer = match oidc.fetch_authentication_issuer().await {
|
||||
Ok(issuer) => issuer,
|
||||
Err(error) => {
|
||||
if error
|
||||
.as_client_api_error()
|
||||
.is_some_and(|err| err.status_code == StatusCode::NOT_FOUND)
|
||||
{
|
||||
return Err(AuthenticationError::OidcNotSupported);
|
||||
} else {
|
||||
return Err(AuthenticationError::ServerUnreachable(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let Some(oidc_configuration) = &self.oidc_configuration else {
|
||||
@@ -305,27 +393,24 @@ impl AuthenticationService {
|
||||
let redirect_url = Url::parse(&oidc_configuration.redirect_uri)
|
||||
.map_err(|_e| AuthenticationError::OidcMetadataInvalid)?;
|
||||
|
||||
let oidc = client.inner.oidc();
|
||||
self.configure_oidc(&oidc, issuer, oidc_configuration).await?;
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
self.configure_oidc(&oidc, authentication_server, oidc_configuration).await?;
|
||||
let mut data_builder = oidc.login(redirect_url, None)?;
|
||||
// TODO: Add a check for the Consent prompt when MAS is updated.
|
||||
data_builder = data_builder.prompt(vec![Prompt::Consent]);
|
||||
let data = data_builder.build().await?;
|
||||
|
||||
let mut data_builder = oidc.login(redirect_url, None)?;
|
||||
// TODO: Add a check for the Consent prompt when MAS is updated.
|
||||
data_builder = data_builder.prompt(vec![Prompt::Consent]);
|
||||
let data = data_builder.build().await?;
|
||||
|
||||
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
|
||||
})
|
||||
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
|
||||
}
|
||||
|
||||
/// Completes the OIDC login process.
|
||||
pub fn login_with_oidc_callback(
|
||||
pub async fn login_with_oidc_callback(
|
||||
&self,
|
||||
authentication_data: Arc<OidcAuthenticationData>,
|
||||
callback_url: String,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
let client_guard = self.client.read().await;
|
||||
let Some(client) = client_guard.as_ref() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
@@ -354,44 +439,59 @@ impl AuthenticationService {
|
||||
return Err(AuthenticationError::OidcCallbackUrlInvalid);
|
||||
};
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
oidc.finish_authorization(code).await?;
|
||||
oidc.finish_authorization(code).await?;
|
||||
|
||||
oidc.finish_login()
|
||||
.await
|
||||
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })
|
||||
})?;
|
||||
oidc.finish_login()
|
||||
.await
|
||||
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })?;
|
||||
|
||||
let user_id = client.inner.user_id().unwrap().to_owned();
|
||||
let session =
|
||||
client.inner.oidc().full_session().ok_or(AuthenticationError::SessionMissing)?;
|
||||
self.finalize_client(client, session, user_id)
|
||||
drop(client_guard);
|
||||
|
||||
// Now that the client is logged in we can take ownership away from the service
|
||||
// to ensure there aren't two clients at any point later.
|
||||
let Some(client) = self.client.write().await.take() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticationService {
|
||||
/// A new client builder pre-configured with the service's base path and
|
||||
/// user agent if specified
|
||||
fn new_client_builder(&self) -> Arc<ClientBuilder> {
|
||||
let mut builder = ClientBuilder::new().base_path(self.base_path.clone());
|
||||
/// Create a new client builder that is pre-configured with the parameters
|
||||
/// passed to the service along with some other sensible defaults
|
||||
fn new_client_builder(&self) -> Result<Arc<ClientBuilder>, AuthenticationError> {
|
||||
let mut builder = ClientBuilder::new()
|
||||
.session_path(self.session_path.clone())
|
||||
.passphrase(self.passphrase.clone())
|
||||
.sliding_sync_proxy(self.custom_sliding_sync_proxy.clone())
|
||||
.auto_enable_cross_signing(true)
|
||||
.backup_download_strategy(BackupDownloadStrategy::AfterDecryptionFailure)
|
||||
.auto_enable_backups(true);
|
||||
|
||||
if let Some(user_agent) = self.user_agent.clone() {
|
||||
builder = builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
if let Some(proxy) = &self.proxy {
|
||||
builder = builder.proxy(proxy.to_owned())
|
||||
}
|
||||
|
||||
/// Get the homeserver login details from a client.
|
||||
async fn details_from_client(
|
||||
&self,
|
||||
client: &Arc<Client>,
|
||||
) -> Result<HomeserverLoginDetails, AuthenticationError> {
|
||||
let supports_oidc_login = client.discovered_authentication_server().is_some();
|
||||
let supports_password_login = client.supports_password_login().await.ok().unwrap_or(false);
|
||||
let url = client.homeserver();
|
||||
builder = builder.add_root_certificates(self.additional_root_certificates.clone());
|
||||
|
||||
Ok(HomeserverLoginDetails { url, supports_oidc_login, supports_password_login })
|
||||
if let Some(id) = &self.cross_process_refresh_lock_id {
|
||||
let Some(ref session_delegate) = self.session_delegate else {
|
||||
return Err(AuthenticationError::OidcError {
|
||||
message: "cross-process refresh lock requires session delegate".to_owned(),
|
||||
});
|
||||
};
|
||||
builder = builder
|
||||
.enable_cross_process_refresh_lock_inner(id.clone(), session_delegate.clone());
|
||||
} else if let Some(ref session_delegate) = self.session_delegate {
|
||||
builder = builder.set_session_delegate_inner(session_delegate.clone());
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
|
||||
/// Handle any necessary configuration in order for login via OIDC to
|
||||
@@ -401,7 +501,7 @@ impl AuthenticationService {
|
||||
async fn configure_oidc(
|
||||
&self,
|
||||
oidc: &Oidc,
|
||||
authentication_server: AuthenticationServerInfo,
|
||||
issuer: String,
|
||||
configuration: &OidcConfiguration,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
if oidc.client_credentials().is_some() {
|
||||
@@ -409,34 +509,42 @@ impl AuthenticationService {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let oidc_metadata = self.oidc_metadata(configuration)?;
|
||||
let oidc_metadata: VerifiedClientMetadata = configuration.try_into()?;
|
||||
let registrations_file = Path::new(&configuration.dynamic_registrations_file);
|
||||
|
||||
if self.load_client_registration(oidc, &authentication_server, oidc_metadata.clone()).await
|
||||
{
|
||||
if self.load_client_registration(
|
||||
oidc,
|
||||
issuer.clone(),
|
||||
oidc_metadata.clone(),
|
||||
registrations_file,
|
||||
) {
|
||||
tracing::info!("OIDC configuration loaded from disk.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("Registering this client for OIDC.");
|
||||
let registration_response = oidc
|
||||
.register_client(&authentication_server.issuer, oidc_metadata.clone(), None)
|
||||
.await?;
|
||||
let registration_response =
|
||||
oidc.register_client(&issuer, oidc_metadata.clone(), None).await?;
|
||||
|
||||
// The format of the credentials changes according to the client metadata that
|
||||
// was sent. Public clients only get a client ID.
|
||||
let credentials =
|
||||
ClientCredentials::None { client_id: registration_response.client_id.clone() };
|
||||
oidc.restore_registered_client(authentication_server, oidc_metadata, credentials);
|
||||
oidc.restore_registered_client(issuer, oidc_metadata, credentials);
|
||||
|
||||
tracing::info!("Persisting OIDC registration data.");
|
||||
self.store_client_registration(oidc).await?;
|
||||
self.store_client_registration(oidc, registrations_file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores the current OIDC dynamic client registration so it can be re-used
|
||||
/// if we ever log in via the same issuer again.
|
||||
async fn store_client_registration(&self, oidc: &Oidc) -> Result<(), AuthenticationError> {
|
||||
fn store_client_registration(
|
||||
&self,
|
||||
oidc: &Oidc,
|
||||
registrations_file: &Path,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
let issuer = Url::parse(oidc.issuer().ok_or(AuthenticationError::OidcNotSupported)?)
|
||||
.map_err(|_| AuthenticationError::OidcError {
|
||||
message: String::from("Failed to parse issuer URL."),
|
||||
@@ -454,7 +562,7 @@ impl AuthenticationService {
|
||||
})?;
|
||||
|
||||
let registrations = OidcRegistrations::new(
|
||||
&self.base_path,
|
||||
registrations_file,
|
||||
metadata.clone(),
|
||||
self.oidc_static_registrations(),
|
||||
)?;
|
||||
@@ -465,30 +573,31 @@ impl AuthenticationService {
|
||||
|
||||
/// Attempts to load an existing OIDC dynamic client registration for the
|
||||
/// currently configured issuer.
|
||||
async fn load_client_registration(
|
||||
fn load_client_registration(
|
||||
&self,
|
||||
oidc: &Oidc,
|
||||
authentication_server: &AuthenticationServerInfo,
|
||||
issuer: String,
|
||||
oidc_metadata: VerifiedClientMetadata,
|
||||
registrations_file: &Path,
|
||||
) -> bool {
|
||||
let Ok(issuer) = Url::parse(&authentication_server.issuer) else {
|
||||
tracing::error!("Failed to parse {:?}", authentication_server.issuer);
|
||||
let Ok(issuer_url) = Url::parse(&issuer) else {
|
||||
tracing::error!("Failed to parse {issuer:?}");
|
||||
return false;
|
||||
};
|
||||
let Some(registrations) = OidcRegistrations::new(
|
||||
&self.base_path,
|
||||
registrations_file,
|
||||
oidc_metadata.clone(),
|
||||
self.oidc_static_registrations(),
|
||||
)
|
||||
.ok() else {
|
||||
return false;
|
||||
};
|
||||
let Some(client_id) = registrations.client_id(&issuer) else {
|
||||
let Some(client_id) = registrations.client_id(&issuer_url) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
oidc.restore_registered_client(
|
||||
authentication_server.clone(),
|
||||
issuer,
|
||||
oidc_metadata,
|
||||
ClientCredentials::None { client_id: client_id.0 },
|
||||
);
|
||||
@@ -496,41 +605,6 @@ impl AuthenticationService {
|
||||
true
|
||||
}
|
||||
|
||||
/// Creates and verifies OIDC client metadata for the supplied OIDC
|
||||
/// configuration.
|
||||
fn oidc_metadata(
|
||||
&self,
|
||||
configuration: &OidcConfiguration,
|
||||
) -> Result<VerifiedClientMetadata, AuthenticationError> {
|
||||
let redirect_uri = Url::parse(&configuration.redirect_uri)
|
||||
.map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
|
||||
let client_name =
|
||||
configuration.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
|
||||
let client_uri = configuration.client_uri.localized_url()?;
|
||||
let logo_uri = configuration.logo_uri.localized_url()?;
|
||||
let policy_uri = configuration.policy_uri.localized_url()?;
|
||||
let tos_uri = configuration.tos_uri.localized_url()?;
|
||||
let contacts = configuration.contacts.clone();
|
||||
|
||||
ClientMetadata {
|
||||
application_type: Some(ApplicationType::Native),
|
||||
redirect_uris: Some(vec![redirect_uri]),
|
||||
grant_types: Some(vec![GrantType::RefreshToken, GrantType::AuthorizationCode]),
|
||||
// A native client shouldn't use authentication as the credentials could be intercepted.
|
||||
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
|
||||
// The server should display the following fields when getting the user's consent.
|
||||
client_name,
|
||||
contacts,
|
||||
client_uri,
|
||||
logo_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
..Default::default()
|
||||
}
|
||||
.validate()
|
||||
.map_err(|_| AuthenticationError::OidcMetadataInvalid)
|
||||
}
|
||||
|
||||
fn oidc_static_registrations(&self) -> HashMap<Url, ClientId> {
|
||||
let registrations = self
|
||||
.oidc_configuration
|
||||
@@ -548,50 +622,42 @@ impl AuthenticationService {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new client to setup the store path now the user ID is known.
|
||||
fn finalize_client(
|
||||
&self,
|
||||
client: Arc<Client>,
|
||||
session: impl Into<AuthSession>,
|
||||
user_id: OwnedUserId,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let homeserver_url = client.homeserver();
|
||||
impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
|
||||
type Error = AuthenticationError;
|
||||
|
||||
let sliding_sync_proxy: Option<String>;
|
||||
if let Some(custom_proxy) = self.custom_sliding_sync_proxy.read().unwrap().clone() {
|
||||
sliding_sync_proxy = Some(custom_proxy);
|
||||
} else if let Some(discovered_proxy) = client.discovered_sliding_sync_proxy() {
|
||||
sliding_sync_proxy = Some(discovered_proxy.to_string());
|
||||
} else {
|
||||
sliding_sync_proxy = None;
|
||||
fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
|
||||
let redirect_uri = Url::parse(&self.redirect_uri)
|
||||
.map_err(|_| AuthenticationError::OidcCallbackUrlInvalid)?;
|
||||
let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
|
||||
let client_uri = self.client_uri.localized_url()?;
|
||||
let logo_uri = self.logo_uri.localized_url()?;
|
||||
let policy_uri = self.policy_uri.localized_url()?;
|
||||
let tos_uri = self.tos_uri.localized_url()?;
|
||||
let contacts = self.contacts.clone();
|
||||
|
||||
ClientMetadata {
|
||||
application_type: Some(ApplicationType::Native),
|
||||
redirect_uris: Some(vec![redirect_uri]),
|
||||
grant_types: Some(vec![
|
||||
GrantType::RefreshToken,
|
||||
GrantType::AuthorizationCode,
|
||||
GrantType::DeviceCode,
|
||||
]),
|
||||
// A native client shouldn't use authentication as the credentials could be intercepted.
|
||||
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
|
||||
// The server should display the following fields when getting the user's consent.
|
||||
client_name,
|
||||
contacts,
|
||||
client_uri,
|
||||
logo_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
let mut client = self
|
||||
.new_client_builder()
|
||||
.passphrase(self.passphrase.clone())
|
||||
.homeserver_url(homeserver_url)
|
||||
.sliding_sync_proxy(sliding_sync_proxy)
|
||||
.username(user_id.to_string());
|
||||
|
||||
if let Some(id) = &self.cross_process_refresh_lock_id {
|
||||
let Some(ref session_delegate) = self.session_delegate else {
|
||||
return Err(AuthenticationError::OidcError {
|
||||
message: "cross-process refresh lock requires session delegate".to_owned(),
|
||||
});
|
||||
};
|
||||
client = client
|
||||
.enable_cross_process_refresh_lock_inner(id.clone(), session_delegate.clone());
|
||||
} else if let Some(ref session_delegate) = self.session_delegate {
|
||||
client = client.set_session_delegate_inner(session_delegate.clone());
|
||||
}
|
||||
|
||||
let client = client.build_inner()?;
|
||||
|
||||
// Restore the client using the session from the login request.
|
||||
client.restore_session_inner(session)?;
|
||||
|
||||
Ok(client)
|
||||
.validate()
|
||||
.map_err(|_| AuthenticationError::OidcMetadataInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,17 @@ use anyhow::{anyhow, Context as _};
|
||||
use matrix_sdk::{
|
||||
media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize},
|
||||
oidc::{
|
||||
requests::account_management::AccountManagementActionFull,
|
||||
types::{
|
||||
client_credentials::ClientCredentials,
|
||||
registration::{
|
||||
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
|
||||
},
|
||||
},
|
||||
OidcAccountManagementAction, OidcSession,
|
||||
OidcSession,
|
||||
},
|
||||
ruma::{
|
||||
api::client::{
|
||||
account::whoami,
|
||||
media::get_content_thumbnail::v3::Method,
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
@@ -32,16 +32,23 @@ use matrix_sdk::{
|
||||
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventEncryptionAlgorithm, TransactionId, UInt, UserId,
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_ui::notification_client::NotificationProcessSetup as MatrixNotificationProcessSetup;
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationProcessSetup as MatrixNotificationProcessSetup,
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::discovery::discover_homeserver::AuthenticationServerInfo,
|
||||
events::room::power_levels::RoomPowerLevelsEventContent,
|
||||
api::client::{alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -53,8 +60,10 @@ use super::{room::Room, session_verification::SessionVerificationController, RUN
|
||||
use crate::{
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClientBuilder,
|
||||
notification::NotificationClient,
|
||||
notification_settings::NotificationSettings,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
ClientError,
|
||||
@@ -137,6 +146,14 @@ pub trait ProgressWatcher: Send + Sync {
|
||||
fn transmission_progress(&self, progress: TransmissionProgress);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) error reporter of the send queue.
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait SendQueueRoomErrorListener: Sync + Send {
|
||||
/// Called every time the send queue has ran into an error for a given room,
|
||||
/// which will disable the send queue for that particular room.
|
||||
fn on_error(&self, room_id: String, error: ClientError);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct TransmissionProgress {
|
||||
pub current: u64,
|
||||
@@ -175,11 +192,11 @@ impl Drop for Client {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(
|
||||
pub async fn new(
|
||||
sdk_client: MatrixClient,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
) -> Result<Arc<Self>, ClientError> {
|
||||
) -> Result<Self, ClientError> {
|
||||
let session_verification_controller: Arc<
|
||||
tokio::sync::RwLock<Option<SessionVerificationController>>,
|
||||
> = Default::default();
|
||||
@@ -193,11 +210,11 @@ impl Client {
|
||||
}
|
||||
});
|
||||
|
||||
let client = Arc::new(Client {
|
||||
let client = Client {
|
||||
inner: ManuallyDrop::new(sdk_client),
|
||||
delegate: RwLock::new(None),
|
||||
session_verification_controller,
|
||||
});
|
||||
};
|
||||
|
||||
if let Some(process_id) = cross_process_refresh_lock_id {
|
||||
if session_delegate.is_none() {
|
||||
@@ -205,9 +222,7 @@ impl Client {
|
||||
"missing session delegates when enabling the cross-process lock"
|
||||
))?;
|
||||
}
|
||||
RUNTIME.block_on(async {
|
||||
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await
|
||||
})?;
|
||||
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
|
||||
}
|
||||
|
||||
if let Some(session_delegate) = session_delegate {
|
||||
@@ -224,9 +239,7 @@ impl Client {
|
||||
let session_delegate = session_delegate.clone();
|
||||
Box::new(move |client| {
|
||||
let session_delegate = session_delegate.clone();
|
||||
Box::pin(
|
||||
async move { Ok(Self::save_session(session_delegate, client).await?) },
|
||||
)
|
||||
Ok(Self::save_session(session_delegate, client)?)
|
||||
})
|
||||
},
|
||||
)?;
|
||||
@@ -239,24 +252,22 @@ impl Client {
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Client {
|
||||
/// Login using a username and password.
|
||||
pub fn login(
|
||||
pub async fn login(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
initial_device_name: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
}
|
||||
if let Some(device_id) = device_id.as_ref() {
|
||||
builder = builder.device_id(device_id);
|
||||
}
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
})
|
||||
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
}
|
||||
if let Some(device_id) = device_id.as_ref() {
|
||||
builder = builder.device_id(device_id);
|
||||
}
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_media_file(
|
||||
@@ -286,11 +297,11 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
pub fn restore_session(&self, session: Session) -> Result<(), ClientError> {
|
||||
pub async fn restore_session(&self, session: Session) -> Result<(), ClientError> {
|
||||
let sliding_sync_proxy = session.sliding_sync_proxy.clone();
|
||||
let auth_session: AuthSession = session.try_into()?;
|
||||
|
||||
self.restore_session_inner(auth_session)?;
|
||||
self.restore_session_inner(auth_session).await?;
|
||||
|
||||
if let Some(sliding_sync_proxy) = sliding_sync_proxy {
|
||||
let sliding_sync_proxy = Url::parse(&sliding_sync_proxy)
|
||||
@@ -301,33 +312,57 @@ impl Client {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enables or disables all the room send queues at once.
|
||||
///
|
||||
/// When connectivity is lost on a device, it is recommended to disable the
|
||||
/// room sending queues.
|
||||
///
|
||||
/// This can be controlled for individual rooms, using
|
||||
/// [`Room::enable_send_queue`].
|
||||
pub fn enable_all_send_queues(&self, enable: bool) {
|
||||
self.inner.send_queue().set_enabled(enable);
|
||||
}
|
||||
|
||||
/// Subscribe to the global enablement status of the send queue, at the
|
||||
/// client-wide level.
|
||||
///
|
||||
/// The given listener will be immediately called with the initial value of
|
||||
/// the enablement status.
|
||||
pub fn subscribe_to_send_queue_status(
|
||||
&self,
|
||||
listener: Box<dyn SendQueueRoomErrorListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.send_queue().subscribe_errors();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(report) => listener
|
||||
.on_error(report.room_id.to_string(), ClientError::new(report.error)),
|
||||
Err(err) => {
|
||||
error!("error when listening to the send queue error reporter: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Restores the client from an `AuthSession`.
|
||||
pub(crate) fn restore_session_inner(
|
||||
pub(crate) async fn restore_session_inner(
|
||||
&self,
|
||||
session: impl Into<AuthSession>,
|
||||
) -> anyhow::Result<()> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.restore_session(session).await?;
|
||||
Ok(())
|
||||
})
|
||||
self.inner.restore_session(session).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The homeserver's trusted OIDC Provider that was discovered in the
|
||||
/// well-known.
|
||||
///
|
||||
/// This will only be set if the homeserver supports authenticating via
|
||||
/// OpenID Connect and this `Client` was constructed using auto-discovery by
|
||||
/// setting the homeserver with [`ClientBuilder::server_name()`].
|
||||
pub(crate) fn discovered_authentication_server(&self) -> Option<AuthenticationServerInfo> {
|
||||
self.inner.oidc().authentication_server_info().cloned()
|
||||
}
|
||||
|
||||
/// The sliding sync proxy that is trusted by the homeserver. `None` when
|
||||
/// not configured.
|
||||
pub fn discovered_sliding_sync_proxy(&self) -> Option<Url> {
|
||||
/// The sliding sync proxy of the homeserver. It is either set automatically
|
||||
/// during discovery or manually via `set_sliding_sync_proxy` or `None`
|
||||
/// when not configured.
|
||||
pub fn sliding_sync_proxy(&self) -> Option<Url> {
|
||||
self.inner.sliding_sync_proxy()
|
||||
}
|
||||
|
||||
@@ -340,12 +375,6 @@ impl Client {
|
||||
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Password(_)));
|
||||
Ok(supports_password)
|
||||
}
|
||||
|
||||
/// Gets information about the owner of a given access token.
|
||||
pub(crate) fn whoami(&self) -> anyhow::Result<whoami::v3::Response> {
|
||||
RUNTIME
|
||||
.block_on(async move { self.inner.whoami().await.map_err(|e| anyhow!(e.to_string())) })
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
@@ -376,14 +405,14 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn session(&self) -> Result<Session, ClientError> {
|
||||
RUNTIME.block_on(async move { Self::session_inner((*self.inner).clone()).await })
|
||||
Self::session_inner((*self.inner).clone())
|
||||
}
|
||||
|
||||
pub fn account_url(
|
||||
pub async fn account_url(
|
||||
&self,
|
||||
action: Option<AccountManagementAction>,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
match self.inner.oidc().account_management_url(action.map(Into::into)) {
|
||||
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
|
||||
Ok(url) => Ok(url.map(|u| u.to_string())),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed retrieving account management URL: {e}");
|
||||
@@ -397,57 +426,43 @@ impl Client {
|
||||
Ok(user_id.to_string())
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Result<String, ClientError> {
|
||||
let l = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let display_name = l.account().get_display_name().await?.context("No User ID found")?;
|
||||
Ok(display_name)
|
||||
})
|
||||
pub async fn display_name(&self) -> Result<String, ClientError> {
|
||||
let display_name =
|
||||
self.inner.account().get_display_name().await?.context("No User ID found")?;
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
pub fn set_display_name(&self, name: String) -> Result<(), ClientError> {
|
||||
let client = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
client
|
||||
.account()
|
||||
.set_display_name(Some(name.as_str()))
|
||||
.await
|
||||
.context("Unable to set display name")?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.account()
|
||||
.set_display_name(Some(name.as_str()))
|
||||
.await
|
||||
.context("Unable to set display name")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
|
||||
let client = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
client.account().upload_avatar(&mime, data).await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner.account().upload_avatar(&mime, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_avatar(&self) -> Result<(), ClientError> {
|
||||
let client = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
client.account().set_avatar_url(None).await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
|
||||
self.inner.account().set_avatar_url(None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self) -> Result<Option<String>, ClientError> {
|
||||
let l = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let avatar_url = l.account().get_avatar_url().await?;
|
||||
Ok(avatar_url.map(|u| u.to_string()))
|
||||
})
|
||||
/// Sends a request to retrieve the avatar URL. Will fill the cache used by
|
||||
/// [`Self::cached_avatar_url`] on success.
|
||||
pub async fn avatar_url(&self) -> Result<Option<String>, ClientError> {
|
||||
let avatar_url = self.inner.account().get_avatar_url().await?;
|
||||
|
||||
Ok(avatar_url.map(|u| u.to_string()))
|
||||
}
|
||||
|
||||
/// Retrieves an avatar cached from a previous call to [`Self::avatar_url`].
|
||||
pub fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
|
||||
let l = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let url = l.account().get_cached_avatar_url().await?;
|
||||
Ok(url)
|
||||
})
|
||||
Ok(RUNTIME.block_on(self.inner.account().get_cached_avatar_url())?)
|
||||
}
|
||||
|
||||
pub fn device_id(&self) -> Result<String, ClientError> {
|
||||
@@ -455,35 +470,31 @@ impl Client {
|
||||
Ok(device_id.to_string())
|
||||
}
|
||||
|
||||
pub fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
|
||||
let client = self.inner.clone();
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
let response = client.create_room(request.into()).await?;
|
||||
Ok(String::from(response.room_id()))
|
||||
})
|
||||
pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
|
||||
let response = self.inner.create_room(request.into()).await?;
|
||||
Ok(String::from(response.room_id()))
|
||||
}
|
||||
|
||||
/// Get the content of the event of the given type out of the account data
|
||||
/// store.
|
||||
///
|
||||
/// It will be returned as a JSON string.
|
||||
pub fn account_data(&self, event_type: String) -> Result<Option<String>, ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let event = self.inner.account().account_data_raw(event_type.into()).await?;
|
||||
Ok(event.map(|e| e.json().get().to_owned()))
|
||||
})
|
||||
pub async fn account_data(&self, event_type: String) -> Result<Option<String>, ClientError> {
|
||||
let event = self.inner.account().account_data_raw(event_type.into()).await?;
|
||||
Ok(event.map(|e| e.json().get().to_owned()))
|
||||
}
|
||||
|
||||
/// Set the given account data content for the given event type.
|
||||
///
|
||||
/// It should be supplied as a JSON string.
|
||||
pub fn set_account_data(&self, event_type: String, content: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let raw_content = Raw::from_json_string(content)?;
|
||||
self.inner.account().set_account_data_raw(event_type.into(), raw_content).await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn set_account_data(
|
||||
&self,
|
||||
event_type: String,
|
||||
content: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let raw_content = Raw::from_json_string(content)?;
|
||||
self.inner.account().set_account_data_raw(event_type.into(), raw_content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upload_media(
|
||||
@@ -547,37 +558,35 @@ impl Client {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn get_session_verification_controller(
|
||||
pub async fn get_session_verification_controller(
|
||||
&self,
|
||||
) -> Result<Arc<SessionVerificationController>, ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
if let Some(session_verification_controller) =
|
||||
&*self.session_verification_controller.read().await
|
||||
{
|
||||
return Ok(Arc::new(session_verification_controller.clone()));
|
||||
}
|
||||
let user_id = self.inner.user_id().context("Failed retrieving current user_id")?;
|
||||
let user_identity = self
|
||||
.inner
|
||||
.encryption()
|
||||
.get_user_identity(user_id)
|
||||
.await?
|
||||
.context("Failed retrieving user identity")?;
|
||||
if let Some(session_verification_controller) =
|
||||
&*self.session_verification_controller.read().await
|
||||
{
|
||||
return Ok(Arc::new(session_verification_controller.clone()));
|
||||
}
|
||||
let user_id = self.inner.user_id().context("Failed retrieving current user_id")?;
|
||||
let user_identity = self
|
||||
.inner
|
||||
.encryption()
|
||||
.get_user_identity(user_id)
|
||||
.await?
|
||||
.context("Failed retrieving user identity")?;
|
||||
|
||||
let session_verification_controller =
|
||||
SessionVerificationController::new(self.inner.encryption(), user_identity);
|
||||
let session_verification_controller =
|
||||
SessionVerificationController::new(self.inner.encryption(), user_identity);
|
||||
|
||||
*self.session_verification_controller.write().await =
|
||||
Some(session_verification_controller.clone());
|
||||
*self.session_verification_controller.write().await =
|
||||
Some(session_verification_controller.clone());
|
||||
|
||||
Ok(Arc::new(session_verification_controller))
|
||||
})
|
||||
Ok(Arc::new(session_verification_controller))
|
||||
}
|
||||
|
||||
/// Log out the current user. This method returns an optional URL that
|
||||
/// should be presented to the user to complete logout (in the case of
|
||||
/// Session having been authenticated using OIDC).
|
||||
pub fn logout(&self) -> Result<Option<String>, ClientError> {
|
||||
pub async fn logout(&self) -> Result<Option<String>, ClientError> {
|
||||
let Some(auth_api) = self.inner.auth_api() else {
|
||||
return Err(anyhow!("Missing authentication API").into());
|
||||
};
|
||||
@@ -585,12 +594,13 @@ impl Client {
|
||||
match auth_api {
|
||||
AuthApi::Matrix(a) => {
|
||||
tracing::info!("Logging out via the homeserver.");
|
||||
RUNTIME.block_on(a.logout())?;
|
||||
a.logout().await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
AuthApi::Oidc(api) => {
|
||||
tracing::info!("Logging out via OIDC.");
|
||||
let end_session_builder = RUNTIME.block_on(api.logout())?;
|
||||
let end_session_builder = api.logout().await?;
|
||||
|
||||
if let Some(builder) = end_session_builder {
|
||||
let url = builder.build()?.url;
|
||||
@@ -604,7 +614,7 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Registers a pusher with given parameters
|
||||
pub fn set_pusher(
|
||||
pub async fn set_pusher(
|
||||
&self,
|
||||
identifiers: PusherIdentifiers,
|
||||
kind: PusherKind,
|
||||
@@ -613,20 +623,24 @@ impl Client {
|
||||
profile_tag: Option<String>,
|
||||
lang: String,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let ids = identifiers.into();
|
||||
let ids = identifiers.into();
|
||||
|
||||
let pusher_init = PusherInit {
|
||||
ids,
|
||||
kind: kind.try_into()?,
|
||||
app_display_name,
|
||||
device_display_name,
|
||||
profile_tag,
|
||||
lang,
|
||||
};
|
||||
self.inner.set_pusher(pusher_init.into()).await?;
|
||||
Ok(())
|
||||
})
|
||||
let pusher_init = PusherInit {
|
||||
ids,
|
||||
kind: kind.try_into()?,
|
||||
app_display_name,
|
||||
device_display_name,
|
||||
profile_tag,
|
||||
lang,
|
||||
};
|
||||
self.inner.pusher().set(pusher_init.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a pusher of given pusher ids
|
||||
pub async fn delete_pusher(&self, identifiers: PusherIdentifiers) -> Result<(), ClientError> {
|
||||
self.inner.pusher().delete(identifiers.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The homeserver this client is configured to use.
|
||||
@@ -645,53 +659,36 @@ impl Client {
|
||||
Ok(dm)
|
||||
}
|
||||
|
||||
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().unignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search_users(
|
||||
pub async fn search_users(
|
||||
&self,
|
||||
search_term: String,
|
||||
limit: u64,
|
||||
) -> Result<SearchUsersResults, ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let response = self.inner.search_users(&search_term, limit).await?;
|
||||
Ok(SearchUsersResults::from(response))
|
||||
let response = self.inner.search_users(&search_term, limit).await?;
|
||||
Ok(SearchUsersResults::from(response))
|
||||
}
|
||||
|
||||
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()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let owned_user_id = UserId::parse(user_id.clone())?;
|
||||
let response = self.inner.get_profile(&owned_user_id).await?;
|
||||
|
||||
let user_profile = UserProfile {
|
||||
user_id,
|
||||
display_name: response.displayname.clone(),
|
||||
avatar_url: response.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
};
|
||||
|
||||
Ok(user_profile)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn notification_client(
|
||||
pub async fn notification_client(
|
||||
self: Arc<Self>,
|
||||
process_setup: NotificationProcessSetup,
|
||||
) -> Result<Arc<NotificationClientBuilder>, ClientError> {
|
||||
NotificationClientBuilder::new(self.clone(), process_setup.into())
|
||||
) -> Result<Arc<NotificationClient>, ClientError> {
|
||||
Ok(Arc::new(NotificationClient {
|
||||
inner: MatrixNotificationClient::new((*self.inner).clone(), process_setup.into())
|
||||
.await?,
|
||||
_client: self.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn sync_service(&self) -> Arc<SyncServiceBuilder> {
|
||||
@@ -699,17 +696,164 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
RUNTIME.block_on(async move {
|
||||
Arc::new(NotificationSettings::new(
|
||||
(*self.inner).clone(),
|
||||
self.inner.notification_settings().await,
|
||||
))
|
||||
})
|
||||
let inner = RUNTIME.block_on(self.inner.notification_settings());
|
||||
|
||||
Arc::new(NotificationSettings::new((*self.inner).clone(), inner))
|
||||
}
|
||||
|
||||
pub fn encryption(&self) -> Arc<Encryption> {
|
||||
Arc::new(self.inner.encryption().into())
|
||||
pub fn encryption(self: Arc<Self>) -> Arc<Encryption> {
|
||||
Arc::new(Encryption { inner: self.inner.encryption(), _client: self.clone() })
|
||||
}
|
||||
|
||||
// Ignored users
|
||||
|
||||
pub async fn ignored_users(&self) -> Result<Vec<String>, ClientError> {
|
||||
if let Some(raw_content) = self
|
||||
.inner
|
||||
.account()
|
||||
.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList)
|
||||
.await?
|
||||
{
|
||||
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
|
||||
let user_ids: Vec<String> =
|
||||
content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
return Ok(user_ids);
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().unignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn subscribe_to_ignored_users(
|
||||
&self,
|
||||
listener: Box<dyn IgnoredUsersListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_to_ignore_user_list_changes();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(user_ids) = subscriber.next().await {
|
||||
listener.call(user_ids);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn room_directory_search(&self) -> Arc<RoomDirectorySearch> {
|
||||
Arc::new(RoomDirectorySearch::new(
|
||||
matrix_sdk::room_directory_search::RoomDirectorySearch::new((*self.inner).clone()),
|
||||
))
|
||||
}
|
||||
|
||||
/// Join a room by its ID.
|
||||
///
|
||||
/// Use this method when the homeserver already knows of the given room ID.
|
||||
/// Otherwise use `join_room_by_id_or_alias` so you can pass a list of
|
||||
/// server names for the homeserver to find the room.
|
||||
pub async fn join_room_by_id(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let room = self.inner.join_room_by_id(room_id.as_ref()).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
}
|
||||
|
||||
/// Join a room by its ID or alias.
|
||||
///
|
||||
/// When supplying the room's ID, you can also supply a list of server names
|
||||
/// for the homeserver to find the room. Typically these server names
|
||||
/// come from a permalink's `via` parameters, or from resolving a room's
|
||||
/// alias into an ID.
|
||||
pub async fn join_room_by_id_or_alias(
|
||||
&self,
|
||||
room_id_or_alias: String,
|
||||
server_names: Vec<String>,
|
||||
) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomOrAliasId::parse(&room_id_or_alias)?;
|
||||
let server_names = server_names
|
||||
.iter()
|
||||
.map(|name| OwnedServerName::try_from(name.as_str()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let room =
|
||||
self.inner.join_room_by_id_or_alias(room_id.as_ref(), server_names.as_ref()).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
}
|
||||
|
||||
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
|
||||
Ok(self.inner.account().get_recently_visited_rooms().await?)
|
||||
}
|
||||
|
||||
pub async fn track_recently_visited_room(&self, room: String) -> Result<(), ClientError> {
|
||||
self.inner.account().track_recently_visited_room(room).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolves the given room alias to a room ID (and a list of servers), if
|
||||
/// possible.
|
||||
pub async fn resolve_room_alias(
|
||||
&self,
|
||||
room_alias: String,
|
||||
) -> Result<ResolvedRoomAlias, ClientError> {
|
||||
let room_alias = RoomAliasId::parse(&room_alias)?;
|
||||
let response = self.inner.resolve_room_alias(&room_alias).await?;
|
||||
Ok(response.into())
|
||||
}
|
||||
|
||||
/// Given a room id, get the preview of a room, to interact with it.
|
||||
///
|
||||
/// The list of `via_servers` must be a list of servers that know
|
||||
/// about the room and can resolve it, and that may appear as a `via`
|
||||
/// parameter in e.g. a permalink URL. This list can be empty.
|
||||
pub async fn get_room_preview_from_room_id(
|
||||
&self,
|
||||
room_id: String,
|
||||
via_servers: Vec<String>,
|
||||
) -> Result<RoomPreview, ClientError> {
|
||||
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
|
||||
|
||||
let via_servers = via_servers
|
||||
.into_iter()
|
||||
.map(ServerName::parse)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("at least one `via` server name is invalid")?;
|
||||
|
||||
// The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
|
||||
// rustc win that one fight.
|
||||
let room_id: &RoomId = &room_id;
|
||||
|
||||
let sdk_room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
|
||||
|
||||
Ok(RoomPreview::from_sdk(sdk_room_preview))
|
||||
}
|
||||
|
||||
/// Given a room alias, get the preview of a room, to interact with it.
|
||||
pub async fn get_room_preview_from_room_alias(
|
||||
&self,
|
||||
room_alias: String,
|
||||
) -> Result<RoomPreview, ClientError> {
|
||||
let room_alias =
|
||||
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
|
||||
|
||||
// The `into()` call below doesn't work if I do `(&room_id).into()`, so I let
|
||||
// rustc win that one fight.
|
||||
let room_alias: &RoomAliasId = &room_alias;
|
||||
|
||||
let sdk_room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
|
||||
|
||||
Ok(RoomPreview::from_sdk(sdk_room_preview))
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: Sync + Send {
|
||||
fn call(&self, ignored_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -733,6 +877,24 @@ impl From<NotificationProcessSetup> for MatrixNotificationProcessSetup {
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a room, that was resolved from a room alias.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ResolvedRoomAlias {
|
||||
/// The room ID that the alias resolved to.
|
||||
pub room_id: String,
|
||||
/// A list of servers that can be used to find the room by its room ID.
|
||||
pub servers: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<get_alias::v3::Response> for ResolvedRoomAlias {
|
||||
fn from(value: get_alias::v3::Response) -> Self {
|
||||
Self {
|
||||
room_id: value.room_id.to_string(),
|
||||
servers: value.servers.iter().map(ToString::to_string).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SearchUsersResults {
|
||||
pub results: Vec<UserProfile>,
|
||||
@@ -766,6 +928,7 @@ impl From<&search_users::v3::User> for UserProfile {
|
||||
impl Client {
|
||||
fn process_session_change(&self, session_change: SessionChange) {
|
||||
if let Some(delegate) = self.delegate.read().unwrap().clone() {
|
||||
debug!("Applying session change: {session_change:?}");
|
||||
RUNTIME.spawn_blocking(move || match session_change {
|
||||
SessionChange::UnknownToken { soft_logout } => {
|
||||
delegate.did_receive_auth_error(soft_logout);
|
||||
@@ -774,6 +937,10 @@ impl Client {
|
||||
delegate.did_refresh_tokens();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
"No client delegate found, session change couldn't be applied: {session_change:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,7 +957,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
async fn session_inner(client: matrix_sdk::Client) -> Result<Session, ClientError> {
|
||||
fn session_inner(client: matrix_sdk::Client) -> Result<Session, ClientError> {
|
||||
let auth_api = client.auth_api().context("Missing authentication API")?;
|
||||
|
||||
let homeserver_url = client.homeserver().into();
|
||||
@@ -799,11 +966,11 @@ impl Client {
|
||||
Session::new(auth_api, homeserver_url, sliding_sync_proxy)
|
||||
}
|
||||
|
||||
async fn save_session(
|
||||
fn save_session(
|
||||
session_delegate: Arc<dyn ClientSessionDelegate>,
|
||||
client: matrix_sdk::Client,
|
||||
) -> anyhow::Result<()> {
|
||||
let session = Self::session_inner(client).await?;
|
||||
let session = Self::session_inner(client)?;
|
||||
session_delegate.save_session_in_keychain(session);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1062,7 +1229,7 @@ impl Session {
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
},
|
||||
issuer_info,
|
||||
issuer,
|
||||
} = api.user_session().context("Missing session")?;
|
||||
let client_id = api
|
||||
.client_credentials()
|
||||
@@ -1075,7 +1242,7 @@ impl Session {
|
||||
client_id,
|
||||
client_metadata,
|
||||
latest_id_token: latest_id_token.map(|t| t.to_string()),
|
||||
issuer_info,
|
||||
issuer,
|
||||
};
|
||||
|
||||
let oidc_data = serde_json::to_string(&oidc_data).ok();
|
||||
@@ -1128,7 +1295,7 @@ impl TryFrom<Session> for AuthSession {
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
},
|
||||
issuer_info: oidc_data.issuer_info,
|
||||
issuer: oidc_data.issuer,
|
||||
};
|
||||
|
||||
let session = OidcSession {
|
||||
@@ -1163,17 +1330,18 @@ pub(crate) struct OidcSessionData {
|
||||
client_id: String,
|
||||
client_metadata: VerifiedClientMetadata,
|
||||
latest_id_token: Option<String>,
|
||||
issuer_info: AuthenticationServerInfo,
|
||||
issuer: String,
|
||||
}
|
||||
|
||||
/// Represents an unverified client registration against an OpenID Connect
|
||||
/// authentication issuer. Call `validate` on this to use it for restoration.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(try_from = "OidcUnvalidatedSessionDataDeHelper")]
|
||||
pub(crate) struct OidcUnvalidatedSessionData {
|
||||
client_id: String,
|
||||
client_metadata: ClientMetadata,
|
||||
latest_id_token: Option<String>,
|
||||
issuer_info: AuthenticationServerInfo,
|
||||
issuer: String,
|
||||
}
|
||||
|
||||
impl OidcUnvalidatedSessionData {
|
||||
@@ -1183,30 +1351,59 @@ impl OidcUnvalidatedSessionData {
|
||||
client_id: self.client_id,
|
||||
client_metadata: self.client_metadata.validate()?,
|
||||
latest_id_token: self.latest_id_token,
|
||||
issuer_info: self.issuer_info,
|
||||
issuer: self.issuer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OidcUnvalidatedSessionDataDeHelper {
|
||||
client_id: String,
|
||||
client_metadata: ClientMetadata,
|
||||
latest_id_token: Option<String>,
|
||||
issuer_info: Option<AuthenticationServerInfo>,
|
||||
issuer: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<OidcUnvalidatedSessionDataDeHelper> for OidcUnvalidatedSessionData {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: OidcUnvalidatedSessionDataDeHelper) -> Result<Self, Self::Error> {
|
||||
let OidcUnvalidatedSessionDataDeHelper {
|
||||
client_id,
|
||||
client_metadata,
|
||||
latest_id_token,
|
||||
issuer_info,
|
||||
issuer,
|
||||
} = value;
|
||||
|
||||
let issuer = issuer
|
||||
.or(issuer_info.map(|info| info.issuer))
|
||||
.ok_or_else(|| "missing field `issuer`".to_owned())?;
|
||||
|
||||
Ok(Self { client_id, client_metadata, latest_id_token, issuer })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AccountManagementAction {
|
||||
Profile,
|
||||
SessionsList,
|
||||
SessionView { device_id: String },
|
||||
SessionEnd { device_id: String },
|
||||
AccountDeactivate,
|
||||
CrossSigningReset,
|
||||
}
|
||||
|
||||
impl From<AccountManagementAction> for OidcAccountManagementAction {
|
||||
impl From<AccountManagementAction> for AccountManagementActionFull {
|
||||
fn from(value: AccountManagementAction) -> Self {
|
||||
match value {
|
||||
AccountManagementAction::Profile => Self::Profile,
|
||||
AccountManagementAction::SessionsList => Self::SessionsList,
|
||||
AccountManagementAction::SessionView { device_id } => {
|
||||
Self::SessionView { device_id: device_id.into() }
|
||||
}
|
||||
AccountManagementAction::SessionEnd { device_id } => {
|
||||
Self::SessionEnd { device_id: device_id.into() }
|
||||
}
|
||||
AccountManagementAction::SessionView { device_id } => Self::SessionView { device_id },
|
||||
AccountManagementAction::SessionEnd { device_id } => Self::SessionEnd { device_id },
|
||||
AccountManagementAction::AccountDeactivate => Self::AccountDeactivate,
|
||||
AccountManagementAction::CrossSigningReset => Self::CrossSigningReset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,222 @@
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
reqwest::Certificate,
|
||||
ruma::{
|
||||
api::{error::UnknownVersionError, MatrixVersion},
|
||||
ServerName, UserId,
|
||||
},
|
||||
Client as MatrixClient, ClientBuilder as MatrixClientBuilder,
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError,
|
||||
ClientBuilder as MatrixClientBuilder, IdParseError,
|
||||
};
|
||||
use sanitize_filename_reader_friendly::sanitize;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{client::Client, RUNTIME};
|
||||
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
|
||||
use crate::{
|
||||
authentication_service::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
|
||||
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum UrlScheme {
|
||||
Http,
|
||||
Https,
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
pub type CertificateBytes = Vec<u8>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum HomeserverConfig {
|
||||
Url(String),
|
||||
ServerName(String),
|
||||
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,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
/// The slice of bytes would generally be returned by a QR code decoder.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Arc<Self>, QrCodeDecodeError> {
|
||||
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the decoding of the [`QrCodeData`].
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum HumanQrLoginError {
|
||||
#[error("Linking with this device is not supported.")]
|
||||
LinkingNotSupported,
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
#[error("The sign in was declined.")]
|
||||
Declined,
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown,
|
||||
#[error("The homeserver doesn't provide a sliding sync proxy 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::Oidc(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 OIDC provider to give us an
|
||||
/// access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: Sync + Send {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel { check_code } => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum ClientBuildError {
|
||||
#[error(transparent)]
|
||||
Sdk(#[from] MatrixClientBuildError),
|
||||
#[error("Failed to build the client: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
impl From<IdParseError> for ClientBuildError {
|
||||
fn from(e: IdParseError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ClientBuildError {
|
||||
fn from(e: std::io::Error) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for ClientBuildError {
|
||||
fn from(e: url::ParseError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientError> for ClientBuildError {
|
||||
fn from(e: ClientError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ClientBuilder {
|
||||
base_path: Option<String>,
|
||||
session_path: Option<String>,
|
||||
username: Option<String>,
|
||||
server_name: Option<(String, UrlScheme)>,
|
||||
homeserver_url: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
server_versions: Option<Vec<String>>,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
user_agent: Option<String>,
|
||||
@@ -37,13 +227,36 @@ pub struct ClientBuilder {
|
||||
inner: MatrixClientBuilder,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
encryption_settings: EncryptionSettings,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self::default())
|
||||
Arc::new(Self {
|
||||
session_path: None,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
server_versions: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
sliding_sync_proxy: None,
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
inner: MatrixClient::builder(),
|
||||
cross_process_refresh_lock_id: None,
|
||||
session_delegate: None,
|
||||
additional_root_certificates: Default::default(),
|
||||
encryption_settings: EncryptionSettings {
|
||||
auto_enable_cross_signing: false,
|
||||
backup_download_strategy:
|
||||
matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
auto_enable_backups: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable_cross_process_refresh_lock(
|
||||
@@ -63,9 +276,14 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn base_path(self: Arc<Self>, path: String) -> Arc<Self> {
|
||||
/// Sets the path that the client will use to store its data once logged in.
|
||||
/// This path **must** be unique per session as the data stores aren't
|
||||
/// capable of handling multiple users.
|
||||
///
|
||||
/// Leaving this unset tells the client to use an in-memory data store.
|
||||
pub fn session_path(self: Arc<Self>, path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.base_path = Some(path);
|
||||
builder.session_path = Some(path);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -83,14 +301,19 @@ impl ClientBuilder {
|
||||
|
||||
pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
// Assume HTTPS if no protocol is provided.
|
||||
builder.server_name = Some((server_name, UrlScheme::Https));
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn homeserver_url(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_url = Some(url);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::Url(url));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn server_name_or_homeserver_url(self: Arc<Self>, server_name_or_url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -130,8 +353,102 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientError> {
|
||||
Ok(self.build_inner()?)
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn auto_enable_cross_signing(
|
||||
self: Arc<Self>,
|
||||
auto_enable_cross_signing: bool,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.encryption_settings.auto_enable_cross_signing = auto_enable_cross_signing;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Select a strategy to download room keys from the backup. By default
|
||||
/// we download after a decryption failure.
|
||||
///
|
||||
/// Take a look at the [`BackupDownloadStrategy`] enum for more options.
|
||||
pub fn backup_download_strategy(
|
||||
self: Arc<Self>,
|
||||
backup_download_strategy: BackupDownloadStrategy,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.encryption_settings.backup_download_strategy = backup_download_strategy;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Automatically create a backup version if no backup exists.
|
||||
pub fn auto_enable_backups(self: Arc<Self>, auto_enable_backups: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.encryption_settings.auto_enable_backups = auto_enable_backups;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
Ok(Arc::new(self.build_inner().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 OIDC
|
||||
/// 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(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<Arc<Client>, HumanQrLoginError> {
|
||||
if let QrCodeModeData::Reciprocate { server_name } = &qr_code_data.inner.mode_data {
|
||||
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
|
||||
|
||||
let client = builder.build().await.map_err(|e| {
|
||||
error!("Couldn't build the client {e:?}");
|
||||
HumanQrLoginError::Unknown
|
||||
})?;
|
||||
|
||||
if client.sliding_sync_proxy().is_none() {
|
||||
return Err(HumanQrLoginError::SlidingSyncNotAvailable);
|
||||
}
|
||||
|
||||
let client_metadata = oidc_configuration
|
||||
.try_into()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let oidc = client.inner.oidc();
|
||||
let login = oidc.login_with_qr_code(&qr_code_data.inner, client_metadata);
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(client)
|
||||
} else {
|
||||
Err(HumanQrLoginError::OtherDeviceNotSignedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,46 +473,64 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub(crate) fn server_name_with_protocol(
|
||||
self: Arc<Self>,
|
||||
server_name: String,
|
||||
protocol: UrlScheme,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.server_name = Some((server_name, protocol));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub(crate) fn build_inner(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
|
||||
pub(crate) async fn build_inner(self: Arc<Self>) -> Result<Client, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = builder.inner;
|
||||
|
||||
if let (Some(base_path), Some(username)) = (builder.base_path, &builder.username) {
|
||||
// Determine store path
|
||||
let data_path = PathBuf::from(base_path).join(sanitize(username));
|
||||
fs::create_dir_all(&data_path)?;
|
||||
if let Some(session_path) = &builder.session_path {
|
||||
let data_path = PathBuf::from(session_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
"Creating directory and using it as the store path."
|
||||
);
|
||||
|
||||
fs::create_dir_all(&data_path)?;
|
||||
inner_builder = inner_builder.sqlite_store(&data_path, builder.passphrase.as_deref());
|
||||
} else {
|
||||
debug!("Not using a store path.");
|
||||
}
|
||||
|
||||
// Determine server either from URL, server name or user ID.
|
||||
if let Some(homeserver_url) = builder.homeserver_url {
|
||||
inner_builder = inner_builder.homeserver_url(homeserver_url);
|
||||
} else if let Some((server_name, protocol)) = builder.server_name {
|
||||
let server_name = ServerName::parse(server_name)?;
|
||||
inner_builder = match protocol {
|
||||
UrlScheme::Http => inner_builder.insecure_server_name_no_tls(&server_name),
|
||||
UrlScheme::Https => inner_builder.server_name(&server_name),
|
||||
};
|
||||
} else if let Some(username) = builder.username {
|
||||
let user = UserId::parse(username)?;
|
||||
inner_builder = inner_builder.server_name(user.server_name());
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Failed to build: One of homeserver_url, server_name or username must be called."
|
||||
);
|
||||
inner_builder = match builder.homeserver_cfg {
|
||||
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
|
||||
Some(HomeserverConfig::ServerName(server_name)) => {
|
||||
let server_name = ServerName::parse(server_name)?;
|
||||
inner_builder.server_name(&server_name)
|
||||
}
|
||||
Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url)) => {
|
||||
inner_builder.server_name_or_homeserver_url(server_name_or_url)
|
||||
}
|
||||
None => {
|
||||
if let Some(username) = builder.username {
|
||||
let user = UserId::parse(username)?;
|
||||
inner_builder.server_name(user.server_name())
|
||||
} else {
|
||||
return Err(ClientBuildError::Generic {
|
||||
message: "Failed to build: One of homeserver_url, server_name, server_name_or_homeserver_url or username must be called.".to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
if let Ok(cert) = Certificate::from_der(&certificate) {
|
||||
certificates.push(cert);
|
||||
} else {
|
||||
let cert =
|
||||
Certificate::from_pem(&certificate).map_err(|e| ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate {e:?}"),
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
}
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
@@ -217,11 +552,14 @@ impl ClientBuilder {
|
||||
server_versions
|
||||
.iter()
|
||||
.map(|s| MatrixVersion::try_from(s.as_str()))
|
||||
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()?,
|
||||
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()
|
||||
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
|
||||
);
|
||||
}
|
||||
|
||||
let sdk_client = RUNTIME.block_on(async move { inner_builder.build().await })?;
|
||||
inner_builder = inner_builder.with_encryption_settings(builder.encryption_settings);
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
// At this point, `sdk_client` might contain a `sliding_sync_proxy` that has
|
||||
// been configured by the homeserver (if it's a `ServerName` and the
|
||||
@@ -240,38 +578,7 @@ impl ClientBuilder {
|
||||
sdk_client.set_sliding_sync_proxy(Some(Url::parse(&sliding_sync_proxy)?));
|
||||
}
|
||||
|
||||
Ok(Client::new(
|
||||
sdk_client,
|
||||
builder.cross_process_refresh_lock_id,
|
||||
builder.session_delegate,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ClientBuilder {
|
||||
fn default() -> Self {
|
||||
let encryption_settings = EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
auto_enable_backups: true,
|
||||
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
};
|
||||
let inner = MatrixClient::builder().with_encryption_settings(encryption_settings);
|
||||
|
||||
Self {
|
||||
base_path: None,
|
||||
username: None,
|
||||
server_name: None,
|
||||
homeserver_url: None,
|
||||
server_versions: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
sliding_sync_proxy: None,
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
inner,
|
||||
cross_process_refresh_lock_id: None,
|
||||
session_delegate: None,
|
||||
}
|
||||
Ok(Client::new(sdk_client, builder.cross_process_refresh_lock_id, builder.session_delegate)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::encryption::{backups, recovery};
|
||||
use matrix_sdk::{
|
||||
encryption,
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
use crate::{client::Client, error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
inner: matrix_sdk::encryption::Encryption,
|
||||
}
|
||||
pub(crate) inner: matrix_sdk::encryption::Encryption,
|
||||
|
||||
impl From<matrix_sdk::encryption::Encryption> for Encryption {
|
||||
fn from(value: matrix_sdk::encryption::Encryption) -> Self {
|
||||
Self { inner: value }
|
||||
}
|
||||
/// A reference to the FFI client.
|
||||
///
|
||||
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
|
||||
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
|
||||
/// would need to repeat the hack done in the FFI `Client::drop` method.
|
||||
pub(crate) _client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
@@ -34,6 +38,11 @@ pub trait RecoveryStateListener: Sync + Send {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait VerificationStateListener: Sync + Send {
|
||||
fn on_update(&self, status: VerificationState);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BackupUploadState {
|
||||
Waiting,
|
||||
@@ -186,8 +195,37 @@ impl From<recovery::EnableProgress> for EnableRecoveryProgress {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VerificationState {
|
||||
Unknown,
|
||||
Verified,
|
||||
Unverified,
|
||||
}
|
||||
|
||||
impl From<encryption::VerificationState> for VerificationState {
|
||||
fn from(value: encryption::VerificationState) -> Self {
|
||||
match &value {
|
||||
encryption::VerificationState::Unknown => Self::Unknown,
|
||||
encryption::VerificationState::Verified => Self::Verified,
|
||||
encryption::VerificationState::Unverified => Self::Unverified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Encryption {
|
||||
/// Get the public ed25519 key of our own device. This is usually what is
|
||||
/// called the fingerprint of the device.
|
||||
pub async fn ed25519_key(&self) -> Option<String> {
|
||||
self.inner.ed25519_key().await
|
||||
}
|
||||
|
||||
/// Get the public curve25519 key of our own device in base64. This is
|
||||
/// usually what is called the identity key of the device.
|
||||
pub async fn curve25519_key(&self) -> Option<String> {
|
||||
self.inner.curve25519_key().await.map(|k| k.to_base64())
|
||||
}
|
||||
|
||||
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.backups().state_stream();
|
||||
|
||||
@@ -326,4 +364,26 @@ impl Encryption {
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub fn verification_state(&self) -> VerificationState {
|
||||
self.inner.verification_state().get().into()
|
||||
}
|
||||
|
||||
pub fn verification_state_listener(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn VerificationStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.verification_state();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(verification_state) = subscriber.next().await {
|
||||
listener.on_update(verification_state.into());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Waits for end-to-end encryption initialization tasks to finish, if any
|
||||
/// was running in the background.
|
||||
pub async fn wait_for_e2ee_initialization_tasks(&self) {
|
||||
self.inner.wait_for_e2ee_initialization_tasks().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use matrix_sdk::{
|
||||
self, encryption::CryptoStoreError, oidc::OidcError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, HttpError,
|
||||
IdParseError, NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
@@ -14,7 +14,7 @@ pub enum ClientError {
|
||||
}
|
||||
|
||||
impl ClientError {
|
||||
fn new<E: Display>(error: E) -> Self {
|
||||
pub(crate) fn new<E: Display>(error: E) -> Self {
|
||||
Self::Generic { msg: error.to_string() }
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,12 @@ impl From<RoomError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheError> for ClientError {
|
||||
fn from(e: EventCacheError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum RoomError {
|
||||
|
||||
@@ -5,7 +5,11 @@ use ruma::events::{
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
};
|
||||
|
||||
use crate::{room_member::MembershipState, ruma::MessageType, ClientError};
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, NotifyType},
|
||||
ClientError,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
|
||||
@@ -117,6 +121,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
pub enum MessageLikeEventContent {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
CallNotify { notify_type: NotifyType },
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
@@ -141,6 +146,12 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
let content = match value {
|
||||
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
|
||||
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
|
||||
AnySyncMessageLikeEvent::CallNotify(content) => {
|
||||
let original_content = get_message_like_event_original_content(content)?;
|
||||
MessageLikeEventContent::CallNotify {
|
||||
notify_type: original_content.notify_type.into(),
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
|
||||
AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates,
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_) => {
|
||||
@@ -216,3 +227,115 @@ where
|
||||
event.as_original().context("Failed to get original content")?.content.clone();
|
||||
Ok(original_content)
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum StateEventType {
|
||||
CallMember,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
}
|
||||
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAliases => Self::RoomAliases,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
StateEventType::RoomEncryption => Self::RoomEncryption,
|
||||
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules => Self::RoomJoinRules,
|
||||
StateEventType::RoomMemberEvent => Self::RoomMember,
|
||||
StateEventType::RoomName => Self::RoomName,
|
||||
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
|
||||
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
|
||||
StateEventType::RoomServerAcl => Self::RoomServerAcl,
|
||||
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
|
||||
StateEventType::RoomTombstone => Self::RoomTombstone,
|
||||
StateEventType::RoomTopic => Self::RoomTopic,
|
||||
StateEventType::SpaceChild => Self::SpaceChild,
|
||||
StateEventType::SpaceParent => Self::SpaceParent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MessageLikeEventType {
|
||||
CallAnswer,
|
||||
CallCandidates,
|
||||
CallHangup,
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationDone,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
PollEnd,
|
||||
PollResponse,
|
||||
PollStart,
|
||||
Reaction,
|
||||
RoomEncrypted,
|
||||
RoomMessage,
|
||||
RoomRedaction,
|
||||
Sticker,
|
||||
UnstablePollEnd,
|
||||
UnstablePollResponse,
|
||||
UnstablePollStart,
|
||||
}
|
||||
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
fn from(val: MessageLikeEventType) -> Self {
|
||||
match val {
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallNotify => Self::CallNotify,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
|
||||
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
|
||||
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
|
||||
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
|
||||
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
|
||||
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
|
||||
MessageLikeEventType::Reaction => Self::Reaction,
|
||||
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
|
||||
MessageLikeEventType::RoomMessage => Self::RoomMessage,
|
||||
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
|
||||
MessageLikeEventType::Sticker => Self::Sticker,
|
||||
MessageLikeEventType::PollEnd => Self::PollEnd,
|
||||
MessageLikeEventType::PollResponse => Self::PollResponse,
|
||||
MessageLikeEventType::PollStart => Self::PollStart,
|
||||
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
|
||||
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
|
||||
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,17 @@ mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod room;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
mod room_member;
|
||||
mod room_preview;
|
||||
mod ruma;
|
||||
mod session_verification;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod timeline_event_filter;
|
||||
mod tracing;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationClientBuilder as MatrixNotificationClientBuilder,
|
||||
NotificationItem as MatrixNotificationItem, NotificationProcessSetup,
|
||||
NotificationClient as MatrixNotificationClient, NotificationItem as MatrixNotificationItem,
|
||||
};
|
||||
use ruma::{EventId, RoomId};
|
||||
|
||||
use crate::{
|
||||
client::Client, error::ClientError, event::TimelineEvent, helpers::unwrap_or_clone_arc, RUNTIME,
|
||||
};
|
||||
use crate::{client::Client, error::ClientError, event::TimelineEvent};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum NotificationEvent {
|
||||
@@ -21,6 +17,7 @@ pub enum NotificationEvent {
|
||||
pub struct NotificationSenderInfo {
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub is_name_ambiguous: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -63,9 +60,10 @@ impl NotificationItem {
|
||||
sender_info: NotificationSenderInfo {
|
||||
display_name: item.sender_display_name,
|
||||
avatar_url: item.sender_avatar_url,
|
||||
is_name_ambiguous: item.is_sender_name_ambiguous,
|
||||
},
|
||||
room_info: NotificationRoomInfo {
|
||||
display_name: item.room_display_name,
|
||||
display_name: item.room_computed_display_name,
|
||||
avatar_url: item.room_avatar_url,
|
||||
canonical_alias: item.room_canonical_alias,
|
||||
joined_members_count: item.joined_members_count,
|
||||
@@ -78,74 +76,37 @@ impl NotificationItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct NotificationClientBuilder {
|
||||
client: Arc<Client>,
|
||||
builder: MatrixNotificationClientBuilder,
|
||||
}
|
||||
|
||||
impl NotificationClientBuilder {
|
||||
pub(crate) fn new(
|
||||
client: Arc<Client>,
|
||||
process_setup: NotificationProcessSetup,
|
||||
) -> Result<Arc<Self>, ClientError> {
|
||||
let builder = RUNTIME.block_on(async {
|
||||
MatrixNotificationClient::builder((*client.inner).clone(), process_setup).await
|
||||
})?;
|
||||
Ok(Arc::new(Self { builder, client }))
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl NotificationClientBuilder {
|
||||
/// Filter out the notification event according to the push rules present in
|
||||
/// the event.
|
||||
pub fn filter_by_push_rules(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.filter_by_push_rules();
|
||||
Arc::new(Self { builder, client: this.client })
|
||||
}
|
||||
|
||||
pub fn finish(self: Arc<Self>) -> Arc<NotificationClient> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Arc::new(NotificationClient { inner: this.builder.build(), _client: this.client })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct NotificationClient {
|
||||
inner: MatrixNotificationClient,
|
||||
pub(crate) inner: MatrixNotificationClient,
|
||||
|
||||
/// A reference to the FFI client.
|
||||
///
|
||||
/// Note: we do this to make it so that the FFI `NotificationClient` keeps
|
||||
/// the FFI `Client` and thus the SDK `Client` alive. Otherwise, we
|
||||
/// would need to repeat the hack done in the FFI `Client::drop` method.
|
||||
_client: Arc<Client>,
|
||||
pub(crate) _client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl NotificationClient {
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
pub fn get_notification(
|
||||
pub async fn get_notification(
|
||||
&self,
|
||||
room_id: String,
|
||||
event_id: String,
|
||||
) -> Result<Option<NotificationItem>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
RUNTIME.block_on(async move {
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use matrix_sdk::{
|
||||
event_handler::EventHandlerHandle,
|
||||
@@ -13,9 +13,8 @@ use ruma::{
|
||||
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
|
||||
RoomId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::error::NotificationSettingsError;
|
||||
|
||||
/// Enum representing the push notification modes for a room.
|
||||
@@ -73,7 +72,7 @@ impl RoomNotificationSettings {
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct NotificationSettings {
|
||||
sdk_client: MatrixClient,
|
||||
sdk_notification_settings: Arc<RwLock<SdkNotificationSettings>>,
|
||||
sdk_notification_settings: Arc<AsyncRwLock<SdkNotificationSettings>>,
|
||||
pushrules_event_handler: Arc<RwLock<Option<EventHandlerHandle>>>,
|
||||
}
|
||||
|
||||
@@ -82,10 +81,9 @@ impl NotificationSettings {
|
||||
sdk_client: MatrixClient,
|
||||
sdk_notification_settings: SdkNotificationSettings,
|
||||
) -> Self {
|
||||
let sdk_notification_settings = Arc::new(RwLock::new(sdk_notification_settings));
|
||||
Self {
|
||||
sdk_client,
|
||||
sdk_notification_settings,
|
||||
sdk_notification_settings: Arc::new(AsyncRwLock::new(sdk_notification_settings)),
|
||||
pushrules_event_handler: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
@@ -94,11 +92,9 @@ impl NotificationSettings {
|
||||
impl Drop for NotificationSettings {
|
||||
fn drop(&mut self) {
|
||||
// Remove the event handler on the sdk_client.
|
||||
RUNTIME.block_on(async move {
|
||||
if let Some(event_handler) = self.pushrules_event_handler.read().await.as_ref() {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
});
|
||||
if let Some(event_handler) = self.pushrules_event_handler.read().unwrap().as_ref() {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,18 +110,14 @@ impl NotificationSettings {
|
||||
delegate.settings_did_change();
|
||||
});
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
*self.pushrules_event_handler.write().await = Some(event_handler);
|
||||
});
|
||||
*self.pushrules_event_handler.write().unwrap() = Some(event_handler);
|
||||
} else {
|
||||
// Remove the event handler if there is no delegate
|
||||
RUNTIME.block_on(async move {
|
||||
let event_handler = &mut *self.pushrules_event_handler.write().await;
|
||||
if let Some(event_handler) = event_handler {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
*event_handler = None;
|
||||
});
|
||||
let event_handler = &mut *self.pushrules_event_handler.write().unwrap();
|
||||
if let Some(event_handler) = event_handler {
|
||||
self.sdk_client.remove_event_handler(event_handler.clone());
|
||||
}
|
||||
*event_handler = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +135,11 @@ impl NotificationSettings {
|
||||
is_encrypted: bool,
|
||||
is_one_to_one: bool,
|
||||
) -> Result<RoomNotificationSettings, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let parsed_room_id = RoomId::parse(&room_id)
|
||||
.map_err(|_e| NotificationSettingsError::InvalidRoomId { room_id })?;
|
||||
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
|
||||
// Get the current user defined mode for this room
|
||||
if let Some(mode) =
|
||||
notification_settings.get_user_defined_room_notification_mode(&parsed_room_id).await
|
||||
@@ -158,6 +152,7 @@ impl NotificationSettings {
|
||||
let mode = notification_settings
|
||||
.get_default_room_notification_mode(is_encrypted.into(), is_one_to_one.into())
|
||||
.await;
|
||||
|
||||
Ok(RoomNotificationSettings::new(mode.into(), true))
|
||||
}
|
||||
|
||||
@@ -167,10 +162,15 @@ impl NotificationSettings {
|
||||
room_id: String,
|
||||
mode: RoomNotificationMode,
|
||||
) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let parsed_room_id = RoomId::parse(&room_id)
|
||||
.map_err(|_e| NotificationSettingsError::InvalidRoomId { room_id })?;
|
||||
notification_settings.set_room_notification_mode(&parsed_room_id, mode.into()).await?;
|
||||
|
||||
self.sdk_notification_settings
|
||||
.read()
|
||||
.await
|
||||
.set_room_notification_mode(&parsed_room_id, mode.into())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -297,10 +297,10 @@ impl NotificationSettings {
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Check if [MSC 4028 push rule][rule] is enabled.
|
||||
/// Returns true if [MSC 4028 push rule][rule] is supported and enabled.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
pub async fn can_push_encrypted_event_to_device(&self) -> bool {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
// Check stable identifier
|
||||
if let Ok(enabled) = notification_settings
|
||||
@@ -319,6 +319,13 @@ impl NotificationSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether [MSC 4028 push rule][rule] is enabled on the homeserver.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
self.sdk_client.can_homeserver_push_encrypted_event_to_device().await.unwrap()
|
||||
}
|
||||
|
||||
/// Set whether user mentions are enabled.
|
||||
pub async fn set_user_mention_enabled(
|
||||
&self,
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
use std::{collections::HashMap, fmt::Debug, pin::Pin};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use futures_core::future::BoxFuture;
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry_otlp::{Protocol, WithExportConfig};
|
||||
use opentelemetry_sdk::{runtime::RuntimeChannel, trace::Tracer, Resource};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, time::FormatTime, FormatEvent, FormatFields, FormattedFields},
|
||||
@@ -15,81 +8,6 @@ use tracing_subscriber::{
|
||||
EnvFilter, Layer,
|
||||
};
|
||||
|
||||
use crate::RUNTIME;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TokioRuntime {
|
||||
runtime: Handle,
|
||||
}
|
||||
|
||||
impl opentelemetry_sdk::runtime::Runtime for TokioRuntime {
|
||||
type Interval = tokio_stream::wrappers::IntervalStream;
|
||||
type Delay = Pin<Box<tokio::time::Sleep>>;
|
||||
|
||||
fn interval(&self, period: std::time::Duration) -> Self::Interval {
|
||||
let _guard = self.runtime.enter();
|
||||
tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period))
|
||||
}
|
||||
|
||||
fn spawn(&self, future: BoxFuture<'static, ()>) {
|
||||
#[allow(clippy::let_underscore_future)]
|
||||
let _ = self.runtime.spawn(future);
|
||||
}
|
||||
|
||||
fn delay(&self, duration: std::time::Duration) -> Self::Delay {
|
||||
let _guard = self.runtime.enter();
|
||||
Box::pin(tokio::time::sleep(duration))
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeChannel for TokioRuntime {
|
||||
type Receiver<T: Debug + Send> = tokio_stream::wrappers::ReceiverStream<T>;
|
||||
type Sender<T: Debug + Send> = tokio::sync::mpsc::Sender<T>;
|
||||
|
||||
fn batch_message_channel<T: Debug + Send>(
|
||||
&self,
|
||||
capacity: usize,
|
||||
) -> (Self::Sender<T>, Self::Receiver<T>) {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(capacity);
|
||||
(sender, tokio_stream::wrappers::ReceiverStream::new(receiver))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_otlp_tracer(
|
||||
user: String,
|
||||
password: String,
|
||||
otlp_endpoint: String,
|
||||
client_name: String,
|
||||
) -> anyhow::Result<Tracer> {
|
||||
let runtime = RUNTIME.handle().to_owned();
|
||||
|
||||
let auth = STANDARD.encode(format!("{user}:{password}"));
|
||||
let headers = HashMap::from([("Authorization".to_owned(), format!("Basic {auth}"))]);
|
||||
let http_client = matrix_sdk::reqwest::ClientBuilder::new().build()?;
|
||||
|
||||
let exporter = opentelemetry_otlp::new_exporter()
|
||||
.http()
|
||||
.with_http_client(http_client)
|
||||
.with_protocol(Protocol::HttpBinary)
|
||||
.with_endpoint(otlp_endpoint)
|
||||
.with_headers(headers);
|
||||
|
||||
let tracer_runtime = TokioRuntime { runtime: runtime.to_owned() };
|
||||
|
||||
let _guard = runtime.enter();
|
||||
let tracer = opentelemetry_otlp::new_pipeline()
|
||||
.tracing()
|
||||
.with_exporter(exporter)
|
||||
.with_trace_config(
|
||||
opentelemetry_sdk::trace::config()
|
||||
.with_resource(Resource::new(vec![KeyValue::new("service.name", client_name)])),
|
||||
)
|
||||
.install_batch(tracer_runtime)?;
|
||||
|
||||
Ok(tracer)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn log_panics() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
log_panics::init();
|
||||
@@ -202,13 +120,26 @@ where
|
||||
}
|
||||
|
||||
let file_layer = config.write_to_files.map(|c| {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&c.file_prefix);
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
|
||||
|
||||
fmt::layer()
|
||||
.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(tracing_appender::rolling::hourly(c.path, c.file_prefix))
|
||||
.with_writer(writer)
|
||||
});
|
||||
|
||||
Layer::and_then(
|
||||
@@ -237,6 +168,8 @@ where
|
||||
pub struct TracingFileConfiguration {
|
||||
path: String,
|
||||
file_prefix: String,
|
||||
file_suffix: Option<String>,
|
||||
max_files: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -250,7 +183,6 @@ pub struct TracingConfiguration {
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
#[cfg(target_os = "android")]
|
||||
log_panics();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
@@ -258,37 +190,3 @@ pub fn setup_tracing(config: TracingConfiguration) {
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OtlpTracingConfiguration {
|
||||
client_name: String,
|
||||
user: String,
|
||||
password: String,
|
||||
otlp_endpoint: String,
|
||||
filter: String,
|
||||
/// Controls whether to print to stdout or, equivalent, the system logs on
|
||||
/// Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn setup_otlp_tracing(config: OtlpTracingConfiguration) {
|
||||
#[cfg(target_os = "android")]
|
||||
log_panics();
|
||||
|
||||
let otlp_tracer =
|
||||
create_otlp_tracer(config.user, config.password, config.otlp_endpoint, config.client_name)
|
||||
.expect("Couldn't configure the OpenTelemetry tracer");
|
||||
let otlp_layer = tracing_opentelemetry::layer().with_tracer(otlp_tracer);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&config.filter))
|
||||
.with(text_layers(TracingConfiguration {
|
||||
filter: config.filter,
|
||||
write_to_stdout_or_system: config.write_to_stdout_or_system,
|
||||
write_to_files: config.write_to_files,
|
||||
}))
|
||||
.with(otlp_layer)
|
||||
.init();
|
||||
}
|
||||
|
||||
+466
-168
@@ -1,14 +1,25 @@
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use matrix_sdk::{room::Room as SdkRoom, RoomMemberships, RoomState};
|
||||
use matrix_sdk_ui::timeline::RoomExt;
|
||||
use matrix_sdk::{
|
||||
event_cache::paginator::PaginatorError,
|
||||
room::{power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole},
|
||||
ComposerDraft, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
|
||||
EventId, UserId,
|
||||
events::{
|
||||
call::notify,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
TimelineEventType,
|
||||
},
|
||||
EventId, Int, RoomAliasId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
@@ -17,10 +28,11 @@ use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
room_info::RoomInfo,
|
||||
room_member::{MessageLikeEventType, RoomMember, StateEventType},
|
||||
ruma::ImageInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
room_member::RoomMember,
|
||||
ruma::{ImageInfo, Mentions, NotifyType},
|
||||
timeline::{FocusEventError, ReceiptType, Timeline},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
@@ -66,7 +78,15 @@ impl Room {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
/// Returns the room's name from the state event if available, otherwise
|
||||
/// compute a room name based on the room's nature (DM or not) and number of
|
||||
/// members.
|
||||
pub fn display_name(&self) -> Option<String> {
|
||||
Some(self.inner.cached_display_name()?.to_string())
|
||||
}
|
||||
|
||||
/// The raw name as present in the room state event.
|
||||
pub fn raw_name(&self) -> Option<String> {
|
||||
self.inner.name()
|
||||
}
|
||||
|
||||
@@ -79,7 +99,7 @@ impl Room {
|
||||
}
|
||||
|
||||
pub fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(async move { self.inner.is_direct().await.unwrap_or(false) })
|
||||
RUNTIME.block_on(self.inner.is_direct()).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
@@ -106,6 +126,11 @@ impl Room {
|
||||
self.inner.state().into()
|
||||
}
|
||||
|
||||
/// Returns the room heroes for this room.
|
||||
pub fn heroes(&self) -> Vec<RoomHero> {
|
||||
self.inner.heroes().into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Is there a non expired membership with application "m.call" and scope
|
||||
/// "m.room" in this room.
|
||||
pub fn has_active_room_call(&self) -> bool {
|
||||
@@ -114,7 +139,7 @@ impl Room {
|
||||
|
||||
/// Returns a Vec of userId's that participate in the room call.
|
||||
///
|
||||
/// matrix_rtc memberships with application "m.call" and scope "m.room" are
|
||||
/// MatrixRTC memberships with application "m.call" and scope "m.room" are
|
||||
/// considered. A user can occur twice if they join with two devices.
|
||||
/// convert to a set depending if the different users are required or the
|
||||
/// amount of sessions.
|
||||
@@ -124,113 +149,120 @@ impl Room {
|
||||
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
|
||||
}
|
||||
|
||||
pub fn inviter(&self) -> Option<Arc<RoomMember>> {
|
||||
/// For rooms one is invited to, retrieves the room member information for
|
||||
/// the user who invited the logged-in user to a room.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
if self.inner.state() == RoomState::Invited {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.inviter)
|
||||
.map(|m| Arc::new(RoomMember::new(m)))
|
||||
})
|
||||
self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn timeline(&self) -> Arc<Timeline> {
|
||||
/// Forces the currently active room key, which is used to encrypt messages,
|
||||
/// to be rotated.
|
||||
///
|
||||
/// A new room key will be crated and shared with all the room members the
|
||||
/// next time a message will be sent. You don't have to call this method,
|
||||
/// room keys will be rotated automatically when necessary. This method is
|
||||
/// still useful for debugging purposes.
|
||||
pub async fn discard_room_key(&self) -> Result<(), ClientError> {
|
||||
self.inner.discard_room_key().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn timeline(&self) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
timeline.clone()
|
||||
Ok(timeline.clone())
|
||||
} else {
|
||||
let timeline = Timeline::new(self.inner.timeline().await);
|
||||
let timeline = Timeline::new(self.inner.timeline().await?);
|
||||
*write_guard = Some(timeline.clone());
|
||||
timeline
|
||||
Ok(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn poll_history(&self) -> Arc<Timeline> {
|
||||
Timeline::new(self.inner.poll_history().await)
|
||||
}
|
||||
/// Returns a timeline focused on the given event.
|
||||
///
|
||||
/// Note: this timeline is independent from that returned with
|
||||
/// [`Self::timeline`], and as such it is not cached.
|
||||
pub async fn timeline_focused_on_event(
|
||||
&self,
|
||||
event_id: String,
|
||||
num_context_events: u16,
|
||||
internal_id_prefix: Option<String>,
|
||||
) -> Result<Arc<Timeline>, FocusEventError> {
|
||||
let parsed_event_id = EventId::parse(&event_id).map_err(|err| {
|
||||
FocusEventError::InvalidEventId { event_id: event_id.clone(), err: err.to_string() }
|
||||
})?;
|
||||
|
||||
pub fn display_name(&self) -> Result<String, ClientError> {
|
||||
let r = self.inner.clone();
|
||||
RUNTIME.block_on(async move { Ok(r.display_name().await?.to_string()) })
|
||||
let room = &self.inner;
|
||||
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
let timeline = match builder
|
||||
.with_focus(TimelineFocus::Event { target: parsed_event_id, num_context_events })
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
if let matrix_sdk_ui::timeline::Error::PaginationError(
|
||||
PaginationError::Paginator(PaginatorError::EventNotFound(..)),
|
||||
) = err
|
||||
{
|
||||
return Err(FocusEventError::EventNotFound { event_id: event_id.to_string() });
|
||||
}
|
||||
return Err(FocusEventError::Other { msg: err.to_string() });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let is_encrypted = room.is_encrypted().await?;
|
||||
Ok(is_encrypted)
|
||||
})
|
||||
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
|
||||
}
|
||||
|
||||
pub async fn members(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
|
||||
Ok(Arc::new(RoomMembersIterator::new(self.inner.members(RoomMemberships::empty()).await?)))
|
||||
}
|
||||
|
||||
pub async fn member(&self, user_id: String) -> Result<Arc<RoomMember>, ClientError> {
|
||||
pub async fn members_no_sync(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
|
||||
Ok(Arc::new(RoomMembersIterator::new(
|
||||
self.inner.members_no_sync(RoomMemberships::empty()).await?,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("No user found")?;
|
||||
Ok(Arc::new(RoomMember::new(member)))
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
Ok(member.into())
|
||||
}
|
||||
|
||||
pub fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = room.get_member(&user_id).await?.context("No user found")?;
|
||||
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
|
||||
Ok(avatar_url_string)
|
||||
})
|
||||
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
|
||||
Ok(avatar_url_string)
|
||||
}
|
||||
|
||||
pub fn member_display_name(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = room.get_member(&user_id).await?.context("No user found")?;
|
||||
let avatar_url_string = member.display_name().map(|m| m.to_owned());
|
||||
Ok(avatar_url_string)
|
||||
})
|
||||
pub async fn member_display_name(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
let avatar_url_string = member.display_name().map(|m| m.to_owned());
|
||||
Ok(avatar_url_string)
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
let avatar_url = self.inner.avatar_url();
|
||||
|
||||
// Look for a local event in the `Timeline`.
|
||||
//
|
||||
// First off, let's see if a `Timeline` exists…
|
||||
if let Some(timeline) = self.timeline.read().await.clone() {
|
||||
// If it contains a `latest_event`…
|
||||
if let Some(timeline_last_event) = timeline.inner.latest_event().await {
|
||||
// If it's a local echo…
|
||||
if timeline_last_event.is_local_echo() {
|
||||
return Ok(RoomInfo::new(
|
||||
&self.inner,
|
||||
avatar_url,
|
||||
Some(Arc::new(EventTimelineItem(timeline_last_event))),
|
||||
)
|
||||
.await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, fallback to the classical path.
|
||||
let latest_event = match self.inner.latest_event() {
|
||||
Some(latest_event) => matrix_sdk_ui::timeline::EventTimelineItem::from_latest_event(
|
||||
self.inner.client(),
|
||||
self.inner.room_id(),
|
||||
latest_event,
|
||||
)
|
||||
.await
|
||||
.map(EventTimelineItem)
|
||||
.map(Arc::new),
|
||||
None => None,
|
||||
};
|
||||
Ok(RoomInfo::new(&self.inner, avatar_url, latest_event).await?)
|
||||
Ok(RoomInfo::new(&self.inner).await?)
|
||||
}
|
||||
|
||||
pub fn subscribe_to_room_info_updates(
|
||||
@@ -250,6 +282,24 @@ impl Room {
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn set_is_favourite(
|
||||
&self,
|
||||
is_favourite: bool,
|
||||
tag_order: Option<f64>,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.set_is_favourite(is_favourite, tag_order).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_is_low_priority(
|
||||
&self,
|
||||
is_low_priority: bool,
|
||||
tag_order: Option<f64>,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.set_is_low_priority(is_low_priority, tag_order).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -258,12 +308,14 @@ impl Room {
|
||||
///
|
||||
/// * `reason` - The reason for the event being redacted (optional).
|
||||
/// its transaction ID (optional). If not given one is created.
|
||||
pub fn redact(&self, event_id: String, reason: Option<String>) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
self.inner.redact(&event_id, reason.as_deref(), None).await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn redact(
|
||||
&self,
|
||||
event_id: String,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
self.inner.redact(&event_id, reason.as_deref(), None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn active_members_count(&self) -> u64 {
|
||||
@@ -288,78 +340,66 @@ impl Room {
|
||||
///
|
||||
/// * `score` - The score to rate this content as where -100 is most
|
||||
/// offensive and 0 is inoffensive (optional).
|
||||
pub fn report_content(
|
||||
pub async fn report_content(
|
||||
&self,
|
||||
event_id: String,
|
||||
score: Option<i32>,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let int_score = score.map(|value| value.into());
|
||||
RUNTIME.block_on(async move {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
self.inner
|
||||
.client()
|
||||
.send(
|
||||
report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
reason,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
self.inner
|
||||
.client()
|
||||
.send(
|
||||
report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
reason,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ignores a user.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_id` - The ID of the user to ignore.
|
||||
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.client().account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
})
|
||||
/// * `user_id` - The ID of the user to ignore.
|
||||
pub async fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.client().account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leave this room.
|
||||
///
|
||||
/// Only invited and joined rooms can be left.
|
||||
pub fn leave(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.leave().await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn leave(&self) -> Result<(), ClientError> {
|
||||
self.inner.leave().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Join this room.
|
||||
///
|
||||
/// Only invited and left rooms can be joined via this method.
|
||||
pub fn join(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.join().await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn join(&self) -> Result<(), ClientError> {
|
||||
self.inner.join().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets a new name to the room.
|
||||
pub fn set_name(&self, name: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.set_name(name).await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn set_name(&self, name: String) -> Result<(), ClientError> {
|
||||
self.inner.set_name(name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets a new topic in the room.
|
||||
pub fn set_topic(&self, topic: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.set_room_topic(&topic).await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn set_topic(&self, topic: String) -> Result<(), ClientError> {
|
||||
self.inner.set_room_topic(&topic).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Upload and set the room's avatar.
|
||||
@@ -375,48 +415,47 @@ impl Room {
|
||||
/// * `data` - The raw data that will be uploaded to the homeserver's
|
||||
/// content repository
|
||||
/// * `media_info` - The media info used as avatar image info.
|
||||
pub fn upload_avatar(
|
||||
pub async fn upload_avatar(
|
||||
&self,
|
||||
mime_type: String,
|
||||
data: Vec<u8>,
|
||||
media_info: Option<ImageInfo>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner
|
||||
.upload_avatar(
|
||||
&mime,
|
||||
data,
|
||||
media_info
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidMediaInfo)?,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner
|
||||
.upload_avatar(
|
||||
&mime,
|
||||
data,
|
||||
media_info
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidMediaInfo)?,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the current room avatar
|
||||
pub fn remove_avatar(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.remove_avatar().await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
|
||||
self.inner.remove_avatar().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn invite_user_by_id(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user = <&UserId>::try_from(user_id.as_str())
|
||||
.context("Could not create user from string")?;
|
||||
self.inner.invite_user_by_id(user).await?;
|
||||
Ok(())
|
||||
})
|
||||
pub async fn invite_user_by_id(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user =
|
||||
<&UserId>::try_from(user_id.as_str()).context("Could not create user from string")?;
|
||||
self.inner.invite_user_by_id(user).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn can_user_redact(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
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(&user_id).await?)
|
||||
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> {
|
||||
@@ -494,6 +533,218 @@ impl Room {
|
||||
pub async fn typing_notice(&self, is_typing: bool) -> Result<(), ClientError> {
|
||||
Ok(self.inner.typing_notice(is_typing).await?)
|
||||
}
|
||||
|
||||
pub fn subscribe_to_typing_notifications(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn TypingNotificationsListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let (_event_handler_drop_guard, mut subscriber) =
|
||||
self.inner.subscribe_to_typing_notifications();
|
||||
while let Ok(typing_user_ids) = subscriber.recv().await {
|
||||
let typing_user_ids =
|
||||
typing_user_ids.into_iter().map(|user_id| user_id.to_string()).collect();
|
||||
listener.call(typing_user_ids);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Set (or unset) a flag on the room to indicate that the user has
|
||||
/// explicitly marked it as unread.
|
||||
pub async fn set_unread_flag(&self, new_value: bool) -> Result<(), ClientError> {
|
||||
Ok(self.inner.set_unread_flag(new_value).await?)
|
||||
}
|
||||
|
||||
/// Mark a room as read, by attaching a read receipt on the latest event.
|
||||
///
|
||||
/// Note: this does NOT unset the unread flag; it's the caller's
|
||||
/// responsibility to do so, if needs be.
|
||||
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
|
||||
let timeline = self.timeline().await?;
|
||||
|
||||
timeline.mark_as_read(receipt_type).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
let power_levels = self.inner.room_power_levels().await?;
|
||||
Ok(RoomPowerLevels::from(power_levels))
|
||||
}
|
||||
|
||||
pub async fn apply_power_level_changes(
|
||||
&self,
|
||||
changes: RoomPowerLevelChanges,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.apply_power_level_changes(changes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_power_levels_for_users(
|
||||
&self,
|
||||
updates: Vec<UserPowerLevelUpdate>,
|
||||
) -> Result<(), ClientError> {
|
||||
let updates = updates
|
||||
.iter()
|
||||
.map(|update| {
|
||||
let user_id: &UserId = update.user_id.as_str().try_into()?;
|
||||
let power_level = Int::new(update.power_level).context("Invalid power level")?;
|
||||
Ok((user_id, power_level))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
self.inner
|
||||
.update_power_levels(updates)
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn suggested_role_for_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<RoomMemberRole, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
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 matrix_to_permalink(&self) -> Result<String, ClientError> {
|
||||
Ok(self.inner.matrix_to_permalink().await?.to_string())
|
||||
}
|
||||
|
||||
pub async fn matrix_to_event_permalink(&self, event_id: String) -> Result<String, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
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
|
||||
pub async fn send_call_notification_if_needed(&self) -> Result<(), ClientError> {
|
||||
self.inner.send_call_notification_if_needed().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.inner.send_queue().is_enabled()
|
||||
}
|
||||
|
||||
/// Enable or disable the send queue for that particular room.
|
||||
pub fn enable_send_queue(&self, enable: bool) {
|
||||
self.inner.send_queue().set_enabled(enable);
|
||||
}
|
||||
|
||||
/// 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).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?)
|
||||
}
|
||||
|
||||
/// 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?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given room alias.
|
||||
#[uniffi::export]
|
||||
pub fn matrix_to_room_alias_permalink(
|
||||
room_alias: String,
|
||||
) -> std::result::Result<String, ClientError> {
|
||||
let room_alias = RoomAliasId::parse(room_alias)?;
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
@@ -501,6 +752,11 @@ pub trait RoomInfoListener: Sync + Send {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait TypingNotificationsListener: Sync + Send {
|
||||
fn call(&self, typing_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMembersIterator {
|
||||
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
|
||||
@@ -518,13 +774,43 @@ impl RoomMembersIterator {
|
||||
self.chunk_iterator.len()
|
||||
}
|
||||
|
||||
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<Arc<RoomMember>>> {
|
||||
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
|
||||
self.chunk_iterator
|
||||
.next(chunk_size)
|
||||
.map(|members| members.into_iter().map(RoomMember::new).map(Arc::new).collect())
|
||||
.map(|members| members.into_iter().map(|m| m.into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a member considered to be a room hero.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomHero {
|
||||
/// The user ID of the hero.
|
||||
user_id: String,
|
||||
/// The display name of the hero.
|
||||
display_name: Option<String>,
|
||||
/// The avatar URL of the hero.
|
||||
avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkRoomHero> for RoomHero {
|
||||
fn from(value: SdkRoomHero) -> Self {
|
||||
Self {
|
||||
user_id: value.user_id.to_string(),
|
||||
display_name: value.display_name.clone(),
|
||||
avatar_url: value.avatar_url.as_ref().map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An update for a particular user's power level within the room.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UserPowerLevelUpdate {
|
||||
/// The user ID of the user to update.
|
||||
user_id: String,
|
||||
/// The power level to assign to the user.
|
||||
power_level: i64,
|
||||
}
|
||||
|
||||
impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
@@ -549,3 +835,15 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RtcApplicationType {
|
||||
Call,
|
||||
}
|
||||
impl From<RtcApplicationType> for notify::ApplicationType {
|
||||
fn from(value: RtcApplicationType) -> Self {
|
||||
match value {
|
||||
RtcApplicationType::Call => notify::ApplicationType::Call,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2024 Mauro Romito
|
||||
// Copyright 2024 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::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PublicRoomJoinRule {
|
||||
Public,
|
||||
Knock,
|
||||
}
|
||||
|
||||
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
|
||||
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
|
||||
rule => Err(format!("unsupported join rule: {rule:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDescription {
|
||||
pub room_id: String,
|
||||
pub name: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
pub alias: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub join_rule: Option<PublicRoomJoinRule>,
|
||||
pub is_world_readable: bool,
|
||||
pub joined_members: u64,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescription {
|
||||
fn from(value: matrix_sdk::room_directory_search::RoomDescription) -> Self {
|
||||
Self {
|
||||
room_id: value.room_id.to_string(),
|
||||
name: value.name,
|
||||
topic: value.topic,
|
||||
alias: value.alias.map(|alias| alias.to_string()),
|
||||
avatar_url: value.avatar_url.map(|url| url.to_string()),
|
||||
join_rule: value.join_rule.try_into().ok(),
|
||||
is_world_readable: value.is_world_readable,
|
||||
joined_members: value.joined_members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomDirectorySearch {
|
||||
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
|
||||
}
|
||||
|
||||
impl RoomDirectorySearch {
|
||||
pub fn new(inner: SdkRoomDirectorySearch) -> Self {
|
||||
Self { inner: RwLock::new(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl RoomDirectorySearch {
|
||||
pub async fn next_page(&self) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.next_page().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search(&self, filter: Option<String>, batch_size: u32) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.search(filter, batch_size).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.loaded_pages() as u32)
|
||||
}
|
||||
|
||||
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.is_at_last_page())
|
||||
}
|
||||
|
||||
pub async fn results(
|
||||
&self,
|
||||
listener: Box<dyn RoomDirectorySearchEntriesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.read().await.results();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDirectorySearchEntriesResult {
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomDirectorySearchEntryUpdate {
|
||||
Append { values: Vec<RoomDescription> },
|
||||
Clear,
|
||||
PushFront { value: RoomDescription },
|
||||
PushBack { value: RoomDescription },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: RoomDescription },
|
||||
Set { index: u32, value: RoomDescription },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<RoomDescription> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
for RoomDirectorySearchEntryUpdate
|
||||
{
|
||||
fn from(diff: VectorDiff<matrix_sdk::room_directory_search::RoomDescription>) -> 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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
@@ -1,36 +1,49 @@
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use ruma::OwnedMxcUri;
|
||||
|
||||
use crate::{
|
||||
notification_settings::RoomNotificationMode, room::Membership, room_member::RoomMember,
|
||||
timeline::EventTimelineItem,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
name: Option<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>,
|
||||
/// Room name as defined by the room state event only.
|
||||
raw_name: Option<String>,
|
||||
topic: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
is_direct: bool,
|
||||
is_public: bool,
|
||||
is_space: bool,
|
||||
is_tombstoned: bool,
|
||||
is_favourite: bool,
|
||||
canonical_alias: Option<String>,
|
||||
alternative_aliases: Vec<String>,
|
||||
membership: Membership,
|
||||
latest_event: Option<Arc<EventTimelineItem>>,
|
||||
inviter: Option<Arc<RoomMember>>,
|
||||
/// Member who invited the current user to a room that's in the invited
|
||||
/// state.
|
||||
///
|
||||
/// Can be missing if the room membership invite event is missing from the
|
||||
/// store.
|
||||
inviter: Option<RoomMember>,
|
||||
heroes: Vec<RoomHero>,
|
||||
active_members_count: u64,
|
||||
invited_members_count: u64,
|
||||
joined_members_count: u64,
|
||||
user_power_levels: HashMap<String, i64>,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
has_room_call: bool,
|
||||
active_room_call_participants: Vec<String>,
|
||||
/// Whether this room has been explicitly marked as unread
|
||||
is_marked_unread: bool,
|
||||
/// "Interesting" messages received in that room, independently of the
|
||||
/// notification settings.
|
||||
num_unread_messages: u64,
|
||||
@@ -43,35 +56,43 @@ pub struct RoomInfo {
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(
|
||||
room: &matrix_sdk::Room,
|
||||
avatar_url: Option<OwnedMxcUri>,
|
||||
latest_event: Option<Arc<EventTimelineItem>>,
|
||||
) -> matrix_sdk::Result<Self> {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result<Self> {
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
name: room.name(),
|
||||
display_name: room.cached_display_name().map(|name| name.to_string()),
|
||||
raw_name: room.name(),
|
||||
topic: room.topic(),
|
||||
avatar_url: avatar_url.map(Into::into),
|
||||
avatar_url: room.avatar_url().map(Into::into),
|
||||
is_direct: room.is_direct().await?,
|
||||
is_public: room.is_public(),
|
||||
is_space: room.is_space(),
|
||||
is_tombstoned: room.is_tombstoned(),
|
||||
is_favourite: room.is_favourite(),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
|
||||
membership: room.state().into(),
|
||||
latest_event,
|
||||
inviter: match room.state() {
|
||||
RoomState::Invited => {
|
||||
room.invite_details().await?.inviter.map(|inner| Arc::new(RoomMember { inner }))
|
||||
}
|
||||
RoomState::Invited => room
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|details| details.inviter)
|
||||
.map(Into::into),
|
||||
_ => None,
|
||||
},
|
||||
heroes: room.heroes().into_iter().map(Into::into).collect(),
|
||||
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,
|
||||
user_defined_notification_mode: room
|
||||
@@ -84,6 +105,7 @@ impl RoomInfo {
|
||||
.iter()
|
||||
.map(|u| u.to_string())
|
||||
.collect(),
|
||||
is_marked_unread: room.is_marked_unread(),
|
||||
num_unread_messages: room.num_unread_messages(),
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::{
|
||||
@@ -12,9 +12,18 @@ use matrix_sdk::{
|
||||
},
|
||||
RoomListEntry as MatrixRoomListEntry,
|
||||
};
|
||||
use matrix_sdk_ui::room_list_service::filters::{
|
||||
new_filter_all, new_filter_all_non_left, new_filter_fuzzy_match_room_name, new_filter_none,
|
||||
new_filter_normalized_match_room_name,
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::{
|
||||
filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
|
||||
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
|
||||
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
|
||||
new_filter_unread, RoomCategory,
|
||||
},
|
||||
BoxedFilterFn,
|
||||
},
|
||||
timeline::default_event_filter,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -23,6 +32,7 @@ use crate::{
|
||||
room::Room,
|
||||
room_info::RoomInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
timeline_event_filter::TimelineEventTypeFilter,
|
||||
TaskHandle, RUNTIME,
|
||||
};
|
||||
|
||||
@@ -38,6 +48,14 @@ pub enum RoomListError {
|
||||
RoomNotFound { room_name: String },
|
||||
#[error("invalid room ID: {error}")]
|
||||
InvalidRoomId { error: String },
|
||||
#[error("A timeline instance already exists for room {room_name}")]
|
||||
TimelineAlreadyExists { room_name: String },
|
||||
#[error("A timeline instance hasn't been initialized for room {room_name}")]
|
||||
TimelineNotInitialized { room_name: String },
|
||||
#[error("Timeline couldn't be initialized: {error}")]
|
||||
InitializingTimeline { error: String },
|
||||
#[error("Event cache ran into an error: {error}")]
|
||||
EventCache { error: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
@@ -49,6 +67,13 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
UnknownList(list_name) => Self::UnknownList { list_name },
|
||||
InputCannotBeApplied(_) => Self::InputCannotBeApplied,
|
||||
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
|
||||
TimelineAlreadyExists(room_id) => {
|
||||
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
|
||||
}
|
||||
InitializingTimeline(source) => {
|
||||
Self::InitializingTimeline { error: source.to_string() }
|
||||
}
|
||||
EventCache(error) => Self::EventCache { error: error.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +108,7 @@ impl From<RoomListInput> for matrix_sdk_ui::room_list_service::Input {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListService {
|
||||
pub(crate) inner: Arc<matrix_sdk_ui::RoomListService>,
|
||||
pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
@@ -103,7 +129,8 @@ impl RoomListService {
|
||||
let room_id = <&RoomId>::try_from(room_id.as_str()).map_err(RoomListError::from)?;
|
||||
|
||||
Ok(Arc::new(RoomListItem {
|
||||
inner: Arc::new(RUNTIME.block_on(async { self.inner.room(room_id).await })?),
|
||||
inner: Arc::new(self.inner.room(room_id)?),
|
||||
utd_hook: self.utd_hook.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -114,13 +141,6 @@ impl RoomListService {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn invites(self: Arc<Self>) -> Result<Arc<RoomList>, RoomListError> {
|
||||
Ok(Arc::new(RoomList {
|
||||
room_list_service: self.clone(),
|
||||
inner: Arc::new(self.inner.invites().await.map_err(RoomListError::from)?),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn apply_input(&self, input: RoomListInput) -> Result<(), RoomListError> {
|
||||
self.inner.apply_input(input.into()).await.map(|_| ()).map_err(Into::into)
|
||||
}
|
||||
@@ -193,7 +213,10 @@ impl RoomList {
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> RoomListEntriesWithDynamicAdaptersResult {
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
self.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
self.inner.entries_with_dynamic_adapters(
|
||||
page_size.try_into().unwrap(),
|
||||
self.room_list_service.inner.client().roominfo_update_receiver(),
|
||||
);
|
||||
|
||||
RoomListEntriesWithDynamicAdaptersResult {
|
||||
controller: Arc::new(RoomListDynamicEntriesController::new(
|
||||
@@ -375,19 +398,8 @@ impl RoomListDynamicEntriesController {
|
||||
#[uniffi::export]
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
|
||||
use RoomListEntriesDynamicFilterKind as Kind;
|
||||
|
||||
match kind {
|
||||
Kind::All => self.inner.set_filter(new_filter_all()),
|
||||
Kind::AllNonLeft => self.inner.set_filter(new_filter_all_non_left(&self.client)),
|
||||
Kind::None => self.inner.set_filter(new_filter_none()),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
self.inner.set_filter(new_filter_normalized_match_room_name(&self.client, &pattern))
|
||||
}
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
self.inner.set_filter(new_filter_fuzzy_match_room_name(&self.client, &pattern))
|
||||
}
|
||||
}
|
||||
let FilterWrapper(filter) = FilterWrapper::from(&self.client, kind);
|
||||
self.inner.set_filter(filter)
|
||||
}
|
||||
|
||||
fn add_one_page(&self) {
|
||||
@@ -401,16 +413,70 @@ impl RoomListDynamicEntriesController {
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All,
|
||||
AllNonLeft,
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
NonLeft,
|
||||
Joined,
|
||||
Unread,
|
||||
Favourite,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
None,
|
||||
NormalizedMatchRoomName { pattern: String },
|
||||
FuzzyMatchRoomName { pattern: String },
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListFilterCategory {
|
||||
Group,
|
||||
People,
|
||||
}
|
||||
|
||||
impl From<RoomListFilterCategory> for RoomCategory {
|
||||
fn from(value: RoomListFilterCategory) -> Self {
|
||||
match value {
|
||||
RoomListFilterCategory::Group => Self::Group,
|
||||
RoomListFilterCategory::People => Self::People,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom internal type to transform a `RoomListEntriesDynamicFilterKind` into
|
||||
/// a `BoxedFilterFn`.
|
||||
struct FilterWrapper(BoxedFilterFn);
|
||||
|
||||
impl FilterWrapper {
|
||||
fn from(client: &matrix_sdk::Client, value: RoomListEntriesDynamicFilterKind) -> Self {
|
||||
use RoomListEntriesDynamicFilterKind as Kind;
|
||||
|
||||
match value {
|
||||
Kind::All { filters } => Self(Box::new(new_filter_all(
|
||||
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
|
||||
))),
|
||||
Kind::Any { filters } => Self(Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
|
||||
))),
|
||||
Kind::NonLeft => Self(Box::new(new_filter_non_left(client))),
|
||||
Kind::Joined => Self(Box::new(new_filter_joined(client))),
|
||||
Kind::Unread => Self(Box::new(new_filter_unread(client))),
|
||||
Kind::Favourite => Self(Box::new(new_filter_favourite(client))),
|
||||
Kind::Invite => Self(Box::new(new_filter_invite(client))),
|
||||
Kind::Category { expect } => Self(Box::new(new_filter_category(client, expect.into()))),
|
||||
Kind::None => Self(Box::new(new_filter_none())),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
Self(Box::new(new_filter_normalized_match_room_name(client, &pattern)))
|
||||
}
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
Self(Box::new(new_filter_fuzzy_match_room_name(client, &pattern)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListItem {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
@@ -419,8 +485,11 @@ impl RoomListItem {
|
||||
self.inner.id().to_string()
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<String> {
|
||||
RUNTIME.block_on(async { self.inner.name().await })
|
||||
/// Returns the room's name from the state event if available, otherwise
|
||||
/// compute a room name based on the room's nature (DM or not) and number of
|
||||
/// members.
|
||||
fn display_name(&self) -> Option<String> {
|
||||
self.inner.cached_display_name()
|
||||
}
|
||||
|
||||
fn avatar_url(&self) -> Option<String> {
|
||||
@@ -428,7 +497,7 @@ impl RoomListItem {
|
||||
}
|
||||
|
||||
fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(async { self.inner.inner_room().is_direct().await.unwrap_or(false) })
|
||||
RUNTIME.block_on(self.inner.inner_room().is_direct()).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn canonical_alias(&self) -> Option<String> {
|
||||
@@ -436,25 +505,65 @@ impl RoomListItem {
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
let avatar_url = self.inner.avatar_url();
|
||||
let latest_event = self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new);
|
||||
Ok(RoomInfo::new(self.inner.inner_room(), avatar_url, latest_event).await?)
|
||||
Ok(RoomInfo::new(self.inner.inner_room()).await?)
|
||||
}
|
||||
|
||||
/// Building a `Room`.
|
||||
/// Build a full `Room` FFI object, filling its associated timeline.
|
||||
///
|
||||
/// Be careful that building a `Room` builds its entire `Timeline` at the
|
||||
/// same time.
|
||||
async fn full_room(&self) -> Arc<Room> {
|
||||
Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(self.inner.timeline().await)))),
|
||||
))
|
||||
/// If its internal timeline hasn't been initialized, it'll fail.
|
||||
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if let Some(timeline) = self.inner.timeline() {
|
||||
Ok(Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
|
||||
)))
|
||||
} else {
|
||||
Err(RoomListError::TimelineNotInitialized {
|
||||
room_name: self.inner.inner_room().room_id().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary workaround for coroutine leaks on Kotlin.
|
||||
fn full_room_blocking(&self) -> Arc<Room> {
|
||||
RUNTIME.block_on(async move { self.full_room().await })
|
||||
/// Checks whether the Room's timeline has been initialized before.
|
||||
fn is_timeline_initialized(&self) -> bool {
|
||||
self.inner.is_timeline_initialized()
|
||||
}
|
||||
|
||||
/// Initializes the timeline for this room using the provided parameters.
|
||||
///
|
||||
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
|
||||
/// used to filter timeline events besides the default timeline filter. If
|
||||
/// `None` is passed, only the default timeline filter will be used.
|
||||
/// * `internal_id_prefix` - An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
async fn init_timeline(
|
||||
&self,
|
||||
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
|
||||
internal_id_prefix: Option<String>,
|
||||
) -> Result<(), RoomListError> {
|
||||
let mut timeline_builder = self
|
||||
.inner
|
||||
.default_room_timeline_builder()
|
||||
.await
|
||||
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
|
||||
|
||||
if let Some(event_type_filter) = event_type_filter {
|
||||
timeline_builder = timeline_builder.event_filter(move |event, room_version_id| {
|
||||
// Always perform the default filter first
|
||||
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
timeline_builder = timeline_builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
if let Some(utd_hook) = self.utd_hook.clone() {
|
||||
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
|
||||
}
|
||||
|
||||
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
|
||||
}
|
||||
|
||||
fn subscribe(&self, settings: Option<RoomSubscription>) {
|
||||
@@ -468,14 +577,6 @@ impl RoomListItem {
|
||||
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
|
||||
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
|
||||
}
|
||||
|
||||
fn has_unread_notifications(&self) -> bool {
|
||||
self.inner.has_unread_notifications()
|
||||
}
|
||||
|
||||
fn unread_notifications(&self) -> Arc<UnreadNotificationsCount> {
|
||||
Arc::new(self.inner.unread_notifications().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, uniffi::Enum)]
|
||||
@@ -513,6 +614,7 @@ pub struct RequiredState {
|
||||
pub struct RoomSubscription {
|
||||
pub required_state: Option<Vec<RequiredState>>,
|
||||
pub timeline_limit: Option<u32>,
|
||||
pub include_heroes: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<RoomSubscription> for RumaRoomSubscription {
|
||||
@@ -521,7 +623,8 @@ impl From<RoomSubscription> for RumaRoomSubscription {
|
||||
required_state: val.required_state.map(|r|
|
||||
r.into_iter().map(|s| (s.key.into(), s.value)).collect()
|
||||
).unwrap_or_default(),
|
||||
timeline_limit: val.timeline_limit.map(|u| u.into())
|
||||
timeline_limit: val.timeline_limit.map(|u| u.into()),
|
||||
include_heroes: val.include_heroes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use matrix_sdk::room::RoomMember as SdkRoomMember;
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::UserId;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::ClientError;
|
||||
use crate::error::ClientError;
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MembershipState {
|
||||
@@ -35,203 +35,57 @@ impl From<matrix_sdk::ruma::events::room::member::MembershipState> for Membershi
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
|
||||
MembershipState::Leave
|
||||
}
|
||||
_ => todo!(
|
||||
_ => unimplemented!(
|
||||
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMember {
|
||||
pub(crate) inner: SdkRoomMember,
|
||||
#[uniffi::export]
|
||||
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
|
||||
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
|
||||
RoomMemberRole::suggested_role_for_power_level(power_level)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl RoomMember {
|
||||
pub fn user_id(&self) -> String {
|
||||
self.inner.user_id().to_string()
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Option<String> {
|
||||
self.inner.display_name().map(|d| d.to_owned())
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self) -> Option<String> {
|
||||
self.inner.avatar_url().map(ToString::to_string)
|
||||
}
|
||||
|
||||
pub fn membership(&self) -> MembershipState {
|
||||
self.inner.membership().to_owned().into()
|
||||
}
|
||||
|
||||
pub fn is_name_ambiguous(&self) -> bool {
|
||||
self.inner.name_ambiguous()
|
||||
}
|
||||
|
||||
pub fn power_level(&self) -> i64 {
|
||||
self.inner.power_level()
|
||||
}
|
||||
|
||||
pub fn normalized_power_level(&self) -> i64 {
|
||||
self.inner.normalized_power_level()
|
||||
}
|
||||
|
||||
pub fn is_ignored(&self) -> bool {
|
||||
self.inner.is_ignored()
|
||||
}
|
||||
|
||||
pub fn is_account_user(&self) -> bool {
|
||||
self.inner.is_account_user()
|
||||
}
|
||||
|
||||
/// Adds the room member to the current account data's ignore list
|
||||
/// which will ignore the user across all rooms.
|
||||
pub fn ignore(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.ignore().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the room member from the current account data's ignore list
|
||||
/// which will unignore the user across all rooms.
|
||||
pub fn unignore(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.unignore().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn can_ban(&self) -> bool {
|
||||
self.inner.can_ban()
|
||||
}
|
||||
|
||||
pub fn can_invite(&self) -> bool {
|
||||
self.inner.can_invite()
|
||||
}
|
||||
|
||||
pub fn can_kick(&self) -> bool {
|
||||
self.inner.can_kick()
|
||||
}
|
||||
|
||||
pub fn can_redact(&self) -> bool {
|
||||
self.inner.can_redact()
|
||||
}
|
||||
|
||||
pub fn can_send_state(&self, state_event: StateEventType) -> bool {
|
||||
self.inner.can_send_state(state_event.into())
|
||||
}
|
||||
|
||||
pub fn can_send_message(&self, event: MessageLikeEventType) -> bool {
|
||||
self.inner.can_send_message(event.into())
|
||||
}
|
||||
|
||||
pub fn can_trigger_room_notification(&self) -> bool {
|
||||
self.inner.can_trigger_room_notification()
|
||||
}
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
|
||||
// It's not possible to expose methods on an Enum through Uniffi ☹️
|
||||
role.suggested_power_level()
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub fn new(room_member: SdkRoomMember) -> Self {
|
||||
RoomMember { inner: room_member }
|
||||
}
|
||||
/// Generates a `matrix.to` permalink to the given userID.
|
||||
#[uniffi::export]
|
||||
pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
Ok(user_id.matrix_to_uri().to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum StateEventType {
|
||||
CallMember,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomMember {
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub membership: MembershipState,
|
||||
pub is_name_ambiguous: bool,
|
||||
pub power_level: i64,
|
||||
pub normalized_power_level: i64,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
}
|
||||
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAliases => Self::RoomAliases,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
StateEventType::RoomEncryption => Self::RoomEncryption,
|
||||
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules => Self::RoomJoinRules,
|
||||
StateEventType::RoomMemberEvent => Self::RoomMember,
|
||||
StateEventType::RoomName => Self::RoomName,
|
||||
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
|
||||
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
|
||||
StateEventType::RoomServerAcl => Self::RoomServerAcl,
|
||||
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
|
||||
StateEventType::RoomTombstone => Self::RoomTombstone,
|
||||
StateEventType::RoomTopic => Self::RoomTopic,
|
||||
StateEventType::SpaceChild => Self::SpaceChild,
|
||||
StateEventType::SpaceParent => Self::SpaceParent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MessageLikeEventType {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationDone,
|
||||
ReactionSent,
|
||||
RoomEncrypted,
|
||||
RoomMessage,
|
||||
RoomRedaction,
|
||||
Sticker,
|
||||
}
|
||||
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
fn from(val: MessageLikeEventType) -> Self {
|
||||
match val {
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
|
||||
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
|
||||
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
|
||||
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
|
||||
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
|
||||
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
|
||||
MessageLikeEventType::ReactionSent => Self::Reaction,
|
||||
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
|
||||
MessageLikeEventType::RoomMessage => Self::RoomMessage,
|
||||
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
|
||||
MessageLikeEventType::Sticker => Self::Sticker,
|
||||
impl From<SdkRoomMember> for RoomMember {
|
||||
fn from(m: SdkRoomMember) -> Self {
|
||||
RoomMember {
|
||||
user_id: m.user_id().to_string(),
|
||||
display_name: m.display_name().map(|s| s.to_owned()),
|
||||
avatar_url: m.avatar_url().map(|a| a.to_string()),
|
||||
membership: m.membership().clone().into(),
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level(),
|
||||
normalized_power_level: m.normalized_power_level(),
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
|
||||
use ruma::space::SpaceRoomJoinRule;
|
||||
|
||||
/// The preview of a room, be it invited/joined/left, or not.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPreview {
|
||||
/// The room id for this room.
|
||||
pub room_id: String,
|
||||
/// The canonical alias for the room.
|
||||
pub canonical_alias: Option<String>,
|
||||
/// The room's name, if set.
|
||||
pub name: Option<String>,
|
||||
/// The room's topic, if set.
|
||||
pub topic: Option<String>,
|
||||
/// The MXC URI to the room's avatar, if set.
|
||||
pub avatar_url: Option<String>,
|
||||
/// The number of joined members.
|
||||
pub num_joined_members: u64,
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: Option<String>,
|
||||
/// Is the history world-readable for this room?
|
||||
pub is_history_world_readable: bool,
|
||||
/// Is the room joined by the current user?
|
||||
pub is_joined: bool,
|
||||
/// Is the current user invited to this room?
|
||||
pub is_invited: bool,
|
||||
/// is the join rule public for this room?
|
||||
pub is_public: bool,
|
||||
/// Can we knock (or restricted-knock) to this room?
|
||||
pub can_knock: bool,
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
pub(crate) fn from_sdk(preview: SdkRoomPreview) -> Self {
|
||||
Self {
|
||||
room_id: preview.room_id.to_string(),
|
||||
canonical_alias: preview.canonical_alias.map(|alias| alias.to_string()),
|
||||
name: preview.name,
|
||||
topic: preview.topic,
|
||||
avatar_url: preview.avatar_url.map(|url| url.to_string()),
|
||||
num_joined_members: preview.num_joined_members,
|
||||
room_type: preview.room_type.map(|room_type| room_type.to_string()),
|
||||
is_history_world_readable: preview.is_world_readable,
|
||||
is_joined: preview.state.map_or(false, |state| state == RoomState::Joined),
|
||||
is_invited: preview.state.map_or(false, |state| state == RoomState::Invited),
|
||||
is_public: preview.join_rule == SpaceRoomJoinRule::Public,
|
||||
can_knock: matches!(
|
||||
preview.join_rule,
|
||||
SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Knock
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use matrix_sdk::attachment::{
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
call::notify::NotifyType as RumaNotifyType,
|
||||
location::AssetType as RumaAssetType,
|
||||
poll::start::PollKind as RumaPollKind,
|
||||
room::{
|
||||
@@ -44,8 +45,9 @@ use ruma::{
|
||||
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
serde::JsonObject,
|
||||
OwnedUserId, UInt, UserId,
|
||||
MatrixToUri, MatrixUri as RumaMatrixUri, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
@@ -55,6 +57,72 @@ use crate::{
|
||||
utils::u64_to_uint,
|
||||
};
|
||||
|
||||
/// Parse a matrix entity from a given URI, be it either
|
||||
/// a `matrix.to` link or a `matrix:` URI
|
||||
#[uniffi::export]
|
||||
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
|
||||
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
|
||||
return Some(MatrixEntity {
|
||||
id: matrix_uri.id().into(),
|
||||
via: matrix_uri.via().iter().map(|via| via.to_string()).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(matrix_to_uri) = MatrixToUri::parse(&uri) {
|
||||
return Some(MatrixEntity {
|
||||
id: matrix_to_uri.id().into(),
|
||||
via: matrix_to_uri.via().iter().map(|via| via.to_string()).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// A Matrix entity that can be a room, room alias, user, or event, and a list
|
||||
/// of via servers.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct MatrixEntity {
|
||||
id: MatrixId,
|
||||
via: Vec<String>,
|
||||
}
|
||||
|
||||
/// A Matrix ID that can be a room, room alias, user, or event.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MatrixId {
|
||||
Room { id: String },
|
||||
RoomAlias { alias: String },
|
||||
User { id: String },
|
||||
EventOnRoomId { room_id: String, event_id: String },
|
||||
EventOnRoomAlias { alias: String, event_id: String },
|
||||
}
|
||||
|
||||
impl From<&RumaMatrixId> for MatrixId {
|
||||
fn from(value: &RumaMatrixId) -> Self {
|
||||
match value {
|
||||
RumaMatrixId::User(id) => MatrixId::User { id: id.to_string() },
|
||||
RumaMatrixId::Room(id) => MatrixId::Room { id: id.to_string() },
|
||||
RumaMatrixId::RoomAlias(id) => MatrixId::RoomAlias { alias: id.to_string() },
|
||||
|
||||
RumaMatrixId::Event(room_id_or_alias, event_id) => {
|
||||
if room_id_or_alias.is_room_id() {
|
||||
MatrixId::EventOnRoomId {
|
||||
room_id: room_id_or_alias.to_string(),
|
||||
event_id: event_id.to_string(),
|
||||
}
|
||||
} else if room_id_or_alias.is_room_alias_id() {
|
||||
MatrixId::EventOnRoomAlias {
|
||||
alias: room_id_or_alias.to_string(),
|
||||
event_id: event_id.to_string(),
|
||||
}
|
||||
} else {
|
||||
panic!("Unexpected MatrixId type: {:?}", room_id_or_alias)
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected MatrixId type: {:?}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
@@ -172,23 +240,38 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
formatted: content.formatted.map(Into::into),
|
||||
}))
|
||||
}
|
||||
MessageType::Image { content } => Self::Image(
|
||||
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Audio { content } => Self::Audio(
|
||||
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Video { content } => Self::Video(
|
||||
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::File { content } => Self::File(
|
||||
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.filename(content.filename)
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Image { content } => {
|
||||
let mut event_content =
|
||||
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::Image(event_content)
|
||||
}
|
||||
MessageType::Audio { content } => {
|
||||
let mut event_content =
|
||||
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::Audio(event_content)
|
||||
}
|
||||
MessageType::Video { content } => {
|
||||
let mut event_content =
|
||||
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::Video(event_content)
|
||||
}
|
||||
MessageType::File { content } => {
|
||||
let mut event_content =
|
||||
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::File(event_content)
|
||||
}
|
||||
MessageType::Notice { content } => {
|
||||
Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), {
|
||||
formatted: content.formatted.map(Into::into),
|
||||
@@ -221,6 +304,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::Image(c) => MessageType::Image {
|
||||
content: ImageMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
@@ -228,6 +313,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
@@ -237,6 +324,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::Video(c) => MessageType::Video {
|
||||
content: VideoMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
@@ -244,6 +333,7 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
content: FileMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
@@ -286,6 +376,30 @@ impl From<RumaMessageType> for MessageType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum NotifyType {
|
||||
Ring,
|
||||
Notify,
|
||||
}
|
||||
|
||||
impl From<RumaNotifyType> for NotifyType {
|
||||
fn from(val: RumaNotifyType) -> Self {
|
||||
match val {
|
||||
RumaNotifyType::Ring => Self::Ring,
|
||||
_ => Self::Notify,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotifyType> for RumaNotifyType {
|
||||
fn from(value: NotifyType) -> Self {
|
||||
match value {
|
||||
NotifyType::Ring => RumaNotifyType::Ring,
|
||||
NotifyType::Notify => RumaNotifyType::Notify,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct EmoteMessageContent {
|
||||
pub body: String,
|
||||
@@ -295,6 +409,8 @@ pub struct EmoteMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ImageMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<ImageInfo>,
|
||||
}
|
||||
@@ -302,6 +418,8 @@ pub struct ImageMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct AudioMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<AudioInfo>,
|
||||
pub audio: Option<UnstableAudioDetailsContent>,
|
||||
@@ -311,6 +429,8 @@ pub struct AudioMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct VideoMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<VideoInfo>,
|
||||
}
|
||||
@@ -318,6 +438,7 @@ pub struct VideoMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct FileMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<FileInfo>,
|
||||
|
||||
@@ -246,7 +246,10 @@ impl SessionVerificationController {
|
||||
}
|
||||
break;
|
||||
}
|
||||
SasState::Started { .. } | SasState::Accepted { .. } | SasState::Confirmed => (),
|
||||
SasState::Created { .. }
|
||||
| SasState::Started { .. }
|
||||
| SasState::Accepted { .. }
|
||||
| SasState::Confirmed => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,20 @@
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::Client;
|
||||
use matrix_sdk_ui::sync_service::{
|
||||
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
|
||||
SyncServiceBuilder as MatrixSyncServiceBuilder,
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, Client};
|
||||
use matrix_sdk_ui::{
|
||||
sync_service::{
|
||||
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
|
||||
SyncServiceBuilder as MatrixSyncServiceBuilder,
|
||||
},
|
||||
unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
|
||||
},
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
|
||||
@@ -53,12 +59,16 @@ pub trait SyncServiceStateObserver: Send + Sync + Debug {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SyncService {
|
||||
pub(crate) inner: Arc<MatrixSyncService>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl SyncService {
|
||||
pub fn room_list_service(&self) -> Arc<RoomListService> {
|
||||
Arc::new(RoomListService { inner: self.inner.room_list_service() })
|
||||
Arc::new(RoomListService {
|
||||
inner: self.inner.room_list_service(),
|
||||
utd_hook: self.utd_hook.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start(&self) {
|
||||
@@ -84,12 +94,19 @@ impl SyncService {
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct SyncServiceBuilder {
|
||||
client: Client,
|
||||
builder: MatrixSyncServiceBuilder,
|
||||
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
impl SyncServiceBuilder {
|
||||
pub(crate) fn new(client: Client) -> Arc<Self> {
|
||||
Arc::new(Self { builder: MatrixSyncService::builder(client) })
|
||||
Arc::new(Self {
|
||||
client: client.clone(),
|
||||
builder: MatrixSyncService::builder(client),
|
||||
utd_hook: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +115,100 @@ impl SyncServiceBuilder {
|
||||
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_cross_process_lock(app_identifier);
|
||||
Arc::new(Self { builder })
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
pub async fn with_utd_hook(
|
||||
self: Arc<Self>,
|
||||
delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
) -> Arc<Self> {
|
||||
// UTDs detected before this duration may be reclassified as "late decryption"
|
||||
// events (or discarded, if they get decrypted fast enough).
|
||||
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
|
||||
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
|
||||
let mut utd_hook = UtdHookManager::new(Arc::new(UtdHook { delegate }), this.client.clone())
|
||||
.with_max_delay(UTD_HOOK_GRACE_PERIOD);
|
||||
|
||||
if let Err(e) = utd_hook.reload_from_store().await {
|
||||
error!("Unable to reload UTD hook data from data store: {}", e);
|
||||
// Carry on with the setup anyway; we shouldn't fail setup just
|
||||
// because the UTD hook failed to load its data.
|
||||
}
|
||||
|
||||
Arc::new(Self {
|
||||
client: this.client,
|
||||
builder: this.builder,
|
||||
utd_hook: Some(Arc::new(utd_hook)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Ok(Arc::new(SyncService { inner: Arc::new(this.builder.build().await?) }))
|
||||
Ok(Arc::new(SyncService {
|
||||
inner: Arc::new(this.builder.build().await?),
|
||||
utd_hook: this.utd_hook,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait UnableToDecryptDelegate: Sync + Send {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo);
|
||||
}
|
||||
|
||||
struct UtdHook {
|
||||
delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UtdHook {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UtdHook").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnableToDecryptHook for UtdHook {
|
||||
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
|
||||
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
|
||||
|
||||
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
|
||||
// not considered UTDs.
|
||||
if let Some(duration) = &info.time_to_decrypt {
|
||||
if *duration < IGNORE_UTD_PERIOD {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Report the UTD to the client.
|
||||
self.delegate.on_utd(info.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UnableToDecryptInfo {
|
||||
/// The identifier of the event that couldn't get decrypted.
|
||||
event_id: String,
|
||||
|
||||
/// If the event could be decrypted late (that is, the event was encrypted
|
||||
/// at first, but could be decrypted later on), then this indicates the
|
||||
/// time it took to decrypt the event. If it is not set, this is
|
||||
/// considered a definite UTD.
|
||||
///
|
||||
/// If set, this is in milliseconds.
|
||||
pub time_to_decrypt_ms: Option<u64>,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent when
|
||||
/// we were not a member of this room?
|
||||
pub cause: UtdCause,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
fn from(value: SdkUnableToDecryptInfo) -> Self {
|
||||
Self {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
cause: value.cause,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
|
||||
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
|
||||
use ruma::events::{
|
||||
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
|
||||
FullStateEventContent,
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use super::ProfileDetails;
|
||||
@@ -36,15 +41,24 @@ impl TimelineItemContent {
|
||||
TimelineItemContentKind::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
url: content.url.to_string(),
|
||||
source: Arc::new(MediaSource::from(content.source.clone())),
|
||||
}
|
||||
}
|
||||
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
|
||||
Content::CallInvite => TimelineItemContentKind::CallInvite,
|
||||
Content::CallNotify => TimelineItemContentKind::CallNotify,
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
|
||||
}
|
||||
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
|
||||
user_id: membership.user_id().to_string(),
|
||||
user_display_name: if let FullStateEventContent::Original { content, .. } =
|
||||
membership.content()
|
||||
{
|
||||
content.displayname.clone()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
change: membership.change().map(Into::into),
|
||||
},
|
||||
Content::ProfileChange(profile) => {
|
||||
@@ -101,7 +115,7 @@ pub enum TimelineItemContentKind {
|
||||
Sticker {
|
||||
body: String,
|
||||
info: ImageInfo,
|
||||
url: String,
|
||||
source: Arc<MediaSource>,
|
||||
},
|
||||
Poll {
|
||||
question: String,
|
||||
@@ -112,11 +126,14 @@ pub enum TimelineItemContentKind {
|
||||
end_time: Option<u64>,
|
||||
has_been_edited: bool,
|
||||
},
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
UnableToDecrypt {
|
||||
msg: EncryptedMessage,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
change: Option<MembershipChange>,
|
||||
},
|
||||
ProfileChange {
|
||||
@@ -164,6 +181,10 @@ impl Message {
|
||||
pub fn is_edited(&self) -> bool {
|
||||
self.0.is_edited()
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(self.0.msgtype().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -172,6 +193,12 @@ pub struct InReplyToDetails {
|
||||
event: RepliedToEventDetails,
|
||||
}
|
||||
|
||||
impl InReplyToDetails {
|
||||
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
let event_id = inner.event_id.to_string();
|
||||
@@ -211,6 +238,10 @@ pub enum EncryptedMessage {
|
||||
MegolmV1AesSha2 {
|
||||
/// The ID of the session used to encrypt the message.
|
||||
session_id: String,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent
|
||||
/// when we were not a member of this room?
|
||||
cause: UtdCause,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
@@ -224,9 +255,9 @@ impl EncryptedMessage {
|
||||
let sender_key = sender_key.clone();
|
||||
Self::OlmV1Curve25519AesSha2 { sender_key }
|
||||
}
|
||||
Message::MegolmV1AesSha2 { session_id, .. } => {
|
||||
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
|
||||
let session_id = session_id.clone();
|
||||
Self::MegolmV1AesSha2 { session_id }
|
||||
Self::MegolmV1AesSha2 { session_id, cause: *cause }
|
||||
}
|
||||
Message::Unknown => Self::Unknown,
|
||||
}
|
||||
@@ -307,7 +338,7 @@ pub enum OtherState {
|
||||
RoomJoinRules,
|
||||
RoomName { name: Option<String> },
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite { display_name: Option<String> },
|
||||
RoomTombstone,
|
||||
@@ -350,7 +381,20 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Self::RoomName { name }
|
||||
}
|
||||
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
|
||||
Content::RoomPowerLevels(_) => Self::RoomPowerLevels,
|
||||
Content::RoomPowerLevels(c) => match c {
|
||||
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
|
||||
users: power_level_user_changes(content, prev_content)
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect(),
|
||||
previous: prev_content.as_ref().map(|prev_content| {
|
||||
prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
|
||||
}),
|
||||
},
|
||||
FullContent::Redacted(_) => {
|
||||
Self::RoomPowerLevels { users: Default::default(), previous: None }
|
||||
}
|
||||
},
|
||||
Content::RoomServerAcl(_) => Self::RoomServerAcl,
|
||||
Content::RoomThirdPartyInvite(c) => {
|
||||
let display_name = match c {
|
||||
|
||||
@@ -16,13 +16,16 @@ use std::{collections::HashMap, fmt::Write as _, fs, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
use content::{InReplyToDetails, RepliedToEventDetails};
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use matrix_sdk::attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{BackPaginationStatus, EventItemOrigin, Profile, TimelineDetails};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
events::{
|
||||
@@ -43,28 +46,30 @@ use ruma::{
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId,
|
||||
EventId, OwnedTransactionId,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::{AbortHandle, JoinHandle},
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
helpers::unwrap_or_clone_arc,
|
||||
ruma::{AssetType, AudioInfo, FileInfo, ImageInfo, PollKind, ThumbnailInfo, VideoInfo},
|
||||
ruma::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, PollKind, ThumbnailInfo,
|
||||
VideoInfo,
|
||||
},
|
||||
task_handle::TaskHandle,
|
||||
RUNTIME,
|
||||
};
|
||||
|
||||
mod content;
|
||||
|
||||
pub use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
#[repr(transparent)]
|
||||
pub struct Timeline {
|
||||
@@ -106,12 +111,12 @@ impl Timeline {
|
||||
|
||||
async fn send_attachment(
|
||||
&self,
|
||||
url: String,
|
||||
filename: String,
|
||||
mime_type: Mime,
|
||||
attachment_config: AttachmentConfig,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<(), RoomError> {
|
||||
let request = self.inner.send_attachment(url, mime_type, attachment_config);
|
||||
let request = self.inner.send_attachment(filename, mime_type, attachment_config);
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
@@ -128,24 +133,28 @@ impl Timeline {
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Timeline {
|
||||
pub async fn add_listener(
|
||||
&self,
|
||||
listener: Box<dyn TimelineListener>,
|
||||
) -> RoomTimelineListenerResult {
|
||||
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
|
||||
let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await;
|
||||
let timeline_stream = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
|
||||
// It's important that the initial items are passed *before* we forward the
|
||||
// stream updates, with a guaranteed ordering. Otherwise, it could
|
||||
// be that the listener be called before the initial items have been
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
|
||||
values: timeline_items,
|
||||
}))]);
|
||||
|
||||
// Then forward new items.
|
||||
while let Some(diffs) = timeline_stream.next().await {
|
||||
listener
|
||||
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
|
||||
}
|
||||
}));
|
||||
|
||||
RoomTimelineListenerResult {
|
||||
items: timeline_items.into_iter().map(TimelineItem::from_arc).collect(),
|
||||
items_stream: Arc::new(timeline_stream),
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn retry_decryption(self: Arc<Self>, session_ids: Vec<String>) {
|
||||
@@ -158,15 +167,19 @@ impl Timeline {
|
||||
self.inner.fetch_members().await
|
||||
}
|
||||
|
||||
pub fn subscribe_to_back_pagination_status(
|
||||
pub async fn subscribe_to_back_pagination_status(
|
||||
&self,
|
||||
listener: Box<dyn BackPaginationStatusListener>,
|
||||
listener: Box<dyn PaginationStatusListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let mut subscriber = self.inner.back_pagination_status();
|
||||
let (initial, mut subscriber) = self
|
||||
.inner
|
||||
.live_back_pagination_status()
|
||||
.await
|
||||
.context("can't subscribe to the back-pagination status on a focused timeline")?;
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// Send the current state even if it hasn't changed right away.
|
||||
listener.on_update(subscriber.next_now());
|
||||
listener.on_update(initial);
|
||||
|
||||
while let Some(status) = subscriber.next().await {
|
||||
listener.on_update(status);
|
||||
@@ -174,32 +187,59 @@ impl Timeline {
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Loads older messages into the timeline.
|
||||
/// Paginate backwards, whether we are in focused mode or in live mode.
|
||||
///
|
||||
/// Raises an exception if there are no timeline listeners.
|
||||
pub fn paginate_backwards(&self, opts: PaginationOptions) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async { Ok(self.inner.paginate_backwards(opts.into()).await?) })
|
||||
/// Returns whether we hit the end of the timeline or not.
|
||||
pub async fn paginate_backwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.paginate_backwards(num_events).await?)
|
||||
}
|
||||
|
||||
pub fn send_read_receipt(
|
||||
/// Paginate forwards, when in focused mode.
|
||||
///
|
||||
/// Returns whether we hit the end of the timeline or not.
|
||||
pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.focused_paginate_forwards(num_events).await?)
|
||||
}
|
||||
|
||||
pub async fn send_read_receipt(
|
||||
&self,
|
||||
receipt_type: ReceiptType,
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
self.inner
|
||||
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
self.inner
|
||||
.send_single_receipt(receipt_type.into(), ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send(self: Arc<Self>, msg: Arc<RoomMessageEventContentWithoutRelation>) {
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send((*msg).to_owned().with_relation(None).into()).await;
|
||||
});
|
||||
/// Mark the room as read by trying to attach an *unthreaded* read receipt
|
||||
/// to the latest room event.
|
||||
///
|
||||
/// This works even if the latest event belongs to a thread, as a threaded
|
||||
/// reply also belongs to the unthreaded timeline. No threaded receipt
|
||||
/// will be sent here (see also #3123).
|
||||
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
|
||||
self.inner.mark_as_read(receipt_type.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queues an event in the room's send queue so it's processed for
|
||||
/// sending later.
|
||||
///
|
||||
/// Returns an abort handle that allows to abort sending, if it hasn't
|
||||
/// happened yet.
|
||||
pub async fn send(
|
||||
self: Arc<Self>,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
) -> Result<Arc<AbortSendHandle>, ClientError> {
|
||||
match self.inner.send((*msg).to_owned().with_relation(None).into()).await {
|
||||
Ok(handle) => Ok(Arc::new(AbortSendHandle { inner: Mutex::new(Some(handle)) })),
|
||||
Err(err) => {
|
||||
error!("error when sending a message: {err}");
|
||||
Err(anyhow::anyhow!(err).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_image(
|
||||
@@ -207,6 +247,8 @@ impl Timeline {
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
image_info: ImageInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -227,7 +269,9 @@ impl Timeline {
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
}
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -238,6 +282,8 @@ impl Timeline {
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
video_info: VideoInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -258,7 +304,9 @@ impl Timeline {
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
}
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -268,6 +316,8 @@ impl Timeline {
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -280,7 +330,10 @@ impl Timeline {
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Audio(base_audio_info);
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -291,6 +344,8 @@ impl Timeline {
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -304,7 +359,10 @@ impl Timeline {
|
||||
|
||||
let attachment_info =
|
||||
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -332,7 +390,7 @@ impl Timeline {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn create_poll(
|
||||
pub async fn create_poll(
|
||||
self: Arc<Self>,
|
||||
question: String,
|
||||
answers: Vec<String>,
|
||||
@@ -348,14 +406,14 @@ impl Timeline {
|
||||
let event_content =
|
||||
AnyMessageLikeEventContent::UnstablePollStart(poll_start_event_content.into());
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send(event_content).await;
|
||||
});
|
||||
if let Err(err) = self.inner.send(event_content).await {
|
||||
error!("unable to start poll: {err}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_poll_response(
|
||||
pub async fn send_poll_response(
|
||||
self: Arc<Self>,
|
||||
poll_start_id: String,
|
||||
answers: Vec<String>,
|
||||
@@ -367,9 +425,9 @@ impl Timeline {
|
||||
let event_content =
|
||||
AnyMessageLikeEventContent::UnstablePollResponse(poll_response_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send(event_content).await;
|
||||
});
|
||||
if let Err(err) = self.inner.send(event_content).await {
|
||||
error!("unable to send poll response: {err}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -385,35 +443,35 @@ impl Timeline {
|
||||
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send(event_content).await;
|
||||
if let Err(err) = self.inner.send(event_content).await {
|
||||
error!("unable to end poll: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_reply(
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes).await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
self.inner
|
||||
.send_reply((*msg).clone(), &reply_item.0, ForwardThread::Yes)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn edit(
|
||||
pub async fn edit(
|
||||
&self,
|
||||
new_content: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
edit_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.edit((*new_content).clone().with_relation(None), &edit_item.0).await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
self.inner
|
||||
.edit((*new_content).clone(), &edit_item.0)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -426,18 +484,14 @@ impl Timeline {
|
||||
edit_item: Arc<EventTimelineItem>,
|
||||
) -> Result<(), ClientError> {
|
||||
let poll_data = PollData { question, answers, max_selections, poll_kind };
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
self.inner
|
||||
.edit_poll(poll_data.fallback_text(), poll_data.try_into()?, &edit_item.0)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
self.inner
|
||||
.edit_poll(poll_data.fallback_text(), poll_data.try_into()?, &edit_item.0)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_location(
|
||||
pub async fn send_location(
|
||||
self: Arc<Self>,
|
||||
body: String,
|
||||
geo_uri: String,
|
||||
@@ -461,85 +515,158 @@ impl Timeline {
|
||||
let room_message_event_content = RoomMessageEventContentWithoutRelation::new(
|
||||
MessageType::Location(location_event_message_content),
|
||||
);
|
||||
self.send(Arc::new(room_message_event_content))
|
||||
// Errors are logged in `Self::send` already.
|
||||
let _ = self.send(Arc::new(room_message_event_content)).await;
|
||||
}
|
||||
|
||||
pub fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
|
||||
pub async fn toggle_reaction(&self, event_id: String, key: String) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.toggle_reaction(&Annotation::new(event_id, key)).await?;
|
||||
Ok(())
|
||||
})
|
||||
self.inner.toggle_reaction(&Annotation::new(event_id, key)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let event_id = <&EventId>::try_from(event_id.as_str())?;
|
||||
RUNTIME.block_on(async {
|
||||
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
|
||||
Ok(())
|
||||
})
|
||||
self.inner.fetch_details_for_event(event_id).await.context("Fetching event details")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn retry_send(self: Arc<Self>, txn_id: String) {
|
||||
RUNTIME.spawn(async move {
|
||||
if let Err(e) = self.inner.retry_send(txn_id.as_str().into()).await {
|
||||
error!(txn_id, "Failed to retry sending: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cancel_send(self: Arc<Self>, txn_id: String) {
|
||||
RUNTIME.spawn(async move {
|
||||
if !self.inner.cancel_send(txn_id.as_str().into()).await {
|
||||
info!(txn_id, "Failed to discard local echo: Not found");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_event_timeline_item_by_event_id(
|
||||
/// Get the current timeline item for the given event ID, if any.
|
||||
///
|
||||
/// Will return a remote event, *or* a local echo that has been sent but not
|
||||
/// yet replaced by a remote echo.
|
||||
///
|
||||
/// It's preferable to store the timeline items in the model for your UI, if
|
||||
/// possible, instead of just storing IDs and coming back to the timeline
|
||||
/// object to look up items.
|
||||
pub async fn get_event_timeline_item_by_event_id(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<Arc<EventTimelineItem>, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
RUNTIME.block_on(async {
|
||||
let item = self
|
||||
.inner
|
||||
.item_by_event_id(&event_id)
|
||||
.await
|
||||
.context("Item with given event ID not found")?;
|
||||
|
||||
Ok(Arc::new(EventTimelineItem(item)))
|
||||
})
|
||||
let item = self
|
||||
.inner
|
||||
.item_by_event_id(&event_id)
|
||||
.await
|
||||
.context("Item with given event ID not found")?;
|
||||
Ok(Arc::new(EventTimelineItem(item)))
|
||||
}
|
||||
|
||||
pub fn get_timeline_event_content_by_event_id(
|
||||
/// Get the current timeline item for the given transaction ID, if any.
|
||||
///
|
||||
/// This will always return a local echo, if found.
|
||||
///
|
||||
/// It's preferable to store the timeline items in the model for your UI, if
|
||||
/// possible, instead of just storing IDs and coming back to the timeline
|
||||
/// object to look up items.
|
||||
pub async fn get_event_timeline_item_by_transaction_id(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
RUNTIME.block_on(async {
|
||||
let item = self
|
||||
.inner
|
||||
.item_by_event_id(&event_id)
|
||||
.await
|
||||
.context("Item with given event ID not found")?;
|
||||
transaction_id: String,
|
||||
) -> Result<Arc<EventTimelineItem>, ClientError> {
|
||||
let transaction_id: OwnedTransactionId = transaction_id.into();
|
||||
let item = self
|
||||
.inner
|
||||
.item_by_transaction_id(&transaction_id)
|
||||
.await
|
||||
.context("Item with given transaction ID not found")?;
|
||||
Ok(Arc::new(EventTimelineItem(item)))
|
||||
}
|
||||
|
||||
let msgtype = item
|
||||
.content()
|
||||
.as_message()
|
||||
.context("Item with given event ID is not a message")?
|
||||
.msgtype()
|
||||
.to_owned();
|
||||
/// Redacts an event from the timeline.
|
||||
///
|
||||
/// Only works for events that exist as timeline items.
|
||||
///
|
||||
/// If it was a local event, this will *try* to cancel it, if it was not
|
||||
/// being sent already. If the event was a remote event, then it will be
|
||||
/// redacted by sending a redaction request to the server.
|
||||
///
|
||||
/// Returns whether the redaction did happen. It can only return false for
|
||||
/// local events that are being processed.
|
||||
pub async fn redact_event(
|
||||
&self,
|
||||
item: Arc<EventTimelineItem>,
|
||||
reason: Option<String>,
|
||||
) -> Result<bool, ClientError> {
|
||||
let removed = self
|
||||
.inner
|
||||
.redact(&item.0, reason.as_deref())
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
|
||||
})
|
||||
Ok(removed)
|
||||
}
|
||||
|
||||
/// Load the reply details for the given event id.
|
||||
///
|
||||
/// This will return an `InReplyToDetails` object that contains the details
|
||||
/// which will either be ready or an error.
|
||||
pub async fn load_reply_details(
|
||||
&self,
|
||||
event_id_str: String,
|
||||
) -> Result<InReplyToDetails, ClientError> {
|
||||
let event_id = EventId::parse(&event_id_str)?;
|
||||
|
||||
match self.inner.room().event(&event_id).await {
|
||||
Ok(timeline_event) => {
|
||||
let replied_to = RepliedToEvent::try_from_timeline_event_for_room(
|
||||
timeline_event,
|
||||
self.inner.room(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
RepliedToEventDetails::Ready {
|
||||
content: Arc::new(TimelineItemContent(replied_to.content().clone())),
|
||||
sender: replied_to.sender().to_string(),
|
||||
sender_profile: replied_to.sender_profile().into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
Err(e) => Ok(InReplyToDetails::new(
|
||||
event_id_str,
|
||||
RepliedToEventDetails::Error { message: e.to_string() },
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomTimelineListenerResult {
|
||||
pub items: Vec<Arc<TimelineItem>>,
|
||||
pub items_stream: Arc<TaskHandle>,
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct AbortSendHandle {
|
||||
inner: Mutex<Option<matrix_sdk::send_queue::AbortSendHandle>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl AbortSendHandle {
|
||||
/// Try to abort the sending of the current event.
|
||||
///
|
||||
/// If this returns `true`, then the sending could be aborted, because the
|
||||
/// event hasn't been sent yet. Otherwise, if this returns `false`, the
|
||||
/// event had already been sent and could not be aborted.
|
||||
///
|
||||
/// This has an effect only on the first call; subsequent calls will always
|
||||
/// return `false`.
|
||||
async fn abort(self: Arc<Self>) -> bool {
|
||||
if let Some(inner) = self.inner.lock().await.take() {
|
||||
inner.abort().await
|
||||
} else {
|
||||
warn!("trying to abort an send handle that's already been actioned");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum FocusEventError {
|
||||
#[error("the event id parameter {event_id} is incorrect: {err}")]
|
||||
InvalidEventId { event_id: String, err: String },
|
||||
|
||||
#[error("the event {event_id} could not be found")]
|
||||
EventNotFound { event_id: String },
|
||||
|
||||
#[error("error when trying to focus on an event: {msg}")]
|
||||
Other { msg: String },
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
@@ -548,8 +675,8 @@ pub trait TimelineListener: Sync + Send {
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait BackPaginationStatusListener: Sync + Send {
|
||||
fn on_update(&self, status: BackPaginationStatus);
|
||||
pub trait PaginationStatusListener: Sync + Send {
|
||||
fn on_update(&self, status: LiveBackPaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
@@ -709,8 +836,8 @@ impl TimelineItem {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unique_id(&self) -> u64 {
|
||||
self.0.unique_id()
|
||||
pub fn unique_id(&self) -> String {
|
||||
self.0.unique_id().to_owned()
|
||||
}
|
||||
|
||||
pub fn fmt_debug(&self) -> String {
|
||||
@@ -725,10 +852,16 @@ pub enum EventSendState {
|
||||
NotSentYet,
|
||||
/// The local event has been sent to the server, but unsuccessfully: The
|
||||
/// sending has failed.
|
||||
SendingFailed { error: String },
|
||||
/// Sending has been cancelled because an earlier event in the
|
||||
/// message-sending queue failed.
|
||||
Cancelled,
|
||||
SendingFailed {
|
||||
/// Stringified error message.
|
||||
error: String,
|
||||
/// Whether the error is considered recoverable or not.
|
||||
///
|
||||
/// An error that's recoverable will disable the room's send queue,
|
||||
/// while an unrecoverable error will be parked, until the user
|
||||
/// decides to cancel sending it.
|
||||
is_recoverable: bool,
|
||||
},
|
||||
/// The local event has been sent successfully to the server.
|
||||
Sent { event_id: String },
|
||||
}
|
||||
@@ -739,8 +872,9 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
|
||||
|
||||
match value {
|
||||
NotSentYet => Self::NotSentYet,
|
||||
SendingFailed { error } => Self::SendingFailed { error: error.to_string() },
|
||||
Cancelled => Self::Cancelled,
|
||||
SendingFailed { error, is_recoverable } => {
|
||||
Self::SendingFailed { error: error.to_string(), is_recoverable: *is_recoverable }
|
||||
}
|
||||
Sent { event_id } => Self::Sent { event_id: event_id.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -939,32 +1073,6 @@ impl SendAttachmentJoinHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PaginationOptions {
|
||||
SimpleRequest { event_limit: u16, wait_for_token: bool },
|
||||
UntilNumItems { event_limit: u16, items: u16, wait_for_token: bool },
|
||||
}
|
||||
|
||||
impl From<PaginationOptions> for matrix_sdk_ui::timeline::PaginationOptions<'static> {
|
||||
fn from(value: PaginationOptions) -> Self {
|
||||
use matrix_sdk_ui::timeline::PaginationOptions as Opts;
|
||||
let (wait_for_token, mut opts) = match value {
|
||||
PaginationOptions::SimpleRequest { event_limit, wait_for_token } => {
|
||||
(wait_for_token, Opts::simple_request(event_limit))
|
||||
}
|
||||
PaginationOptions::UntilNumItems { event_limit, items, wait_for_token } => {
|
||||
(wait_for_token, Opts::until_num_items(event_limit, items))
|
||||
}
|
||||
};
|
||||
|
||||
if wait_for_token {
|
||||
opts = opts.wait_for_token();
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VirtualTimelineItem {
|
||||
@@ -991,7 +1099,7 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
|
||||
fn from(value: ReceiptType) -> Self {
|
||||
match value {
|
||||
ReceiptType::Read => Self::Read,
|
||||
ReceiptType::ReadPrivate => Self::Read,
|
||||
ReceiptType::ReadPrivate => Self::ReadPrivate,
|
||||
ReceiptType::FullyRead => Self::FullyRead,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
|
||||
use ruma::events::{AnySyncTimelineEvent, TimelineEventType};
|
||||
|
||||
use crate::event::{MessageLikeEventType, StateEventType};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
#[uniffi::constructor]
|
||||
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineEventTypeFilter {
|
||||
/// Filters an [`event`] to decide whether it should be part of the timeline
|
||||
/// based on [`AnySyncTimelineEvent::event_type()`].
|
||||
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
|
||||
self.inner.filter(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum FilterTimelineEventType {
|
||||
MessageLike { event_type: MessageLikeEventType },
|
||||
State { event_type: StateEventType },
|
||||
}
|
||||
|
||||
impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
fn from(value: FilterTimelineEventType) -> TimelineEventType {
|
||||
match value {
|
||||
FilterTimelineEventType::MessageLike { event_type } => {
|
||||
ruma::events::MessageLikeEventType::from(event_type).into()
|
||||
}
|
||||
FilterTimelineEventType::State { event_type } => {
|
||||
ruma::events::StateEventType::from(event_type).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,13 +260,21 @@ pub fn new_virtual_element_call_widget(
|
||||
/// but should only be done as temporal workarounds until this function is
|
||||
/// adjusted
|
||||
#[uniffi::export]
|
||||
pub fn get_element_call_required_permissions() -> WidgetCapabilities {
|
||||
pub fn get_element_call_required_permissions(own_user_id: String) -> WidgetCapabilities {
|
||||
use ruma::events::StateEventType;
|
||||
|
||||
WidgetCapabilities {
|
||||
read: vec![
|
||||
// TODO: we really should not have this permission in here, since it is not used
|
||||
// anymore. The only reason `org.matrix.msc3401.call` is still here is to
|
||||
// not break current EC deployments. (EC still expects to get this
|
||||
// permission even though its not using it.) https://github.com/element-hq/element-call/pull/2399 needs to be merged and deployed
|
||||
WidgetEventFilter::StateWithType { event_type: "org.matrix.msc3401.call".to_owned() },
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
|
||||
WidgetEventFilter::StateWithType {
|
||||
event_type: StateEventType::RoomEncryption.to_string(),
|
||||
},
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
@@ -275,11 +283,14 @@ pub fn get_element_call_required_permissions() -> WidgetCapabilities {
|
||||
},
|
||||
],
|
||||
send: vec![
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
|
||||
WidgetEventFilter::StateWithType {
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: own_user_id.clone(),
|
||||
},
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
WidgetEventFilter::StateWithType {
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[bindings.kotlin]
|
||||
package_name = "org.matrix.rustcomponents.sdk"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
android_cleaner = true
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
This models the room key sharing algorithm as a decision tree and provides
|
||||
This models the room key forwarding algorithm as a decision tree and provides
|
||||
tooling to render it as a PDF or PNG.
|
||||
|
||||
# Usage
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
digraph {
|
||||
label="Matrix room key sharing algorithm"
|
||||
label="Matrix room key forwarding algorithm"
|
||||
fontname="Fira Sans"
|
||||
ratio=0.5
|
||||
|
||||
@@ -16,10 +16,10 @@ digraph {
|
||||
/* End states */
|
||||
allow_verified [label="Share the entire session from the earliest known index.\n\nOk(None)", color=4, fillcolor=3]
|
||||
allow_limited [label="Share a limited session starting from index i, which is the index we previously shared at.\n\nOk(Some(i))", color=4, fillcolor=3]
|
||||
refuse_device_key_changed [label="Sender key changed, refuse to share.\n\nErr(KeyForwardDecision::ChangedSenderKey)", color=6, fillcolor=5]
|
||||
refuse_not_shared [label="Session was never shared with this device, refuse to share.\n\nErr(KeyForwardDecision::OutboundSessionNotShared)", color=6, fillcolor=5]
|
||||
refuse_untrusted_own_device [label="Our own device, but it is untrusted and we haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::UntrustedDevice)", color=6, fillcolor=5]
|
||||
refuse_missing_outbound_session [label="Not our device and haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::MissingOutboundSession)", color=6, fillcolor=5]
|
||||
refuse_device_key_changed [label="Sender key changed, refuse to forward.\n\nErr(KeyForwardDecision::ChangedSenderKey)", color=6, fillcolor=5]
|
||||
refuse_not_shared [label="Session was never shared with this device, refuse to forward.\n\nErr(KeyForwardDecision::OutboundSessionNotShared)", color=6, fillcolor=5]
|
||||
refuse_untrusted_own_device [label="Our own device, but it is untrusted and we haven't previously shared with it. Refuse to forward.\n\nErr(KeyForwardDecision::UntrustedDevice)", color=6, fillcolor=5]
|
||||
refuse_missing_outbound_session [label="Not our device and haven't previously shared with it. Refuse to forward.\n\nErr(KeyForwardDecision::MissingOutboundSession)", color=6, fillcolor=5]
|
||||
|
||||
/* Checks */
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 151 KiB |
@@ -1,3 +1,16 @@
|
||||
# unreleased
|
||||
|
||||
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one
|
||||
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
|
||||
- `AmbiguityCache` contains the room member's user ID
|
||||
- `Store::get_rooms` and `Store::get_rooms_filtered` are way faster because they
|
||||
don't acquire the lock for every room they read.
|
||||
- `Store::get_rooms`, `Store::get_rooms_filtered` and `Store::get_room` are
|
||||
renamed `Store::rooms`, `Store::rooms_filtered` and `Store::room`.
|
||||
- `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
|
||||
`Client::rooms` and `Client::rooms_filtered`.
|
||||
- `Client::get_stripped_rooms` has finally been removed.
|
||||
|
||||
# 0.7.0
|
||||
|
||||
- Rename `RoomType` to `RoomState`
|
||||
|
||||
@@ -23,6 +23,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
message-ids = ["matrix-sdk-crypto?/message-ids"]
|
||||
experimental-sliding-sync = ["ruma/unstable-msc3575"]
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi"]
|
||||
|
||||
# helpers for testing features build upon this
|
||||
testing = [
|
||||
@@ -38,22 +39,24 @@ as_variant = { workspace = true }
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = "2.1.0"
|
||||
bitflags = { version = "2.4.0", features = ["serde"] }
|
||||
eyeball = { workspace = true }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
growable-bloom-filter = { workspace = true }
|
||||
http = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381"] }
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867"] }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uniffi = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
@@ -63,9 +66,13 @@ futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
web-time = "1.1.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,10 @@
|
||||
|
||||
//! Helpers for creating `std::fmt::Debug` implementations.
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
use std::fmt;
|
||||
|
||||
pub use matrix_sdk_common::debug::*;
|
||||
use ruma::{
|
||||
api::client::{push::get_notifications::v3::Notification, sync::sync_events::v3::InvitedRoom},
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
};
|
||||
use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw};
|
||||
|
||||
/// A wrapper around a slice of `Raw` events that implements `Debug` in a way
|
||||
/// that only prints the event type of each item.
|
||||
@@ -36,47 +32,6 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a notification map 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 each notification.
|
||||
pub struct DebugNotificationMap<'a>(pub &'a BTreeMap<OwnedRoomId, Vec<Notification>>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotificationMap<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
map.entries(self.0.iter().map(|(room_id, raw)| (room_id, DebugNotificationList(raw))));
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugNotificationList<'a>(&'a [Notification]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotificationList<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugNotification));
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugNotification<'a>(&'a Notification);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotification<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Notification")
|
||||
.field("actions", &self.0.actions)
|
||||
.field("event", &DebugRawEvent(&self.0.event))
|
||||
.field("profile_tag", &self.0.profile_tag)
|
||||
.field("read", &self.0.read)
|
||||
.field("room_id", &self.0.room_id)
|
||||
.field("ts", &self.0.ts)
|
||||
.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`.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
//! SDK-specific variations of response types from Ruma.
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
use std::{collections::BTreeMap, fmt, iter};
|
||||
|
||||
pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use ruma::{
|
||||
@@ -23,7 +23,7 @@ use ruma::{
|
||||
member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, EventContentFromType,
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
@@ -34,9 +34,12 @@ use serde::Serialize;
|
||||
|
||||
/// A change in ambiguity of room members that an `m.room.member` event
|
||||
/// triggers.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct AmbiguityChange {
|
||||
/// The user ID of the member that is contained in the state key of the
|
||||
/// `m.room.member` event.
|
||||
pub member_id: OwnedUserId,
|
||||
/// Is the member that is contained in the state key of the `m.room.member`
|
||||
/// event itself ambiguous because of the event.
|
||||
pub member_ambiguous: bool,
|
||||
@@ -46,6 +49,15 @@ pub struct AmbiguityChange {
|
||||
pub ambiguated_member: Option<OwnedUserId>,
|
||||
}
|
||||
|
||||
impl AmbiguityChange {
|
||||
/// Get an iterator over the user IDs listed in this `AmbiguityChange`.
|
||||
pub fn user_ids(&self) -> impl Iterator<Item = &UserId> {
|
||||
iter::once(&*self.member_id)
|
||||
.chain(self.disambiguated_member.as_deref())
|
||||
.chain(self.ambiguated_member.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
@@ -66,6 +78,16 @@ pub struct MembersResponse {
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any event received via sync.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RawAnySyncOrStrippedTimelineEvent {
|
||||
/// An event from a room in joined or left state.
|
||||
Sync(Raw<AnySyncTimelineEvent>),
|
||||
/// An event from a room in invited state.
|
||||
Stripped(Raw<AnyStrippedStateEvent>),
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any raw state event.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
|
||||
@@ -56,4 +56,9 @@ pub enum Error {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
MegolmError(#[from] MegolmError),
|
||||
|
||||
/// An error caused by calling the `BaseClient::receive_all_members`
|
||||
/// function with invalid parameters
|
||||
#[error("receive_all_members function was called with invalid parameters")]
|
||||
InvalidReceiveMembersParameters,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,13 @@ use ruma::events::{
|
||||
poll::unstable_start::SyncUnstablePollStartEvent, room::message::SyncRoomMessageEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedEventId};
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
relation::RelationType,
|
||||
},
|
||||
MxcUri, OwnedEventId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
@@ -25,11 +31,18 @@ pub enum PossibleLatestEvent<'a> {
|
||||
YesRoomMessage(&'a SyncRoomMessageEvent),
|
||||
/// This message is suitable - it is a poll
|
||||
YesPoll(&'a SyncUnstablePollStartEvent),
|
||||
|
||||
/// This message is suitable - it is a call invite
|
||||
YesCallInvite(&'a SyncCallInviteEvent),
|
||||
|
||||
/// This message is suitable - it's a call notification
|
||||
YesCallNotify(&'a SyncCallNotifyEvent),
|
||||
|
||||
// Later: YesState(),
|
||||
// Later: YesReaction(),
|
||||
/// Not suitable - it's a state event
|
||||
NoUnsupportedEventType,
|
||||
/// Not suitable - it's not an m.room.message
|
||||
/// Not suitable - it's not a m.room.message or an edit/replacement
|
||||
NoUnsupportedMessageLikeType,
|
||||
/// Not suitable - it's encrypted
|
||||
NoEncrypted,
|
||||
@@ -40,15 +53,41 @@ pub enum PossibleLatestEvent<'a> {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
|
||||
match event {
|
||||
// Suitable - we have an m.room.message that was not redacted
|
||||
// Suitable - we have an m.room.message that was not redacted or edited
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
if let Some(original_message) = message.as_original() {
|
||||
let is_replacement =
|
||||
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
relation_type == RelationType::Replacement
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if is_replacement {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
} else {
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
|
||||
PossibleLatestEvent::YesPoll(poll)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
|
||||
PossibleLatestEvent::YesCallInvite(invite)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(notify)) => {
|
||||
PossibleLatestEvent::YesCallNotify(notify)
|
||||
}
|
||||
|
||||
// Encrypted events are not suitable
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
@@ -225,10 +264,18 @@ mod tests {
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{
|
||||
invite::{CallInviteEventContent, SyncCallInviteEvent},
|
||||
notify::{
|
||||
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
|
||||
},
|
||||
SessionDescription,
|
||||
},
|
||||
poll::unstable_start::{
|
||||
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, UnstablePollAnswer,
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
relation::Replacement,
|
||||
room::{
|
||||
encrypted::{
|
||||
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
|
||||
@@ -236,27 +283,27 @@ mod tests {
|
||||
},
|
||||
message::{
|
||||
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
|
||||
RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
},
|
||||
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
|
||||
ImageInfo, MediaSource,
|
||||
},
|
||||
sticker::{StickerEventContent, SyncStickerEvent},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
|
||||
MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
|
||||
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
UnsignedRoomRedactionEvent,
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch, UInt,
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
|
||||
|
||||
#[test]
|
||||
fn room_messages_are_suitable() {
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomMessageEventContent::new(MessageType::Image(
|
||||
@@ -280,7 +327,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polls_are_suitable() {
|
||||
fn test_polls_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
|
||||
SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
|
||||
@@ -303,7 +350,51 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_types_of_messagelike_are_unsuitable() {
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
|
||||
SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: CallInviteEventContent::new(
|
||||
"call_id".into(),
|
||||
UInt::new(123).unwrap(),
|
||||
SessionDescription::new("".into(), "".into()),
|
||||
VoipVersionId::V1,
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
|
||||
SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: CallNotifyEventContent::new(
|
||||
"call_id".into(),
|
||||
ApplicationType::Call,
|
||||
NotifyType::Ring,
|
||||
Mentions::new(),
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: StickerEventContent::new(
|
||||
@@ -325,7 +416,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacted_messages_are_suitable() {
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
|
||||
"content": {},
|
||||
@@ -353,7 +444,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_messages_are_unsuitable() {
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomEncryptedEventContent::new(
|
||||
@@ -373,7 +464,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_events_are_unsuitable() {
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
|
||||
content: RoomTopicEventContent::new("".to_owned()),
|
||||
@@ -392,7 +483,31 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_latest_event() {
|
||||
fn test_replacement_events_are_unsuitable() {
|
||||
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
|
||||
event_content.relates_to = Some(Relation::Replacement(Replacement::new(
|
||||
owned_event_id!("$1"),
|
||||
RoomMessageEventContent::text_plain("Hello, world!").into(),
|
||||
)));
|
||||
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: event_content,
|
||||
event_id: owned_event_id!("$2"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_latest_event() {
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct TestStruct {
|
||||
latest_event: LatestEvent,
|
||||
|
||||
@@ -31,15 +31,20 @@ pub mod latest_event;
|
||||
pub mod media;
|
||||
mod rooms;
|
||||
|
||||
mod read_receipts;
|
||||
pub mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
pub mod sync;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
mod test_utils;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
pub use client::BaseClient;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use http;
|
||||
@@ -47,10 +52,12 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomMember, RoomMemberships,
|
||||
RoomState, RoomStateFilter,
|
||||
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, RoomInfoUpdate,
|
||||
RoomMember, RoomMemberships, RoomState, RoomStateFilter,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
|
||||
};
|
||||
pub use store::{StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError};
|
||||
pub use utils::{
|
||||
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
};
|
||||
|
||||
@@ -114,7 +114,7 @@ pub trait MediaEventContent {
|
||||
|
||||
impl MediaEventContent for StickerEventContent {
|
||||
fn source(&self) -> Option<MediaSource> {
|
||||
Some(MediaSource::Plain(self.url.clone()))
|
||||
Some(MediaSource::from(self.source.clone()))
|
||||
}
|
||||
|
||||
fn thumbnail_source(&self) -> Option<MediaSource> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,8 +52,12 @@ pub struct RoomMember {
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub(crate) fn from_parts(member_info: MemberInfo, room_info: &MemberRoomInfo<'_>) -> Self {
|
||||
let MemberInfo { event, profile, presence } = member_info;
|
||||
pub(crate) fn from_parts(
|
||||
event: MemberEvent,
|
||||
profile: Option<MinimalRoomMemberEvent>,
|
||||
presence: Option<PresenceEvent>,
|
||||
room_info: &MemberRoomInfo<'_>,
|
||||
) -> Self {
|
||||
let MemberRoomInfo {
|
||||
power_levels,
|
||||
max_power_level,
|
||||
@@ -122,7 +126,8 @@ impl RoomMember {
|
||||
/// Get the normalized power level of this member.
|
||||
///
|
||||
/// The normalized power level depends on the maximum power level that can
|
||||
/// be found in a certain room, it's always in the range of 0-100.
|
||||
/// be found in a certain room, positive values are always in the range of
|
||||
/// 0-100.
|
||||
pub fn normalized_power_level(&self) -> i64 {
|
||||
if self.max_power_level > 0 {
|
||||
(self.power_level() * 100) / self.max_power_level
|
||||
@@ -160,11 +165,19 @@ impl RoomMember {
|
||||
self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
|
||||
}
|
||||
|
||||
/// Whether this user can redact events based on the power levels.
|
||||
/// Whether this user can redact their own events based on the power levels.
|
||||
///
|
||||
/// Same as `member.can_do(PowerLevelAction::Redact)`.
|
||||
pub fn can_redact(&self) -> bool {
|
||||
self.can_do_impl(|pls| pls.user_can_redact(self.user_id()))
|
||||
/// Same as `member.can_do(PowerLevelAction::RedactOwn)`.
|
||||
pub fn can_redact_own(&self) -> bool {
|
||||
self.can_do_impl(|pls| pls.user_can_redact_own_event(self.user_id()))
|
||||
}
|
||||
|
||||
/// Whether this user can redact events of other users based on the power
|
||||
/// levels.
|
||||
///
|
||||
/// Same as `member.can_do(PowerLevelAction::RedactOther)`.
|
||||
pub fn can_redact_other(&self) -> bool {
|
||||
self.can_do_impl(|pls| pls.user_can_redact_event_of_other(self.user_id()))
|
||||
}
|
||||
|
||||
/// Whether this user can send message events based on the power levels.
|
||||
@@ -222,14 +235,7 @@ impl RoomMember {
|
||||
}
|
||||
}
|
||||
|
||||
// Information about a room member.
|
||||
pub(crate) struct MemberInfo {
|
||||
pub event: MemberEvent,
|
||||
pub(crate) profile: Option<MinimalRoomMemberEvent>,
|
||||
pub(crate) presence: Option<PresenceEvent>,
|
||||
}
|
||||
|
||||
// Information about a the room a member is in.
|
||||
// Information about the room a member is in.
|
||||
pub(crate) struct MemberRoomInfo<'a> {
|
||||
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
|
||||
pub(crate) max_power_level: i64,
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::{
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{Room, RoomInfo, RoomState, RoomStateFilter};
|
||||
pub use normal::{Room, RoomHero, RoomInfo, RoomInfoUpdate, RoomState, RoomStateFilter};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
@@ -30,6 +30,7 @@ use ruma::{
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
tag::{TagName, Tags},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncStateEvent,
|
||||
},
|
||||
@@ -104,6 +105,15 @@ pub struct BaseRoomInfo {
|
||||
/// memberships.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
|
||||
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
|
||||
/// Whether this room has been manually marked as unread.
|
||||
#[serde(default)]
|
||||
pub(crate) is_marked_unread: bool,
|
||||
/// Some notable tags.
|
||||
///
|
||||
/// We are not interested by all the tags. Some tags are more important than
|
||||
/// others, and this field collects them.
|
||||
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
|
||||
pub(crate) notable_tags: RoomNotableTags,
|
||||
}
|
||||
|
||||
impl BaseRoomInfo {
|
||||
@@ -112,19 +122,6 @@ impl BaseRoomInfo {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_room_name(
|
||||
&self,
|
||||
joined_member_count: u64,
|
||||
invited_member_count: u64,
|
||||
heroes: Vec<RoomMember>,
|
||||
) -> DisplayName {
|
||||
calculate_room_name(
|
||||
joined_member_count,
|
||||
invited_member_count,
|
||||
heroes.iter().take(3).map(|mem| mem.name()).collect::<Vec<&str>>(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the room version of this room.
|
||||
///
|
||||
/// For room versions earlier than room version 11, if the event is
|
||||
@@ -285,6 +282,36 @@ impl BaseRoomInfo {
|
||||
self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_notable_tags(&mut self, tags: &Tags) {
|
||||
let mut notable_tags = RoomNotableTags::empty();
|
||||
|
||||
if tags.contains_key(&TagName::Favorite) {
|
||||
notable_tags.insert(RoomNotableTags::FAVOURITE);
|
||||
}
|
||||
|
||||
if tags.contains_key(&TagName::LowPriority) {
|
||||
notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
|
||||
}
|
||||
|
||||
self.notable_tags = notable_tags;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Notable tags, i.e. subset of tags that we are more interested by.
|
||||
///
|
||||
/// We are not interested by all the tags. Some tags are more important than
|
||||
/// others, and this struct describes them.
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
|
||||
pub(crate) struct RoomNotableTags: u8 {
|
||||
/// The `m.favourite` tag.
|
||||
const FAVOURITE = 0b0000_0001;
|
||||
|
||||
/// THe `m.lowpriority` tag.
|
||||
const LOW_PRIORITY = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
trait OptionExt {
|
||||
@@ -317,48 +344,12 @@ impl Default for BaseRoomInfo {
|
||||
tombstone: None,
|
||||
topic: None,
|
||||
rtc_member: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate room name according to step 3 of the [naming algorithm.]
|
||||
fn calculate_room_name(
|
||||
joined_member_count: u64,
|
||||
invited_member_count: u64,
|
||||
heroes: Vec<&str>,
|
||||
) -> DisplayName {
|
||||
let heroes_count = heroes.len() as u64;
|
||||
let invited_joined = invited_member_count + joined_member_count;
|
||||
let invited_joined_minus_one = invited_joined.saturating_sub(1);
|
||||
|
||||
let names = if heroes_count >= invited_joined_minus_one {
|
||||
let mut names = heroes;
|
||||
// stabilize ordering
|
||||
names.sort_unstable();
|
||||
names.join(", ")
|
||||
} else if heroes_count < invited_joined_minus_one && invited_joined > 1 {
|
||||
let mut names = heroes;
|
||||
names.sort_unstable();
|
||||
|
||||
// TODO: What length does the spec want us to use here and in
|
||||
// the `else`?
|
||||
format!("{}, and {} others", names.join(", "), (invited_joined - heroes_count))
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
|
||||
// User is alone.
|
||||
if invited_joined <= 1 {
|
||||
if names.is_empty() {
|
||||
DisplayName::Empty
|
||||
} else {
|
||||
DisplayName::EmptyWas(names)
|
||||
}
|
||||
} else {
|
||||
DisplayName::Calculated(names)
|
||||
}
|
||||
}
|
||||
|
||||
/// The content of an `m.room.create` event, with a required `creator` field.
|
||||
///
|
||||
/// Starting with room version 11, the `creator` field should be removed and the
|
||||
@@ -520,38 +511,39 @@ impl RoomMemberships {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{calculate_room_name, DisplayName};
|
||||
use std::ops::Not;
|
||||
|
||||
use ruma::events::tag::{TagInfo, TagName, Tags};
|
||||
|
||||
use super::{BaseRoomInfo, RoomNotableTags};
|
||||
|
||||
#[test]
|
||||
fn test_calculate_room_name() {
|
||||
let mut actual = calculate_room_name(2, 0, vec!["a"]);
|
||||
assert_eq!(DisplayName::Calculated("a".to_owned()), actual);
|
||||
fn test_handle_notable_tags_favourite() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
actual = calculate_room_name(3, 0, vec!["a", "b"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual);
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::Favorite, TagInfo::default());
|
||||
|
||||
actual = calculate_room_name(4, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
}
|
||||
|
||||
actual = calculate_room_name(5, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
|
||||
#[test]
|
||||
fn test_handle_notable_tags_low_priority() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
actual = calculate_room_name(0, 0, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::LowPriority, TagInfo::default());
|
||||
|
||||
actual = calculate_room_name(1, 0, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
|
||||
actual = calculate_room_name(0, 1, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a", "b"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -118,6 +118,7 @@ impl AmbiguityCache {
|
||||
self.update(room_id, old_map, new_map);
|
||||
|
||||
let change = AmbiguityChange {
|
||||
member_id: member_event.state_key().clone(),
|
||||
disambiguated_member,
|
||||
ambiguated_member,
|
||||
member_ambiguous: ambiguous,
|
||||
@@ -159,12 +160,9 @@ impl AmbiguityCache {
|
||||
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
|
||||
use MembershipState::*;
|
||||
|
||||
let old_event = if let Some(m) = changes
|
||||
.state
|
||||
.get(room_id)
|
||||
.and_then(|events| events.get(&StateEventType::RoomMember))
|
||||
.and_then(|m| m.get(member_event.state_key().as_str()))
|
||||
{
|
||||
let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str())
|
||||
}) {
|
||||
Some(RawMemberEvent::Sync(m.clone().cast()))
|
||||
} else {
|
||||
self.store.get_member_event(room_id, member_event.state_key()).await?
|
||||
@@ -173,20 +171,15 @@ impl AmbiguityCache {
|
||||
// FIXME: Use let chains once stable
|
||||
let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) {
|
||||
if matches!(event.membership(), Join | Invite) {
|
||||
let display_name = if let Some(d) = changes
|
||||
.profiles
|
||||
.get(room_id)
|
||||
.and_then(|p| p.get(member_event.state_key()))
|
||||
.and_then(|p| p.as_original())
|
||||
.and_then(|p| p.content.displayname.as_deref())
|
||||
{
|
||||
let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| {
|
||||
p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref()
|
||||
}) {
|
||||
Some(d.to_owned())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, member_event.state_key())
|
||||
.await?
|
||||
.and_then(|p| p.into_original())
|
||||
.and_then(|p| p.content.displayname)
|
||||
.and_then(|p| p.into_original()?.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloomBuilder;
|
||||
use matrix_sdk_test::test_json;
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
@@ -41,8 +42,8 @@ use crate::{
|
||||
|
||||
/// `StateStore` integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the [``]
|
||||
/// macro.
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`statestore_integration_tests!`] macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait StateStoreIntegrationTests {
|
||||
@@ -58,8 +59,12 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_member_saving(&self);
|
||||
/// Test filter saving.
|
||||
async fn test_filter_saving(&self);
|
||||
/// Test saving a user avatar URL.
|
||||
async fn test_user_avatar_url_saving(&self);
|
||||
/// Test sync token saving.
|
||||
async fn test_sync_token_saving(&self);
|
||||
/// Test UtdHookManagerData saving.
|
||||
async fn test_utd_hook_manager_data_saving(&self);
|
||||
/// Test stripped room member saving.
|
||||
async fn test_stripped_member_saving(&self);
|
||||
/// Test room power levels saving.
|
||||
@@ -74,6 +79,8 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_stripped_non_stripped(&self) -> Result<()>;
|
||||
/// Test room removal.
|
||||
async fn test_room_removal(&self) -> Result<()>;
|
||||
/// Test profile removal.
|
||||
async fn test_profile_removal(&self) -> Result<()>;
|
||||
/// Test presence saving.
|
||||
async fn test_presence_saving(&self);
|
||||
/// Test display names saving.
|
||||
@@ -535,6 +542,30 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::Filter(filter_name)).await, Ok(None));
|
||||
}
|
||||
|
||||
async fn test_user_avatar_url_saving(&self) {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let url = "https://example.org";
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::UserAvatarUrl(user_id),
|
||||
StateStoreDataValue::UserAvatarUrl(url.to_owned()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(
|
||||
Ok(Some(StateStoreDataValue::UserAvatarUrl(stored_url))) =
|
||||
self.get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await
|
||||
);
|
||||
assert_eq!(stored_url, url);
|
||||
|
||||
self.remove_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await.unwrap();
|
||||
assert_matches!(
|
||||
self.get_kv_data(StateStoreDataKey::UserAvatarUrl(user_id)).await,
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_sync_token_saving(&self) {
|
||||
let sync_token_1 = "t392-516_47314_0_7_1";
|
||||
let sync_token_2 = "t392-516_47314_0_7_2";
|
||||
@@ -566,6 +597,37 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::SyncToken).await, Ok(None));
|
||||
}
|
||||
|
||||
async fn test_utd_hook_manager_data_saving(&self) {
|
||||
// Before any data is written, the getter should return None.
|
||||
assert!(
|
||||
self.get_kv_data(StateStoreDataKey::UtdHookManagerData)
|
||||
.await
|
||||
.expect("Could not read data")
|
||||
.is_none(),
|
||||
"Store was not empty at start"
|
||||
);
|
||||
|
||||
// Put some data in the store...
|
||||
let data = GrowableBloomBuilder::new().build();
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::UtdHookManagerData,
|
||||
StateStoreDataValue::UtdHookManagerData(data.clone()),
|
||||
)
|
||||
.await
|
||||
.expect("Could not save data");
|
||||
|
||||
// ... and check it comes back.
|
||||
let read_data = self
|
||||
.get_kv_data(StateStoreDataKey::UtdHookManagerData)
|
||||
.await
|
||||
.expect("Could not read data")
|
||||
.expect("no data found")
|
||||
.into_utd_hook_manager_data()
|
||||
.expect("not UtdHookManagerData");
|
||||
|
||||
assert_eq!(read_data, data);
|
||||
}
|
||||
|
||||
async fn test_stripped_member_saving(&self) {
|
||||
let room_id = room_id!("!test_stripped_member_saving:localhost");
|
||||
let user_id = user_id();
|
||||
@@ -1058,6 +1120,77 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_profile_removal(&self) -> Result<()> {
|
||||
let room_id = room_id();
|
||||
|
||||
// Both the user id and invited user id get a profile in populate().
|
||||
let user_id = user_id();
|
||||
let invited_user_id = invited_user_id();
|
||||
|
||||
self.populate().await?;
|
||||
|
||||
let new_invite_member_json = json!({
|
||||
"content": {
|
||||
"avatar_url": "mxc://localhost/SEsfnsuifSDFSSEG",
|
||||
"displayname": "example after update",
|
||||
"membership": "invite",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSm:localhost",
|
||||
"origin_server_ts": 1432735824,
|
||||
"room_id": room_id,
|
||||
"sender": user_id,
|
||||
"state_key": invited_user_id,
|
||||
"type": "m.room.member",
|
||||
});
|
||||
let new_invite_member_event: SyncRoomMemberEvent =
|
||||
serde_json::from_value(new_invite_member_json.clone()).unwrap();
|
||||
|
||||
let mut changes = StateChanges {
|
||||
// Both get their profiles deleted…
|
||||
profiles_to_delete: [(
|
||||
room_id.to_owned(),
|
||||
vec![user_id.to_owned(), invited_user_id.to_owned()],
|
||||
)]
|
||||
.into(),
|
||||
|
||||
// …but the invited user get a new profile.
|
||||
profiles: {
|
||||
let mut map = BTreeMap::default();
|
||||
map.insert(
|
||||
room_id.to_owned(),
|
||||
[(invited_user_id.to_owned(), new_invite_member_event.into())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
map
|
||||
},
|
||||
|
||||
..StateChanges::default()
|
||||
};
|
||||
|
||||
let raw = serde_json::from_value::<Raw<AnySyncStateEvent>>(new_invite_member_json)
|
||||
.expect("can create sync-state-event for topic");
|
||||
let event = raw.deserialize().unwrap();
|
||||
changes.add_state_event(room_id, event, raw);
|
||||
|
||||
self.save_changes(&changes).await.unwrap();
|
||||
|
||||
// The profile for user has been removed.
|
||||
assert!(self.get_profile(room_id, user_id).await?.is_none());
|
||||
assert!(self.get_member_event(room_id, user_id).await?.is_some());
|
||||
|
||||
// The profile for the invited user has been updated.
|
||||
let invited_member_event = self.get_profile(room_id, invited_user_id).await?.unwrap();
|
||||
assert_eq!(
|
||||
invited_member_event.as_original().unwrap().content.displayname.as_deref(),
|
||||
Some("example after update")
|
||||
);
|
||||
assert!(self.get_member_event(room_id, invited_user_id).await?.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_presence_saving(&self) {
|
||||
let user_id = user_id();
|
||||
let second_user_id = user_id!("@second:localhost");
|
||||
@@ -1240,12 +1373,24 @@ macro_rules! statestore_integration_tests {
|
||||
store.test_filter_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_user_avatar_url_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_user_avatar_url_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_sync_token_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_sync_token_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_utd_hook_manager_data_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_utd_hook_manager_data_saving().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_stripped_member_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
@@ -1288,6 +1433,12 @@ macro_rules! statestore_integration_tests {
|
||||
store.test_room_removal().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_profile_removal() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_profile_removal().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_presence_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
num::NonZeroUsize,
|
||||
sync::RwLock as StdRwLock,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloom;
|
||||
use matrix_sdk_common::{instant::Instant, ring_buffer::RingBuffer};
|
||||
use ruma::{
|
||||
canonical_json::{redact, RedactedBecause},
|
||||
@@ -34,7 +36,7 @@ use ruma::{
|
||||
};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use super::{Result, RoomInfo, StateChanges, StateStore, StoreError};
|
||||
use super::{traits::ComposerDraft, Result, RoomInfo, StateChanges, StateStore, StoreError};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedState,
|
||||
media::{MediaRequest, UniqueKey as _},
|
||||
@@ -45,11 +47,14 @@ use crate::{
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
recently_visited_rooms: StdRwLock<HashMap<String, Vec<String>>>,
|
||||
composer_drafts: StdRwLock<HashMap<OwnedRoomId, ComposerDraft>>,
|
||||
user_avatar_url: StdRwLock<HashMap<String, String>>,
|
||||
sync_token: StdRwLock<Option<String>>,
|
||||
filters: StdRwLock<HashMap<String, String>>,
|
||||
utd_hook_manager_data: StdRwLock<Option<GrowableBloom>>,
|
||||
account_data: StdRwLock<HashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>>,
|
||||
profiles: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MinimalRoomMemberEvent>>>,
|
||||
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<String, BTreeSet<OwnedUserId>>>>,
|
||||
@@ -82,10 +87,40 @@ pub struct MemoryStore {
|
||||
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
recently_visited_rooms: Default::default(),
|
||||
composer_drafts: Default::default(),
|
||||
user_avatar_url: Default::default(),
|
||||
sync_token: Default::default(),
|
||||
filters: Default::default(),
|
||||
utd_hook_manager_data: Default::default(),
|
||||
account_data: Default::default(),
|
||||
profiles: Default::default(),
|
||||
display_names: Default::default(),
|
||||
members: Default::default(),
|
||||
room_info: Default::default(),
|
||||
room_state: Default::default(),
|
||||
room_account_data: Default::default(),
|
||||
stripped_room_state: Default::default(),
|
||||
stripped_members: Default::default(),
|
||||
presence: Default::default(),
|
||||
room_user_receipts: Default::default(),
|
||||
room_event_receipts: Default::default(),
|
||||
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
|
||||
custom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
/// Create a new empty MemoryStore
|
||||
pub fn new() -> Self {
|
||||
Self { media: StdRwLock::new(RingBuffer::new(20)), ..Default::default() }
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn get_user_room_receipt_event_impl(
|
||||
@@ -149,6 +184,26 @@ impl StateStore for MemoryStore {
|
||||
.get(user_id.as_str())
|
||||
.cloned()
|
||||
.map(StateStoreDataValue::UserAvatarUrl),
|
||||
StateStoreDataKey::RecentlyVisitedRooms(user_id) => self
|
||||
.recently_visited_rooms
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(user_id.as_str())
|
||||
.cloned()
|
||||
.map(StateStoreDataValue::RecentlyVisitedRooms),
|
||||
StateStoreDataKey::UtdHookManagerData => self
|
||||
.utd_hook_manager_data
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.map(StateStoreDataValue::UtdHookManagerData),
|
||||
StateStoreDataKey::ComposerDraft(room_id) => self
|
||||
.composer_drafts
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(room_id)
|
||||
.cloned()
|
||||
.map(StateStoreDataValue::ComposerDraft),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -169,11 +224,32 @@ impl StateStore for MemoryStore {
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::UserAvatarUrl(user_id) => {
|
||||
self.filters.write().unwrap().insert(
|
||||
self.user_avatar_url.write().unwrap().insert(
|
||||
user_id.to_string(),
|
||||
value.into_user_avatar_url().expect("Session data not a user avatar url"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::RecentlyVisitedRooms(user_id) => {
|
||||
self.recently_visited_rooms.write().unwrap().insert(
|
||||
user_id.to_string(),
|
||||
value
|
||||
.into_recently_visited_rooms()
|
||||
.expect("Session data not a list of recently visited rooms"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::UtdHookManagerData => {
|
||||
*self.utd_hook_manager_data.write().unwrap() = Some(
|
||||
value
|
||||
.into_utd_hook_manager_data()
|
||||
.expect("Session data not the hook manager data"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::ComposerDraft(room_id) => {
|
||||
self.composer_drafts.write().unwrap().insert(
|
||||
room_id.to_owned(),
|
||||
value.into_composer_draft().expect("Session data not a composer draft"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -186,7 +262,16 @@ impl StateStore for MemoryStore {
|
||||
self.filters.write().unwrap().remove(filter_name);
|
||||
}
|
||||
StateStoreDataKey::UserAvatarUrl(user_id) => {
|
||||
self.filters.write().unwrap().remove(user_id.as_str());
|
||||
self.user_avatar_url.write().unwrap().remove(user_id.as_str());
|
||||
}
|
||||
StateStoreDataKey::RecentlyVisitedRooms(user_id) => {
|
||||
self.recently_visited_rooms.write().unwrap().remove(user_id.as_str());
|
||||
}
|
||||
StateStoreDataKey::UtdHookManagerData => {
|
||||
*self.utd_hook_manager_data.write().unwrap() = None
|
||||
}
|
||||
StateStoreDataKey::ComposerDraft(room_id) => {
|
||||
self.composer_drafts.write().unwrap().remove(room_id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -199,14 +284,25 @@ impl StateStore for MemoryStore {
|
||||
*self.sync_token.write().unwrap() = Some(s.to_owned());
|
||||
}
|
||||
|
||||
for (room, users) in &changes.profiles {
|
||||
for (user_id, profile) in users {
|
||||
self.profiles
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(user_id.clone(), profile.clone());
|
||||
{
|
||||
let mut profiles = self.profiles.write().unwrap();
|
||||
|
||||
for (room, users) in &changes.profiles_to_delete {
|
||||
let Some(room_profiles) = profiles.get_mut(room) else {
|
||||
continue;
|
||||
};
|
||||
for user in users {
|
||||
room_profiles.remove(user);
|
||||
}
|
||||
}
|
||||
|
||||
for (room, users) in &changes.profiles {
|
||||
for (user_id, profile) in users {
|
||||
profiles
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(user_id.clone(), profile.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
//! Data migration helpers for StateStore implementations.
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
@@ -44,7 +47,7 @@ use crate::{
|
||||
deserialized_responses::SyncOrStrippedState,
|
||||
rooms::{
|
||||
normal::{RoomSummary, SyncInfo},
|
||||
BaseRoomInfo,
|
||||
BaseRoomInfo, RoomNotableTags,
|
||||
},
|
||||
sync::UnreadNotificationsCount,
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomInfo, RoomState,
|
||||
@@ -120,6 +123,8 @@ impl RoomInfoV1 {
|
||||
latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
|
||||
read_receipts: Default::default(),
|
||||
base_info: base_info.migrate(create),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
cached_display_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +209,8 @@ impl BaseRoomInfoV1 {
|
||||
tombstone,
|
||||
topic,
|
||||
rtc_member: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
pin::Pin,
|
||||
result::Result as StdResult,
|
||||
str::Utf8Error,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
@@ -51,13 +50,10 @@ use ruma::{
|
||||
serde::Raw,
|
||||
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// BoxStream of owned Types
|
||||
pub type BoxStream<T> = Pin<Box<dyn futures_util::Stream<Item = T> + Send>>;
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
|
||||
use crate::{
|
||||
rooms::{RoomInfo, RoomState},
|
||||
rooms::{normal::RoomInfoUpdate, RoomInfo, RoomState},
|
||||
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
@@ -70,8 +66,8 @@ pub use self::integration_tests::StateStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{
|
||||
DynStateStore, IntoStateStore, StateStore, StateStoreDataKey, StateStoreDataValue,
|
||||
StateStoreExt,
|
||||
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, StateStore,
|
||||
StateStoreDataKey, StateStoreDataValue, StateStoreExt,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -142,13 +138,11 @@ pub(crate) struct Store {
|
||||
session_meta: Arc<OnceCell<SessionMeta>>,
|
||||
/// The current sync token that should be used for the next sync call.
|
||||
pub(super) sync_token: Arc<RwLock<Option<String>>>,
|
||||
/// All rooms the store knows about.
|
||||
rooms: Arc<StdRwLock<BTreeMap<OwnedRoomId, Room>>>,
|
||||
/// A lock to synchronize access to the store, such that data by the sync is
|
||||
/// never overwritten. The sync processing is supposed to use write access,
|
||||
/// such that only it is currently accessing the store overall. Other things
|
||||
/// might acquire read access, such that access to different rooms can be
|
||||
/// parallelized.
|
||||
sync_lock: Arc<RwLock<()>>,
|
||||
/// never overwritten.
|
||||
sync_lock: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
@@ -164,7 +158,7 @@ impl Store {
|
||||
}
|
||||
|
||||
/// Get access to the syncing lock.
|
||||
pub fn sync_lock(&self) -> &RwLock<()> {
|
||||
pub fn sync_lock(&self) -> &Mutex<()> {
|
||||
&self.sync_lock
|
||||
}
|
||||
|
||||
@@ -174,9 +168,19 @@ impl Store {
|
||||
/// inner `StateStore`.
|
||||
///
|
||||
/// This method panics if it is called twice.
|
||||
pub async fn set_session_meta(&self, session_meta: SessionMeta) -> Result<()> {
|
||||
pub async fn set_session_meta(
|
||||
&self,
|
||||
session_meta: SessionMeta,
|
||||
roominfo_update_sender: &broadcast::Sender<RoomInfoUpdate>,
|
||||
) -> Result<()> {
|
||||
for info in self.inner.get_room_infos().await? {
|
||||
let room = Room::restore(&session_meta.user_id, self.inner.clone(), info);
|
||||
let room = Room::restore(
|
||||
&session_meta.user_id,
|
||||
self.inner.clone(),
|
||||
info,
|
||||
roominfo_update_sender.clone(),
|
||||
);
|
||||
|
||||
self.rooms.write().unwrap().insert(room.room_id().to_owned(), room);
|
||||
}
|
||||
|
||||
@@ -195,29 +199,34 @@ impl Store {
|
||||
}
|
||||
|
||||
/// Get all the rooms this store knows about.
|
||||
pub fn get_rooms(&self) -> Vec<Room> {
|
||||
self.rooms.read().unwrap().keys().filter_map(|id| self.get_room(id)).collect()
|
||||
pub fn rooms(&self) -> Vec<Room> {
|
||||
self.rooms.read().unwrap().values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get all the rooms this store knows about, filtered by state.
|
||||
pub fn get_rooms_filtered(&self, filter: RoomStateFilter) -> Vec<Room> {
|
||||
pub fn rooms_filtered(&self, filter: RoomStateFilter) -> Vec<Room> {
|
||||
self.rooms
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(_, r)| filter.matches(r.state()))
|
||||
.filter_map(|(id, _)| self.get_room(id))
|
||||
.filter(|(_, room)| filter.matches(room.state()))
|
||||
.map(|(_, room)| room.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the room with the given room id.
|
||||
pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
|
||||
pub fn room(&self, room_id: &RoomId) -> Option<Room> {
|
||||
self.rooms.read().unwrap().get(room_id).cloned()
|
||||
}
|
||||
|
||||
/// Lookup the Room for the given RoomId, or create one, if it didn't exist
|
||||
/// yet in the store
|
||||
pub fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomState) -> Room {
|
||||
/// yet in the store.
|
||||
pub fn get_or_create_room(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
room_type: RoomState,
|
||||
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
|
||||
) -> Room {
|
||||
let user_id =
|
||||
&self.session_meta.get().expect("Creating room while not being logged in").user_id;
|
||||
|
||||
@@ -225,7 +234,9 @@ impl Store {
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, room_type))
|
||||
.or_insert_with(|| {
|
||||
Room::new(user_id, self.inner.clone(), room_id, room_type, roominfo_update_sender)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
@@ -264,6 +275,11 @@ pub struct StateChanges {
|
||||
/// `MinimalRoomMemberEvent`.
|
||||
pub profiles: BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, MinimalRoomMemberEvent>>,
|
||||
|
||||
/// A mapping of room profiles to delete.
|
||||
///
|
||||
/// These are deleted *before* other room profiles are inserted.
|
||||
pub profiles_to_delete: BTreeMap<OwnedRoomId, Vec<OwnedUserId>>,
|
||||
|
||||
/// A mapping of `RoomId` to a map of event type string to a state key and
|
||||
/// `AnySyncStateEvent`.
|
||||
pub state:
|
||||
@@ -401,8 +417,8 @@ pub struct StoreConfig {
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for StoreConfig {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> {
|
||||
impl fmt::Debug for StoreConfig {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
|
||||
fmt.debug_struct("StoreConfig").finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use std::{
|
||||
|
||||
use as_variant::as_variant;
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloom;
|
||||
use matrix_sdk_common::AsyncTraitDeps;
|
||||
use ruma::{
|
||||
events::{
|
||||
@@ -34,6 +35,7 @@ use ruma::{
|
||||
serde::Raw,
|
||||
EventId, MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{StateChanges, StoreError};
|
||||
use crate::{
|
||||
@@ -302,7 +304,8 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
/// * `key` - The key to fetch data for
|
||||
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Put arbitrary data into the custom store
|
||||
/// Put arbitrary data into the custom store, return the data previously
|
||||
/// stored
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -315,6 +318,27 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
value: Vec<u8>,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Put arbitrary data into the custom store, do not attempt to read any
|
||||
/// previous data
|
||||
///
|
||||
/// Optimization option for set_custom_values for stores that would perform
|
||||
/// better withouts the extra read and the caller not needing that data
|
||||
/// returned. Otherwise this just wraps around `set_custom_data` and
|
||||
/// discards the result.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to insert data into
|
||||
///
|
||||
/// * `value` - The value to insert
|
||||
async fn set_custom_value_no_read(
|
||||
&self,
|
||||
key: &[u8],
|
||||
value: Vec<u8>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.set_custom_value(key, value).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Remove arbitrary data from the custom store and return it if existed
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -783,6 +807,50 @@ pub enum StateStoreDataValue {
|
||||
|
||||
/// The user avatar url
|
||||
UserAvatarUrl(String),
|
||||
|
||||
/// A list of recently visited room identifiers for the current user
|
||||
RecentlyVisitedRooms(Vec<String>),
|
||||
|
||||
/// Persistent data for
|
||||
/// `matrix_sdk_ui::unable_to_decrypt_hook::UtdHookManager`.
|
||||
UtdHookManagerData(GrowableBloom),
|
||||
|
||||
/// A composer draft for the room.
|
||||
/// To learn more, see [`ComposerDraft`].
|
||||
///
|
||||
/// [`ComposerDraft`]: Self::ComposerDraft
|
||||
ComposerDraft(ComposerDraft),
|
||||
}
|
||||
|
||||
/// Current draft of the composer for the room.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
|
||||
pub struct ComposerDraft {
|
||||
/// The draft content in plain text.
|
||||
pub plain_text: String,
|
||||
/// If the message is formatted in HTML, the HTML representation of the
|
||||
/// message.
|
||||
pub html_text: Option<String>,
|
||||
/// The type of draft.
|
||||
pub draft_type: ComposerDraftType,
|
||||
}
|
||||
|
||||
/// The type of draft of the composer.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
|
||||
pub enum ComposerDraftType {
|
||||
/// The draft is a new message.
|
||||
NewMessage,
|
||||
/// The draft is a reply to an event.
|
||||
Reply {
|
||||
/// The ID of the event being replied to.
|
||||
event_id: String,
|
||||
},
|
||||
/// The draft is an edit of an event.
|
||||
Edit {
|
||||
/// The ID of the event being edited.
|
||||
event_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl StateStoreDataValue {
|
||||
@@ -800,6 +868,21 @@ impl StateStoreDataValue {
|
||||
pub fn into_user_avatar_url(self) -> Option<String> {
|
||||
as_variant!(self, Self::UserAvatarUrl)
|
||||
}
|
||||
|
||||
/// Get this value if it is a list of recently visited rooms.
|
||||
pub fn into_recently_visited_rooms(self) -> Option<Vec<String>> {
|
||||
as_variant!(self, Self::RecentlyVisitedRooms)
|
||||
}
|
||||
|
||||
/// Get this value if it is the data for the `UtdHookManager`.
|
||||
pub fn into_utd_hook_manager_data(self) -> Option<GrowableBloom> {
|
||||
as_variant!(self, Self::UtdHookManagerData)
|
||||
}
|
||||
|
||||
/// Get this value if it is a composer draft.
|
||||
pub fn into_composer_draft(self) -> Option<ComposerDraft> {
|
||||
as_variant!(self, Self::ComposerDraft)
|
||||
}
|
||||
}
|
||||
|
||||
/// A key for key-value data.
|
||||
@@ -813,6 +896,19 @@ pub enum StateStoreDataKey<'a> {
|
||||
|
||||
/// Avatar URL
|
||||
UserAvatarUrl(&'a UserId),
|
||||
|
||||
/// Recently visited room identifiers
|
||||
RecentlyVisitedRooms(&'a UserId),
|
||||
|
||||
/// Persistent data for
|
||||
/// `matrix_sdk_ui::unable_to_decrypt_hook::UtdHookManager`.
|
||||
UtdHookManagerData,
|
||||
|
||||
/// A composer draft for the room.
|
||||
/// To learn more, see [`ComposerDraft`].
|
||||
///
|
||||
/// [`ComposerDraft`]: Self::ComposerDraft
|
||||
ComposerDraft(&'a RoomId),
|
||||
}
|
||||
|
||||
impl StateStoreDataKey<'_> {
|
||||
@@ -823,4 +919,16 @@ impl StateStoreDataKey<'_> {
|
||||
/// Key prefix to use for the [`UserAvatarUrl`][Self::UserAvatarUrl]
|
||||
/// variant.
|
||||
pub const USER_AVATAR_URL: &'static str = "user_avatar_url";
|
||||
|
||||
/// Key prefix to use for the
|
||||
/// [`RecentlyVisitedRooms`][Self::RecentlyVisitedRooms] variant.
|
||||
pub const RECENTLY_VISITED_ROOMS: &'static str = "recently_visited_rooms";
|
||||
|
||||
/// Key to use for the [`UtdHookManagerData`][Self::UtdHookManagerData]
|
||||
/// variant.
|
||||
pub const UTD_HOOK_MANAGER_DATA: &'static str = "utd_hook_manager_data";
|
||||
|
||||
/// Key prefix to use for the [`ComposerDraft`][Self::ComposerDraft]
|
||||
/// variant.
|
||||
pub const COMPOSER_DRAFT: &'static str = "composer_draft";
|
||||
}
|
||||
|
||||
@@ -16,46 +16,42 @@
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
push::get_notifications::v3::Notification,
|
||||
sync::sync_events::{
|
||||
v3::InvitedRoom, UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
api::client::sync::sync_events::{
|
||||
v3::InvitedRoom as InvitedRoomUpdate,
|
||||
UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
events::{
|
||||
presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
|
||||
AnySyncEphemeralRoomEvent, AnySyncStateEvent, AnyToDeviceEvent,
|
||||
},
|
||||
push::Action,
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
OwnedEventId, OwnedRoomId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
debug::{
|
||||
DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId, DebugNotificationMap,
|
||||
},
|
||||
deserialized_responses::AmbiguityChanges,
|
||||
debug::{DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
|
||||
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
|
||||
store::Store,
|
||||
};
|
||||
|
||||
/// Internal representation of a `/sync` response.
|
||||
/// Generalized representation of a `/sync` response.
|
||||
///
|
||||
/// This type is intended to be applicable regardless of the endpoint used for
|
||||
/// syncing.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SyncResponse {
|
||||
/// Updates to rooms.
|
||||
pub rooms: Rooms,
|
||||
pub rooms: RoomUpdates,
|
||||
/// Updates to the presence status of other users.
|
||||
pub presence: Vec<Raw<PresenceEvent>>,
|
||||
/// The global private data created by this user.
|
||||
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
|
||||
/// Messages sent directly between devices.
|
||||
pub to_device: Vec<Raw<AnyToDeviceEvent>>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
/// New notifications per room.
|
||||
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
}
|
||||
@@ -67,37 +63,53 @@ impl fmt::Debug for SyncResponse {
|
||||
.field("rooms", &self.rooms)
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
.field("to_device", &DebugListOfRawEventsNoId(&self.to_device))
|
||||
.field("ambiguity_changes", &self.ambiguity_changes)
|
||||
.field("notifications", &DebugNotificationMap(&self.notifications))
|
||||
.field("notifications", &self.notifications)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates to rooms in a [`SyncResponse`].
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Rooms {
|
||||
pub struct RoomUpdates {
|
||||
/// The rooms that the user has left or been banned from.
|
||||
pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
|
||||
pub leave: BTreeMap<OwnedRoomId, LeftRoomUpdate>,
|
||||
/// The rooms that the user has joined.
|
||||
pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
|
||||
pub join: BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
|
||||
/// The rooms that the user has been invited to.
|
||||
pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
|
||||
pub invite: BTreeMap<OwnedRoomId, InvitedRoomUpdate>,
|
||||
}
|
||||
|
||||
impl RoomUpdates {
|
||||
/// Update the caches for the rooms that received updates.
|
||||
///
|
||||
/// This will only fill the in-memory caches, not save the info on disk.
|
||||
pub(crate) async fn update_in_memory_caches(&self, store: &Store) {
|
||||
for room in self
|
||||
.leave
|
||||
.keys()
|
||||
.chain(self.join.keys())
|
||||
.chain(self.invite.keys())
|
||||
.filter_map(|room_id| store.room(room_id))
|
||||
{
|
||||
let _ = room.compute_display_name().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for Rooms {
|
||||
impl fmt::Debug for RoomUpdates {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Rooms")
|
||||
.field("leave", &self.leave)
|
||||
.field("join", &self.join)
|
||||
.field("invite", &DebugInvitedRooms(&self.invite))
|
||||
.field("invite", &DebugInvitedRoomUpdates(&self.invite))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates to joined rooms.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct JoinedRoom {
|
||||
pub struct JoinedRoomUpdate {
|
||||
/// Counts of unread notifications for this room.
|
||||
pub unread_notifications: UnreadNotificationsCount,
|
||||
/// The timeline of messages and state changes in the room.
|
||||
@@ -112,10 +124,15 @@ pub struct JoinedRoom {
|
||||
/// The ephemeral events in the room that aren't recorded in the timeline or
|
||||
/// state of the room. e.g. typing.
|
||||
pub ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
///
|
||||
/// This is a map of event ID of the `m.room.member` event to the
|
||||
/// details of the ambiguity change.
|
||||
pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for JoinedRoom {
|
||||
impl fmt::Debug for JoinedRoomUpdate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
.field("unread_notifications", &self.unread_notifications)
|
||||
@@ -123,19 +140,21 @@ impl fmt::Debug for JoinedRoom {
|
||||
.field("state", &DebugListOfRawEvents(&self.state))
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
.field("ephemeral", &self.ephemeral)
|
||||
.field("ambiguity_changes", &self.ambiguity_changes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl JoinedRoom {
|
||||
impl JoinedRoomUpdate {
|
||||
pub(crate) fn new(
|
||||
timeline: Timeline,
|
||||
state: Vec<Raw<AnySyncStateEvent>>,
|
||||
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
|
||||
ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
|
||||
unread_notifications: UnreadNotificationsCount,
|
||||
ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
) -> Self {
|
||||
Self { unread_notifications, timeline, state, account_data, ephemeral }
|
||||
Self { unread_notifications, timeline, state, account_data, ephemeral, ambiguity_changes }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +178,8 @@ impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
|
||||
}
|
||||
|
||||
/// Updates to left rooms.
|
||||
#[derive(Clone)]
|
||||
pub struct LeftRoom {
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LeftRoomUpdate {
|
||||
/// The timeline of messages and state changes in the room up to the point
|
||||
/// when the user left.
|
||||
pub timeline: Timeline,
|
||||
@@ -171,25 +190,32 @@ pub struct LeftRoom {
|
||||
pub state: Vec<Raw<AnySyncStateEvent>>,
|
||||
/// The private data that this user has attached to this room.
|
||||
pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
///
|
||||
/// This is a map of event ID of the `m.room.member` event to the
|
||||
/// details of the ambiguity change.
|
||||
pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
}
|
||||
|
||||
impl LeftRoom {
|
||||
impl LeftRoomUpdate {
|
||||
pub(crate) fn new(
|
||||
timeline: Timeline,
|
||||
state: Vec<Raw<AnySyncStateEvent>>,
|
||||
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
|
||||
ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
) -> Self {
|
||||
Self { timeline, state, account_data }
|
||||
Self { timeline, state, account_data, ambiguity_changes }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for LeftRoom {
|
||||
impl fmt::Debug for LeftRoomUpdate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
.field("timeline", &self.timeline)
|
||||
.field("state", &DebugListOfRawEvents(&self.state))
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
.field("ambiguity_changes", &self.ambiguity_changes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -215,11 +241,36 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugInvitedRooms<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoom>);
|
||||
struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRooms<'a> {
|
||||
impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A notification triggered by a sync response.
|
||||
#[derive(Clone)]
|
||||
pub struct Notification {
|
||||
/// The actions to perform when the conditions for this rule are met.
|
||||
pub actions: Vec<Action>,
|
||||
|
||||
/// The event that triggered the notification.
|
||||
pub event: RawAnySyncOrStrippedTimelineEvent,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for Notification {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let event_debug = match &self.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(ev) => DebugRawEvent(ev),
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => DebugRawEvent(ev.cast_ref()),
|
||||
};
|
||||
|
||||
f.debug_struct("Notification")
|
||||
.field("actions", &self.actions)
|
||||
.field("event", &event_debug)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2024 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.
|
||||
|
||||
//! Testing utilities - DO NOT USE IN PRODUCTION.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use ruma::{owned_user_id, UserId};
|
||||
|
||||
use crate::{BaseClient, SessionMeta};
|
||||
|
||||
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
|
||||
/// one otherwise.
|
||||
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
|
||||
let client = BaseClient::new();
|
||||
let user_id =
|
||||
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
|
||||
client
|
||||
.set_session_meta(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("set_session_meta failed!");
|
||||
client
|
||||
}
|
||||
@@ -45,3 +45,6 @@ wasm-bindgen-test = "0.3.33"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
js-sys = "0.3.64"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::{env, process};
|
||||
|
||||
fn main() {
|
||||
// Prevent unnecessary rerunning of this build script
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
let is_wasm = env::var_os("CARGO_CFG_TARGET_ARCH").is_some_and(|arch| arch == "wasm32");
|
||||
if is_wasm && env::var_os("CARGO_FEATURE_JS").is_none() {
|
||||
eprintln!(
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
//! Abstraction over an executor so we can spawn tasks under WASM the same way
|
||||
//! we do usually.
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use std::convert::Infallible as JoinError;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use std::{
|
||||
future::Future,
|
||||
@@ -25,7 +23,12 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use futures_util::{future::RemoteHandle, FutureExt};
|
||||
pub use futures_util::future::Aborted as JoinError;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use futures_util::{
|
||||
future::{AbortHandle, Abortable, RemoteHandle},
|
||||
FutureExt,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::task::{spawn, JoinError, JoinHandle};
|
||||
|
||||
@@ -34,16 +37,31 @@ pub fn spawn<F, T>(future: F) -> JoinHandle<T>
|
||||
where
|
||||
F: Future<Output = T> + 'static,
|
||||
{
|
||||
let (fut, handle) = future.remote_handle();
|
||||
wasm_bindgen_futures::spawn_local(fut);
|
||||
let (future, remote_handle) = future.remote_handle();
|
||||
let (abort_handle, abort_registration) = AbortHandle::new_pair();
|
||||
let future = Abortable::new(future, abort_registration);
|
||||
|
||||
JoinHandle { handle }
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
// Poll the future, and ignore the result (either it's `Ok(())`, or it's
|
||||
// `Err(Aborted)`).
|
||||
let _ = future.await;
|
||||
});
|
||||
|
||||
JoinHandle { remote_handle, abort_handle }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug)]
|
||||
pub struct JoinHandle<T> {
|
||||
handle: RemoteHandle<T>,
|
||||
remote_handle: RemoteHandle<T>,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T> JoinHandle<T> {
|
||||
pub fn abort(&self) {
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -51,6 +69,37 @@ impl<T: 'static> Future for JoinHandle<T> {
|
||||
type Output = Result<T, JoinError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.handle).poll(cx).map(Ok)
|
||||
if self.abort_handle.is_aborted() {
|
||||
// The future has been aborted. It is not possible to poll it again.
|
||||
Poll::Ready(Err(JoinError))
|
||||
} else {
|
||||
Pin::new(&mut self.remote_handle).poll(cx).map(Ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
use super::spawn;
|
||||
|
||||
#[async_test]
|
||||
async fn test_spawn() {
|
||||
let future = async { 42 };
|
||||
let join_handle = spawn(future);
|
||||
|
||||
assert_matches!(join_handle.await, Ok(42));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_abort() {
|
||||
let future = async { 42 };
|
||||
let join_handle = spawn(future);
|
||||
|
||||
join_handle.abort();
|
||||
|
||||
assert!(join_handle.await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@ use std::{
|
||||
vec_deque::{Drain, Iter},
|
||||
VecDeque,
|
||||
},
|
||||
num::NonZeroUsize,
|
||||
ops::RangeBounds,
|
||||
};
|
||||
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A simple fixed-size ring buffer implementation.
|
||||
///
|
||||
@@ -35,8 +36,8 @@ pub struct RingBuffer<T> {
|
||||
impl<T> RingBuffer<T> {
|
||||
/// Create a ring buffer with the supplied capacity, reserving it so we
|
||||
/// never need to reallocate.
|
||||
pub fn new(size: usize) -> Self {
|
||||
Self { inner: VecDeque::with_capacity(size) }
|
||||
pub fn new(size: NonZeroUsize) -> Self {
|
||||
Self { inner: VecDeque::with_capacity(size.into()) }
|
||||
}
|
||||
|
||||
/// Returns the number of items that are stored in this ring buffer.
|
||||
@@ -105,6 +106,14 @@ impl<T> RingBuffer<T> {
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.inner.capacity()
|
||||
}
|
||||
|
||||
/// Retains only the elements specified by the predicate.
|
||||
pub fn retain<F>(&mut self, predicate: F)
|
||||
where
|
||||
F: FnMut(&T) -> bool,
|
||||
{
|
||||
self.inner.retain(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
impl<U> Extend<U> for RingBuffer<U> {
|
||||
@@ -115,21 +124,15 @@ impl<U> Extend<U> for RingBuffer<U> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for RingBuffer<T> {
|
||||
fn default() -> Self {
|
||||
Self { inner: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
use std::{num::NonZeroUsize, ops::Not};
|
||||
|
||||
use super::RingBuffer;
|
||||
|
||||
#[test]
|
||||
pub fn test_fixed_size() {
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
|
||||
assert!(ring_buffer.is_empty());
|
||||
|
||||
@@ -163,7 +166,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
pub fn test_push_and_pop_and_remove_and_length() {
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
|
||||
ring_buffer.push(1);
|
||||
assert_eq!(ring_buffer.len(), 1);
|
||||
@@ -220,7 +223,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_drain() {
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
|
||||
ring_buffer.push(1);
|
||||
ring_buffer.push(2);
|
||||
@@ -241,7 +244,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_on_empty_buffer_is_a_noop() {
|
||||
let mut ring_buffer: RingBuffer<u8> = RingBuffer::new(3);
|
||||
let mut ring_buffer: RingBuffer<u8> = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.clear();
|
||||
assert_eq!(ring_buffer.len(), 0);
|
||||
}
|
||||
@@ -249,7 +252,7 @@ mod tests {
|
||||
#[test]
|
||||
fn clear_removes_all_items() {
|
||||
// Given a RingBuffer that has been used
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push(4);
|
||||
ring_buffer.push(5);
|
||||
ring_buffer.push(6);
|
||||
@@ -269,7 +272,7 @@ mod tests {
|
||||
#[test]
|
||||
fn clear_does_not_affect_capacity() {
|
||||
// Given a RingBuffer that has been used
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push(4);
|
||||
ring_buffer.push(5);
|
||||
ring_buffer.push(6);
|
||||
@@ -287,7 +290,7 @@ mod tests {
|
||||
#[test]
|
||||
fn capacity_is_what_we_passed_to_new() {
|
||||
// Given a RingBuffer
|
||||
let ring_buffer = RingBuffer::<i32>::new(13);
|
||||
let ring_buffer = RingBuffer::<i32>::new(NonZeroUsize::new(13).unwrap());
|
||||
// When I ask for its capacity I get what I provided at the start
|
||||
assert_eq!(ring_buffer.capacity(), 13);
|
||||
}
|
||||
@@ -295,7 +298,7 @@ mod tests {
|
||||
#[test]
|
||||
fn capacity_is_not_affected_by_overflowing() {
|
||||
// Given a RingBuffer that has been used
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push(4);
|
||||
ring_buffer.push(5);
|
||||
ring_buffer.push(6);
|
||||
@@ -317,7 +320,7 @@ mod tests {
|
||||
#[test]
|
||||
fn roundtrip_serialization() {
|
||||
// Given a RingBuffer
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -337,7 +340,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extending_an_empty_ringbuffer_adds_the_items() {
|
||||
// Given a RingBuffer
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
|
||||
// When I extend it
|
||||
ring_buffer.extend(vec!["a".to_owned(), "b".to_owned()]);
|
||||
@@ -349,7 +352,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extend_adds_items_to_the_end() {
|
||||
// Given a RingBuffer with something in it
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -366,7 +369,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extend_does_not_overflow_max_length() {
|
||||
// Given a RingBuffer with something in it
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -389,7 +392,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extending_a_full_ringbuffer_preserves_max_length() {
|
||||
// Given a full RingBuffer with something in it
|
||||
let mut ring_buffer = RingBuffer::new(2);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(2).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -405,4 +408,16 @@ mod tests {
|
||||
// Then only the last N items remain
|
||||
assert_eq!(ring_buffer.iter().map(String::as_str).collect::<Vec<_>>(), vec!["6", "7"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retain() {
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(2).unwrap());
|
||||
ring_buffer.push(1);
|
||||
ring_buffer.push(2);
|
||||
|
||||
ring_buffer.retain(|v| v % 2 == 0);
|
||||
|
||||
assert_eq!(ring_buffer.len(), 1);
|
||||
assert_eq!(ring_buffer.get(0).copied().unwrap(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ use std::{
|
||||
};
|
||||
|
||||
use tokio::{sync::Mutex, time::sleep};
|
||||
use tracing::instrument;
|
||||
use tracing::{debug, error, info, instrument, trace};
|
||||
|
||||
use crate::{
|
||||
executor::{spawn, JoinHandle},
|
||||
@@ -180,7 +180,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
|
||||
// decrement `num_holders`. That's fine because that means the lock
|
||||
// was taken by at least one thread, and after this call it will be
|
||||
// taken by at least one thread.
|
||||
tracing::trace!("We already had the lock, incrementing holder count");
|
||||
trace!("We already had the lock, incrementing holder count");
|
||||
self.num_holders.fetch_add(1, atomic::Ordering::SeqCst);
|
||||
let guard = CrossProcessStoreLockGuard { num_holders: self.num_holders.clone() };
|
||||
return Ok(Some(guard));
|
||||
@@ -193,11 +193,11 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
|
||||
.map_err(|err| LockStoreError::BackingStoreError(Box::new(err)))?;
|
||||
|
||||
if !acquired {
|
||||
tracing::trace!("Couldn't acquire the lock immediately.");
|
||||
trace!("Couldn't acquire the lock immediately.");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
tracing::trace!("Acquired the lock, spawning the lease extension task.");
|
||||
trace!("Acquired the lock, spawning the lease extension task.");
|
||||
|
||||
// This is the first time we've acquired the lock. We're going to spawn the task
|
||||
// that will renew the lease.
|
||||
@@ -214,7 +214,10 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
|
||||
|
||||
if let Some(_prev) = renew_task.take() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
_prev.abort();
|
||||
if !_prev.is_finished() {
|
||||
info!("aborting the previous renew task");
|
||||
_prev.abort();
|
||||
}
|
||||
}
|
||||
|
||||
// Restart a new one.
|
||||
@@ -234,7 +237,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
|
||||
|
||||
// If there are no more users, we can quit.
|
||||
if this.num_holders.load(atomic::Ordering::SeqCst) == 0 {
|
||||
tracing::info!("exiting the lease extension loop");
|
||||
info!("exiting the lease extension loop");
|
||||
|
||||
// Cancel the lease with another 0ms lease.
|
||||
// If we don't get the lock, that's (weird but) fine.
|
||||
@@ -250,7 +253,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
|
||||
|
||||
let fut = this.store.try_lock(LEASE_DURATION_MS, &this.lock_key, &this.lock_holder);
|
||||
if let Err(err) = fut.await {
|
||||
tracing::error!("error when extending lock lease: {err:#}");
|
||||
error!("error when extending lock lease: {err:#}");
|
||||
// Exit the loop.
|
||||
break;
|
||||
}
|
||||
@@ -307,7 +310,7 @@ impl<S: BackingStore + Clone + SendOutsideWasm + 'static> CrossProcessStoreLock<
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("Waiting {wait} before re-attempting to take the lock");
|
||||
debug!("Waiting {wait} before re-attempting to take the lock");
|
||||
sleep(Duration::from_millis(wait.into())).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,115 @@
|
||||
# UNRELEASED
|
||||
|
||||
Changes:
|
||||
|
||||
- Sign the device keys with the user-identity (i.e. cross-signing keys) if
|
||||
we're uploading the device keys and if the cross-signing keys are available.
|
||||
This approach eliminates the need to upload signatures in a separate request,
|
||||
ensuring that other users/devices will never encounter this device without a
|
||||
signature from their user identity. Consequently, they will never see the
|
||||
device as unverified.
|
||||
([#3453](https://github.com/matrix-org/matrix-rust-sdk/pull/3453))
|
||||
|
||||
- Avoid emitting entries from `identities_stream_raw` and `devices_stream` when
|
||||
we receive a `/keys/query` response which shows that no devices changed.
|
||||
([#3442](https://github.com/matrix-org/matrix-rust-sdk/pull/3442))
|
||||
|
||||
- Fallback keys are rotated in a time-based manner, instead of waiting for the
|
||||
server to tell us that a fallback key got used.
|
||||
([#3151](https://github.com/matrix-org/matrix-rust-sdk/pull/3151))
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- Add a `custom_account` argument to the `OlmMachine::with_store()` method, this
|
||||
allows users to learn their identity keys before they get access to the user
|
||||
and device ID.
|
||||
([#3451](https://github.com/matrix-org/matrix-rust-sdk/pull/3451))
|
||||
|
||||
- Add a `backup_version` argument to `CryptoStore`'s
|
||||
`inbound_group_sessions_for_backup`,
|
||||
`mark_inbound_group_sessions_as_backed_up` and
|
||||
`inbound_group_session_counts` methods.
|
||||
([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253))
|
||||
|
||||
- Rename the `OlmMachine::invalidate_group_session` method to
|
||||
`OlmMachine::discard_room_key`
|
||||
|
||||
- Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`.
|
||||
(Call it with `olm_machine.store().export_room_keys(...)`.)
|
||||
|
||||
- Add new `dehydrated` property to `olm::account::PickledAccount`.
|
||||
([#3164](https://github.com/matrix-org/matrix-rust-sdk/pull/3164))
|
||||
|
||||
- Remove deprecated `OlmMachine::import_room_keys`.
|
||||
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
|
||||
|
||||
- Add the `SasState::Created` variant to differentiate the state between the
|
||||
party that sent the verification start and the party that received it.
|
||||
|
||||
Deprecations:
|
||||
|
||||
- Deprecate `BackupMachine::import_backed_up_room_keys`.
|
||||
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
|
||||
|
||||
Additions:
|
||||
|
||||
|
||||
- Expose new method `OlmMachine::clear_crypto_cache()`, with FFI bindings
|
||||
([#3462](https://github.com/matrix-org/matrix-rust-sdk/pull/3462))
|
||||
|
||||
- Expose new method `OlmMachine::upload_device_keys()`.
|
||||
([#3457](https://github.com/matrix-org/matrix-rust-sdk/pull/3457))
|
||||
|
||||
- Expose new method `CryptoStore::import_room_keys`.
|
||||
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
|
||||
|
||||
- Expose new method `BackupMachine::backup_version`.
|
||||
([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320))
|
||||
|
||||
- Add data types to parse the QR code data for the QR code login defined in
|
||||
[MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108)
|
||||
|
||||
- Expose new method `CryptoStore::clear_caches`.
|
||||
([#3338](https://github.com/matrix-org/matrix-rust-sdk/pull/3338))
|
||||
|
||||
- Expose new method `OlmMachine::device_creation_time`.
|
||||
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
|
||||
|
||||
- Log more details about the Olm session after encryption and decryption.
|
||||
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
|
||||
|
||||
- When Olm message decryption fails, report the error code(s) from the failure.
|
||||
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
|
||||
|
||||
- Expose new methods `OlmMachine::set_room_settings` and
|
||||
`OlmMachine::get_room_settings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Add new properties `session_rotation_period` and
|
||||
`session_rotation_period_msgs` to `store::RoomSettings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
|
||||
keys whose metadata lacked check fields.
|
||||
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
|
||||
|
||||
- Add new API `Device::encrypt_event_raw` that allows
|
||||
to encrypt an event to a specific device.
|
||||
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
|
||||
|
||||
- Add new API `store::Store::export_room_keys_stream` that provides room
|
||||
keys on demand.
|
||||
|
||||
- Include event timestamps on logs from event decryption.
|
||||
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
|
||||
|
||||
|
||||
## 0.7.1
|
||||
|
||||
Security fixes:
|
||||
|
||||
- Don't log the private part of the backup key, introduced in [#71136e4](https://github.com/matrix-org/matrix-rust-sdk/commit/71136e44c03c79f80d6d1a2446673bc4d53a2067).
|
||||
|
||||
# 0.7.0
|
||||
|
||||
- Add method to mark a list of inbound group sessions as backed up:
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
@@ -17,10 +17,12 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
[features]
|
||||
default = []
|
||||
automatic-room-key-forwarding = []
|
||||
js = ["ruma/js", "vodozemac/js"]
|
||||
js = ["ruma/js", "vodozemac/js", "matrix-sdk-common/js"]
|
||||
qrcode = ["dep:matrix-sdk-qrcode"]
|
||||
message-ids = ["dep:ulid"]
|
||||
experimental-algorithms = []
|
||||
uniffi = ["dep:uniffi"]
|
||||
_disable-minimum-rotation-period-ms = []
|
||||
|
||||
# Testing helpers for implementations based upon this
|
||||
testing = ["dep:http"]
|
||||
@@ -41,6 +43,7 @@ hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true, optional = true } # feature = testing only
|
||||
itertools = { workspace = true }
|
||||
js_option = "0.1.1"
|
||||
matrix-sdk-qrcode = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
@@ -51,11 +54,14 @@ serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = "2.5.0"
|
||||
time = { version = "0.3.34", features = ["formatting"] }
|
||||
tokio-stream = { workspace = true, features = ["sync"] }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true, features = ["attributes"] }
|
||||
url = { workspace = true }
|
||||
ulid = { version = "1.0.0", optional = true }
|
||||
uniffi = { workspace = true, optional = true }
|
||||
vodozemac = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
@@ -75,6 +81,10 @@ indoc = "2.0.1"
|
||||
matrix-sdk-test = { workspace = true }
|
||||
olm-rs = { version = "2.2.0", features = ["serde"] }
|
||||
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
|
||||
similar-asserts = "1.5.0"
|
||||
# required for async_test macro
|
||||
stream_assert = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -56,10 +56,14 @@ async fn main() -> Result<(), OlmError> {
|
||||
[Matrix]: https://matrix.org/
|
||||
[matrix-sdk]: https://github.com/matrix-org/matrix-rust-sdk/
|
||||
|
||||
# Room key sharing algorithm
|
||||
# Room key forwarding algorithm
|
||||
|
||||
The decision tree below visualizes the way this crate decides whether a room
|
||||
key will be shared with a requester upon a key request.
|
||||
The decision tree below visualizes the way this crate decides whether a message
|
||||
key ("room key") will be [forwarded][forwarded_room_key] to a requester upon a
|
||||
key request, provided the `automatic-room-key-forwarding` feature is enabled.
|
||||
Key forwarding is sometimes also referred to as key *gossiping*.
|
||||
|
||||
[forwarded_room_key]: <https://spec.matrix.org/v1.10/client-server-api/#mforwarded_room_key>
|
||||
|
||||

|
||||
|
||||
@@ -70,4 +74,6 @@ The following crate feature flags are available:
|
||||
|
||||
* `qrcode`: Enbles QRcode generation and reading code
|
||||
|
||||
* `testing`: provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
|
||||
* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
|
||||
|
||||
* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys.
|
||||
|
||||
@@ -106,12 +106,12 @@ impl PkDecryption {
|
||||
|
||||
let keys = Keys::new(shared_secret);
|
||||
|
||||
let cipher = Aes256CbcDec::new(keys.aes_key(), keys.iv());
|
||||
let decrypted = cipher.decrypt_padded_vec_mut::<Pkcs7>(&message.ciphertext)?;
|
||||
|
||||
let hmac = keys.hmac();
|
||||
hmac.verify_truncated_left(&message.mac)?;
|
||||
|
||||
let cipher = Aes256CbcDec::new(keys.aes_key(), keys.iv());
|
||||
let decrypted = cipher.decrypt_padded_vec_mut::<Pkcs7>(&message.ciphertext)?;
|
||||
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ use std::{
|
||||
ops::DerefMut,
|
||||
};
|
||||
|
||||
use bs58;
|
||||
use ruma::api::client::backup::EncryptedSessionData;
|
||||
use thiserror::Error;
|
||||
use vodozemac::Curve25519PublicKey;
|
||||
@@ -45,7 +44,7 @@ pub enum DecodeError {
|
||||
/// The recovery key has an invalid length.
|
||||
#[error("The decoded recovery key has a invalid length: expected {0}, got {1}")]
|
||||
Length(usize, usize),
|
||||
/// The recovry key isn't valid base58.
|
||||
/// The recovery key isn't valid base58.
|
||||
#[error(transparent)]
|
||||
Base58(#[from] bs58::decode::Error),
|
||||
/// The recovery key isn't valid base64.
|
||||
|
||||
@@ -39,8 +39,7 @@ use crate::{
|
||||
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
|
||||
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest, RoomKeyImportResult,
|
||||
SignatureError,
|
||||
CryptoStoreError, Device, KeysBackupRequest, RoomKeyImportResult, SignatureError,
|
||||
};
|
||||
|
||||
mod keys;
|
||||
@@ -70,12 +69,6 @@ struct PendingBackup {
|
||||
sessions: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
|
||||
}
|
||||
|
||||
impl From<PendingBackup> for OutgoingRequest {
|
||||
fn from(b: PendingBackup) -> Self {
|
||||
OutgoingRequest { request_id: b.request_id, request: Arc::new(b.request.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a signature verification of a signed JSON object.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SignatureVerification {
|
||||
@@ -108,6 +101,7 @@ impl SignatureVerification {
|
||||
|
||||
/// The result of a signature check.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
|
||||
pub enum SignatureState {
|
||||
/// The signature is missing.
|
||||
#[default]
|
||||
@@ -151,7 +145,7 @@ impl BackupMachine {
|
||||
}
|
||||
|
||||
/// Check if our own device has signed the given signed JSON payload.
|
||||
async fn check_own_device_signature(
|
||||
fn check_own_device_signature(
|
||||
&self,
|
||||
signatures: &Signatures,
|
||||
auth_data: &str,
|
||||
@@ -159,7 +153,7 @@ impl BackupMachine {
|
||||
match self.store.static_account().has_signed_raw(signatures, auth_data) {
|
||||
Ok(_) => SignatureState::ValidAndTrusted,
|
||||
Err(e) => match e {
|
||||
crate::SignatureError::NoSignatureFound => SignatureState::Missing,
|
||||
SignatureError::NoSignatureFound => SignatureState::Missing,
|
||||
_ => SignatureState::Invalid,
|
||||
},
|
||||
}
|
||||
@@ -185,7 +179,7 @@ impl BackupMachine {
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
crate::SignatureError::NoSignatureFound => SignatureState::Missing,
|
||||
SignatureError::NoSignatureFound => SignatureState::Missing,
|
||||
_ => SignatureState::Invalid,
|
||||
},
|
||||
}
|
||||
@@ -291,7 +285,7 @@ impl BackupMachine {
|
||||
|
||||
// Check if there's a signature from our own device.
|
||||
let device_signature =
|
||||
self.check_own_device_signature(&auth_data.signatures, &serialized_auth_data).await;
|
||||
self.check_own_device_signature(&auth_data.signatures, &serialized_auth_data);
|
||||
// Check if there's a signature from our own user identity.
|
||||
let user_identity_signature =
|
||||
self.check_own_identity_signature(&auth_data.signatures, &serialized_auth_data).await?;
|
||||
@@ -403,7 +397,8 @@ impl BackupMachine {
|
||||
|
||||
/// Get the number of backed up room keys and the total number of room keys.
|
||||
pub async fn room_key_counts(&self) -> Result<RoomKeyCounts, CryptoStoreError> {
|
||||
self.store.inbound_group_session_counts().await
|
||||
let backup_version = self.backup_key.read().await.as_ref().and_then(|k| k.backup_version());
|
||||
self.store.inbound_group_session_counts(backup_version.as_deref()).await
|
||||
}
|
||||
|
||||
/// Disable and reset our backup state.
|
||||
@@ -424,6 +419,13 @@ impl BackupMachine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provide the `backup_version` of the current `backup_key`, or None if
|
||||
/// there is no current key, or the key is not used with any backup
|
||||
/// version.
|
||||
pub async fn backup_version(&self) -> Option<String> {
|
||||
self.backup_key.read().await.as_ref().and_then(|k| k.backup_version())
|
||||
}
|
||||
|
||||
/// Store the backup decryption key in the crypto store.
|
||||
///
|
||||
/// This is useful if the client wants to support gossiping of the backup
|
||||
@@ -482,7 +484,12 @@ impl BackupMachine {
|
||||
|
||||
trace!(request_id = ?r.request_id, keys = ?r.sessions, "Marking room keys as backed up");
|
||||
|
||||
self.store.mark_inbound_group_sessions_as_backed_up(&room_and_session_ids).await?;
|
||||
self.store
|
||||
.mark_inbound_group_sessions_as_backed_up(
|
||||
&r.request.version,
|
||||
&room_and_session_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
trace!(
|
||||
request_id = ?r.request_id,
|
||||
@@ -520,7 +527,7 @@ impl BackupMachine {
|
||||
};
|
||||
|
||||
let sessions =
|
||||
self.store.inbound_group_sessions_for_backup(Self::BACKUP_BATCH_SIZE).await?;
|
||||
self.store.inbound_group_sessions_for_backup(&version, Self::BACKUP_BATCH_SIZE).await?;
|
||||
|
||||
if sessions.is_empty() {
|
||||
trace!(?backup_key, "No room keys need to be backed up");
|
||||
@@ -593,6 +600,7 @@ impl BackupMachine {
|
||||
///
|
||||
/// Returns a [`RoomKeyImportResult`] containing information about room keys
|
||||
/// which were imported.
|
||||
#[deprecated(note = "Use the OlmMachine::store::import_room_keys method instead")]
|
||||
pub async fn import_backed_up_room_keys(
|
||||
&self,
|
||||
room_keys: BTreeMap<OwnedRoomId, BTreeMap<String, BackedUpRoomKey>>,
|
||||
@@ -612,7 +620,15 @@ impl BackupMachine {
|
||||
}
|
||||
}
|
||||
|
||||
self.store.import_room_keys(decrypted_room_keys, true, progress_listener).await
|
||||
// FIXME: This method is a bit flawed: we have no real idea which backup version
|
||||
// these keys came from. For example, we might have reset the backup
|
||||
// since the keys were downloaded. For now, let's assume they came from
|
||||
// the "current" backup version.
|
||||
let backup_version = self.backup_version().await;
|
||||
|
||||
self.store
|
||||
.import_room_keys(decrypted_room_keys, backup_version.as_deref(), progress_listener)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,9 +641,12 @@ mod tests {
|
||||
use ruma::{device_id, room_id, user_id, CanonicalJsonValue, DeviceId, RoomId, UserId};
|
||||
use serde_json::json;
|
||||
|
||||
use super::BackupMachine;
|
||||
use crate::{
|
||||
olm::BackedUpRoomKey, store::BackupDecryptionKey, types::RoomKeyBackupInfo, OlmError,
|
||||
OlmMachine,
|
||||
olm::BackedUpRoomKey,
|
||||
store::{BackupDecryptionKey, Changes, CryptoStore, MemoryStore},
|
||||
types::RoomKeyBackupInfo,
|
||||
OlmError, OlmMachine,
|
||||
};
|
||||
|
||||
fn room_key() -> BackedUpRoomKey {
|
||||
@@ -664,7 +683,10 @@ mod tests {
|
||||
|
||||
async fn backup_flow(machine: OlmMachine) -> Result<(), OlmError> {
|
||||
let backup_machine = machine.backup_machine();
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let backup_version = current_backup_version(backup_machine).await;
|
||||
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
|
||||
assert_eq!(counts.total, 0, "Initially no keys exist");
|
||||
assert_eq!(counts.backed_up, 0, "Initially no backed up keys exist");
|
||||
@@ -672,7 +694,8 @@ mod tests {
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id()).await?;
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id2()).await?;
|
||||
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
|
||||
assert_eq!(counts.backed_up, 0, "No room keys have been backed up yet");
|
||||
|
||||
@@ -691,8 +714,10 @@ mod tests {
|
||||
);
|
||||
|
||||
backup_machine.mark_request_as_sent(&request_id).await?;
|
||||
let backup_version = current_backup_version(backup_machine).await;
|
||||
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
assert_eq!(counts.total, 2);
|
||||
assert_eq!(counts.backed_up, 2, "All room keys have been backed up");
|
||||
|
||||
@@ -702,8 +727,10 @@ mod tests {
|
||||
);
|
||||
|
||||
backup_machine.disable_backup().await?;
|
||||
let backup_version = current_backup_version(backup_machine).await;
|
||||
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
assert_eq!(counts.total, 2);
|
||||
assert_eq!(
|
||||
counts.backed_up, 0,
|
||||
@@ -713,6 +740,10 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn current_backup_version(backup_machine: &BackupMachine) -> Option<String> {
|
||||
backup_machine.backup_key.read().await.as_ref().and_then(|k| k.backup_version())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn memory_store_backups() -> Result<(), OlmError> {
|
||||
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
|
||||
@@ -804,6 +835,12 @@ mod tests {
|
||||
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
|
||||
let backup_machine = machine.backup_machine();
|
||||
|
||||
// We set up a backup key, so that we can test `backup_machine.backup()` later.
|
||||
let decryption_key = BackupDecryptionKey::new().expect("Couldn't create new recovery key");
|
||||
let backup_key = decryption_key.megolm_v1_public_key();
|
||||
backup_key.set_version("1".to_owned());
|
||||
backup_machine.enable_backup_v1(backup_key).await.expect("Couldn't enable backup");
|
||||
|
||||
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
|
||||
let session_id = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
|
||||
let room_key = room_key();
|
||||
@@ -817,19 +854,29 @@ mod tests {
|
||||
|
||||
assert!(session.is_none(), "Initially we should not have the session in the store");
|
||||
|
||||
#[allow(deprecated)]
|
||||
backup_machine
|
||||
.import_backed_up_room_keys(room_keys, |_, _| {})
|
||||
.await
|
||||
.expect("We should be able to import a room key");
|
||||
|
||||
// Now check that the session was correctly imported, and that it is marked as
|
||||
// backed up
|
||||
let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
|
||||
|
||||
assert_let!(Some(session) = session);
|
||||
assert!(
|
||||
session.backed_up(),
|
||||
"If a session was imported from a backup, it should be considered to be backed up"
|
||||
);
|
||||
assert!(session.has_been_imported());
|
||||
|
||||
// Also check that it is not returned by a backup request.
|
||||
let backup_request =
|
||||
backup_machine.backup().await.expect("We should be able to create a backup request");
|
||||
assert!(
|
||||
backup_request.is_none(),
|
||||
"If a session was imported from backup, it should not be backed up again."
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -850,4 +897,34 @@ mod tests {
|
||||
|
||||
assert!(result.trusted());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_fix_backup_key_mismatch() {
|
||||
let store = MemoryStore::new();
|
||||
|
||||
let backup_decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
|
||||
store
|
||||
.save_changes(Changes {
|
||||
backup_decryption_key: Some(backup_decryption_key.clone()),
|
||||
backup_version: Some("1".to_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create the machine using `with_store` and without a call to enable_backup_v1,
|
||||
// like regenerate_olm would do
|
||||
let alice =
|
||||
OlmMachine::with_store(alice_id(), alice_device_id(), store, None).await.unwrap();
|
||||
|
||||
let binding = alice.backup_machine().backup_key.read().await;
|
||||
let machine_backup_key = binding.as_ref().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
machine_backup_key.to_base64(),
|
||||
backup_decryption_key.megolm_v1_public_key().to_base64(),
|
||||
"The OlmMachine loaded the wrong backup key."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ impl DehydratedDevices {
|
||||
let user_id = self.inner.user_id();
|
||||
let user_identity = self.inner.store().private_identity();
|
||||
|
||||
let account = Account::new(user_id);
|
||||
let account = Account::new_dehydrated(user_id);
|
||||
let store = Arc::new(CryptoStoreWrapper::new(user_id, MemoryStore::new()));
|
||||
|
||||
let verification_machine = VerificationMachine::new(
|
||||
@@ -318,7 +318,7 @@ impl DehydratedDevice {
|
||||
let mut transaction = self.store.transaction().await;
|
||||
|
||||
let account = transaction.account().await?;
|
||||
account.generate_fallback_key_helper();
|
||||
account.generate_fallback_key_if_needed();
|
||||
|
||||
let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload();
|
||||
|
||||
@@ -378,6 +378,7 @@ fn expand_pickle_key(key: &[u8; 32], device_id: &DeviceId) -> Box<[u8; 32]> {
|
||||
mod tests {
|
||||
use std::{collections::BTreeMap, iter};
|
||||
|
||||
use js_option::JsOption;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
api::client::keys::get_keys::v3::Response as KeysQueryResponse, assign,
|
||||
@@ -390,7 +391,7 @@ mod tests {
|
||||
create_session, get_prepared_machine_test_helper, to_device_requests_to_content,
|
||||
},
|
||||
olm::OutboundGroupSession,
|
||||
types::events::ToDeviceEvent,
|
||||
types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
|
||||
utilities::json_convert,
|
||||
EncryptionSettings, OlmMachine,
|
||||
};
|
||||
@@ -477,6 +478,13 @@ mod tests {
|
||||
!request.fallback_keys.is_empty(),
|
||||
"The dehydrated device creation request should contain some fallback keys"
|
||||
);
|
||||
|
||||
let device_keys: DeviceKeysType = request.device_keys.deserialize_as().unwrap();
|
||||
assert_eq!(
|
||||
device_keys.dehydrated,
|
||||
JsOption::Some(true),
|
||||
"The device keys of the dehydrated device should be marked as dehydrated."
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
|
||||
@@ -38,10 +38,6 @@ pub enum OlmError {
|
||||
#[error(transparent)]
|
||||
JsonError(#[from] SerdeError),
|
||||
|
||||
/// The event could not have been decrypted.
|
||||
#[error(transparent)]
|
||||
Decryption(#[from] vodozemac::olm::DecryptionError),
|
||||
|
||||
/// The received room key couldn't be converted into a valid Megolm session.
|
||||
#[error(transparent)]
|
||||
SessionCreation(#[from] SessionCreationError),
|
||||
@@ -263,11 +259,11 @@ pub enum SessionCreationError {
|
||||
)]
|
||||
InvalidSignature {
|
||||
/// The one-time key that failed the signature verification.
|
||||
one_time_key: SignedKey,
|
||||
one_time_key: Box<SignedKey>,
|
||||
/// The key that was used to verify the signature.
|
||||
signing_key: Option<Ed25519PublicKey>,
|
||||
signing_key: Option<Box<Ed25519PublicKey>>,
|
||||
/// The exact error describing why the signature verification failed.
|
||||
error: SignatureError,
|
||||
error: Box<SignatureError>,
|
||||
},
|
||||
|
||||
/// The user's device is missing a curve25519 key.
|
||||
@@ -289,3 +285,22 @@ pub enum SessionCreationError {
|
||||
#[error(transparent)]
|
||||
InboundCreation(#[from] vodozemac::olm::SessionCreationError),
|
||||
}
|
||||
|
||||
/// Errors that can be returned by
|
||||
/// [`crate::machine::OlmMachine::set_room_settings`].
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SetRoomSettingsError {
|
||||
/// The changes are rejected because they conflict with the previous
|
||||
/// settings for this room.
|
||||
#[error("the new settings would cause a downgrade of encryption security")]
|
||||
EncryptionDowngrade,
|
||||
|
||||
/// The changes are rejected because we would be unable to use them to
|
||||
/// encrypt events.
|
||||
#[error("the new settings are invalid")]
|
||||
InvalidSettings,
|
||||
|
||||
/// The store ran into an error.
|
||||
#[error(transparent)]
|
||||
Store(#[from] CryptoStoreError),
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user