Compare commits
1124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b16d488ad | |||
| d40aac89cb | |||
| e4ebeb8a42 | |||
| 22bbe0c32e | |||
| 05505a5a48 | |||
| 21bb85ac21 | |||
| f1a442bad0 | |||
| a8a83c3b45 | |||
| 47246483fa | |||
| 31006ab3bf | |||
| 3ed5d34f49 | |||
| 232391c6b2 | |||
| cefd5a27f5 | |||
| 97952902a3 | |||
| bf4a2ed297 | |||
| a499988621 | |||
| 0d01cabb8d | |||
| f3c0309fbc | |||
| 8070e3c165 | |||
| 02c7c2cdfc | |||
| 9b6de4e436 | |||
| b7d4be9b65 | |||
| bc86027853 | |||
| 50db563363 | |||
| 8fa07ec22d | |||
| 7aa930b81c | |||
| c02d8cee77 | |||
| 2872af234b | |||
| d614878436 | |||
| afaecdc457 | |||
| aca83fb4ed | |||
| c3e28f7e33 | |||
| 949cd78d94 | |||
| 99b9c50548 | |||
| 371e7bc052 | |||
| 0541ec7e3f | |||
| 0509236cf8 | |||
| 6cef7f20c5 | |||
| e798a51709 | |||
| 8f8aad6f4d | |||
| af84c79e69 | |||
| a920c3fdec | |||
| 9dd2d5ee3c | |||
| f341dc4131 | |||
| d446eb933e | |||
| 8f0f0fa4d4 | |||
| 36b96ccef2 | |||
| 5957232e54 | |||
| 6f60eea9ce | |||
| 982c6eab54 | |||
| cfd0c5ce0c | |||
| 8e2939bd91 | |||
| 66a79729ed | |||
| bd5f5f3fe0 | |||
| 403be3dea0 | |||
| 4d39d176d9 | |||
| d3a232607a | |||
| 3070154a57 | |||
| 563c3aae31 | |||
| 90b8ba3c2e | |||
| 031a96200b | |||
| 57e78dd22b | |||
| 53900294d0 | |||
| f483f35573 | |||
| 204e6e4ca0 | |||
| ca8c635f62 | |||
| b8a61cfc17 | |||
| ab61077a8b | |||
| 26bee1cc38 | |||
| 46232ee2c1 | |||
| 7c600fddf0 | |||
| 965a59d5b8 | |||
| f032d16d20 | |||
| 57137cdd5b | |||
| 5d83808143 | |||
| df465a0420 | |||
| 4ca69da93c | |||
| 4039359512 | |||
| 1304902cb4 | |||
| 219be9b731 | |||
| 237419c740 | |||
| 1658397139 | |||
| bab6761388 | |||
| 5193c2033f | |||
| 0f9bc20bb8 | |||
| d54f2a8b04 | |||
| 00c4071fe1 | |||
| 65287178d1 | |||
| 90b8015d71 | |||
| f256fe4b24 | |||
| 8d07f36247 | |||
| 77ee02f529 | |||
| 566a13b16e | |||
| 4bbe620d0f | |||
| 9178e4ce33 | |||
| c04a73c28d | |||
| 13244d808b | |||
| e9d5aa1221 | |||
| 57ad256fe1 | |||
| c196a9754b | |||
| a8992f37d7 | |||
| 9483703e35 | |||
| 0942dab2fd | |||
| 94bd421a8d | |||
| 7b3eb0b6f1 | |||
| 8b85ff2434 | |||
| 94c507dd38 | |||
| 37304c8cdc | |||
| 16a86587ea | |||
| e24d9b3ce3 | |||
| 9f11bced10 | |||
| e5d4ea5964 | |||
| fbc914f586 | |||
| bb2d19a1d8 | |||
| 933033cc25 | |||
| b233aa64d2 | |||
| ace96e372f | |||
| 8865e2ff74 | |||
| 2fa54e5cfa | |||
| 04275d7c27 | |||
| 90d6a37b31 | |||
| 590c2dd9fd | |||
| 478dc0ea90 | |||
| 7a422fe126 | |||
| a739ddfc84 | |||
| 1f2e8c5007 | |||
| c2a921cb58 | |||
| 06e6cba156 | |||
| 6828f93720 | |||
| 7f7b996d24 | |||
| ee252437d1 | |||
| 494532d579 | |||
| 4002136cfb | |||
| 5717eb1722 | |||
| c08194aa44 | |||
| 5d141fce13 | |||
| 70bcddfba5 | |||
| 3c48459768 | |||
| 5107f5f23a | |||
| d4b9145bc2 | |||
| 49f7fe90a9 | |||
| 75683d268f | |||
| 71abbeb1f1 | |||
| fe79826c7a | |||
| 7d64ea1bbc | |||
| 5158b39277 | |||
| be88e0ad69 | |||
| 50473ba1a8 | |||
| 5d828d234e | |||
| 9c858c1208 | |||
| 58d46f015b | |||
| 4cbd18cb37 | |||
| 888f992df0 | |||
| ee80291c41 | |||
| de3a667eb9 | |||
| ce9dc73376 | |||
| 03535832ec | |||
| c143f981bd | |||
| f4a18989fb | |||
| 6752cf73df | |||
| e87bed8ef4 | |||
| 2f19e2b762 | |||
| b66024c386 | |||
| c48bb13159 | |||
| 5f0ba1e7df | |||
| 91fa1669be | |||
| a1a4ce0a95 | |||
| 131921c045 | |||
| b62661bc70 | |||
| e0be1e8e32 | |||
| c23c3b9558 | |||
| ca3d5693b4 | |||
| 135c448f2d | |||
| 01cbce907c | |||
| df4a5c36fc | |||
| 8492968792 | |||
| d31f5b2a72 | |||
| 8469cb1146 | |||
| bb8b0cf6b9 | |||
| 35dabf7346 | |||
| f771eec3c5 | |||
| 6455585f1e | |||
| 3a34b03726 | |||
| 8cf0716db2 | |||
| 660a305cfa | |||
| b2af1eeb20 | |||
| 77b3aa8124 | |||
| cf7cb5c350 | |||
| ac7bc6461f | |||
| 7c39fd6ae5 | |||
| 40f4fc138b | |||
| d3d7c03892 | |||
| 3558886b98 | |||
| 47361b93e9 | |||
| f5cdbd8e41 | |||
| f8c23d8aa0 | |||
| 1a3c5045dd | |||
| ca1d829788 | |||
| 31e9600078 | |||
| 3291a426d8 | |||
| 1368a8534c | |||
| 7cfcc8ecc1 | |||
| c4f9c20115 | |||
| 0c81206653 | |||
| a61bc3cbbd | |||
| 74de617d76 | |||
| 3f5d54c494 | |||
| f2f99fb207 | |||
| 6196ebaba6 | |||
| 65bb373379 | |||
| e62c47132e | |||
| 996b391506 | |||
| 74722f48aa | |||
| e3180cdbc5 | |||
| 9c03c5dd7e | |||
| b46ebbf34e | |||
| d3bfdb9563 | |||
| 3887c10444 | |||
| b69575d5ff | |||
| 543152d914 | |||
| c8b38257f1 | |||
| 2df359d316 | |||
| 951a4354c6 | |||
| befcd069c3 | |||
| 0c26988cf5 | |||
| a7f69973c2 | |||
| 1750bf597f | |||
| ad677cb6f2 | |||
| 350a26cee9 | |||
| 08152bd9fc | |||
| 89183a3d4b | |||
| 65ed4f3f22 | |||
| 41d392f899 | |||
| 1ce5160846 | |||
| dc4c6b4d73 | |||
| 3b33f3779f | |||
| 7089ff51c4 | |||
| d8de12561b | |||
| 56edc9d00f | |||
| 2ea114d988 | |||
| c7708d6154 | |||
| bdfe64179b | |||
| 59c47fb22d | |||
| 821fa8fa99 | |||
| a5f1769e28 | |||
| 59fce90943 | |||
| 81bebcf692 | |||
| c4dd2d192e | |||
| a901506a53 | |||
| 87f89ec561 | |||
| 2820f5f3b4 | |||
| 427c59e266 | |||
| 4a7f924161 | |||
| 2829b07305 | |||
| 4f49b23751 | |||
| 962a78ab13 | |||
| 664f6d5f5a | |||
| 30f3a3c2e4 | |||
| 8df5d655c0 | |||
| 1552426961 | |||
| 8b7494d17b | |||
| 77e5281781 | |||
| efc2e2c4c8 | |||
| 79798a9de9 | |||
| 3d0423447c | |||
| 9999d3ba96 | |||
| ee4ef2eb53 | |||
| 0b57ef4bf6 | |||
| 6dd2e3becf | |||
| 1018d71bb7 | |||
| 87472e7679 | |||
| cdbfae2aee | |||
| 2eca7271ea | |||
| 92a02a51c4 | |||
| 019d198af8 | |||
| e6db85b7d4 | |||
| a4bda1ac66 | |||
| e46e63771b | |||
| 41a2ad09cf | |||
| 32919405d6 | |||
| b002a8da52 | |||
| 85682ac37f | |||
| 22c765b9ab | |||
| 3ad8f1d607 | |||
| a4782939b3 | |||
| 72dc307400 | |||
| 248cf55272 | |||
| 1260e740ba | |||
| 9a4a67d488 | |||
| ab0871f299 | |||
| 86d9fe59d2 | |||
| 1945b508c3 | |||
| 4c7461357c | |||
| ca7f2ad3d0 | |||
| 711f4cb868 | |||
| cb51a3155a | |||
| 81119a66d8 | |||
| 9c6413551c | |||
| 42f0d83b53 | |||
| d9167f208a | |||
| 7f0a3f0e47 | |||
| 8fe61e1fb3 | |||
| 07cfe3da94 | |||
| 4d472f6aed | |||
| ce231e6c2b | |||
| b36a9ad781 | |||
| 95ae5d1938 | |||
| 9d976d0bcf | |||
| 17370a5702 | |||
| b793acd2b1 | |||
| 752706c51d | |||
| 736aa0351c | |||
| 4bcb9b7d9f | |||
| 867d9c71fd | |||
| 2dcf06fad2 | |||
| 1fc3450eac | |||
| 19b9a73ecc | |||
| 4d45b02e91 | |||
| a9cfba2c03 | |||
| ff7e8c75ee | |||
| 2967b73aff | |||
| 6f0fbf92e4 | |||
| 4cbc162964 | |||
| 6c7acf6faa | |||
| 181ee643b1 | |||
| a12a46b777 | |||
| 93fce02606 | |||
| 351fbf60c1 | |||
| 5c353923cd | |||
| dc4cc02926 | |||
| 6b543d105f | |||
| 5a1728a468 | |||
| 5c8d1d816e | |||
| bd7f0d695b | |||
| ccf8bf8652 | |||
| f21de25da0 | |||
| d789983eff | |||
| f8e65f53cd | |||
| 05cbb9e290 | |||
| d403bf3431 | |||
| 56ccda4ded | |||
| 157499955a | |||
| 8a71ac622d | |||
| 45968b2a2b | |||
| efbf9472f2 | |||
| 1434285a1b | |||
| 46856f54af | |||
| 3ce2f16d55 | |||
| abf3c6e7b7 | |||
| a3a0125421 | |||
| 657c72904a | |||
| de752eb089 | |||
| a4415c9fa5 | |||
| 5d46b35d95 | |||
| 65b422312c | |||
| 7bac0340d6 | |||
| 1d1863d323 | |||
| a695e291fa | |||
| c5f5bc8496 | |||
| cd072e6dff | |||
| 9b36a04bb9 | |||
| 6b357de947 | |||
| e85e50b185 | |||
| c9fd5a0787 | |||
| 6de676f491 | |||
| 06f60e3b62 | |||
| 96765cad28 | |||
| 4f265ccd22 | |||
| 759a9b0e18 | |||
| 0a854cdbf7 | |||
| 40e6e9f028 | |||
| 8f4fcf6299 | |||
| 2283c28503 | |||
| 59d3608c32 | |||
| 06e9f01a4a | |||
| 0ff63d3008 | |||
| 5cc7730dd9 | |||
| e5bd7602e4 | |||
| 2fc4aacdd0 | |||
| f7d99cc506 | |||
| 60319914e1 | |||
| 740356a350 | |||
| 806ee13aa0 | |||
| 3fd2f5794e | |||
| dc055c632c | |||
| c2ab795122 | |||
| e7bc510313 | |||
| 7efee5a5af | |||
| 866b6e5f2d | |||
| 1e0e815fab | |||
| 99fe49bbac | |||
| 68bc14e567 | |||
| fe648d9cb5 | |||
| 743799fbd2 | |||
| ac61fc8830 | |||
| 52898fa526 | |||
| 263386ea53 | |||
| 0082fbc0b4 | |||
| 281a79ffc6 | |||
| bdf303aa57 | |||
| 7dcf45562c | |||
| 548c66750f | |||
| 67df36f733 | |||
| e61fb45504 | |||
| 9b7f89c183 | |||
| 322c5b3f83 | |||
| 14ec35e67f | |||
| 2bb0c50266 | |||
| d254217217 | |||
| 83ce4c7ca2 | |||
| 2562aa3fee | |||
| fcb1c96869 | |||
| 88ceeb3513 | |||
| a5dc8ff871 | |||
| c3caf6cbca | |||
| 8469c6465e | |||
| 03f7806000 | |||
| 5ba90611b4 | |||
| bda2acf5f6 | |||
| 40f1ce80ea | |||
| 3492bd6929 | |||
| 5e9f629edb | |||
| 794dbb36dc | |||
| 2a03de3bd5 | |||
| 79d8738ff5 | |||
| e16ca9d8ba | |||
| 746d7e13ab | |||
| 1b4f665d99 | |||
| 7d1bbfaa32 | |||
| 8119697ef0 | |||
| decdd6f47e | |||
| 72febaee57 | |||
| 7a2728f8b5 | |||
| f576c72ef8 | |||
| 1429c1a06a | |||
| b7ce2dc7e6 | |||
| ea9da3bdad | |||
| a1bb7c0acc | |||
| ea4b9635c9 | |||
| 5b79a9843e | |||
| abbe2ec523 | |||
| 965390cbdc | |||
| 119bee66ce | |||
| 4fd4410f4a | |||
| af390328b5 | |||
| 98a3a0b3c4 | |||
| aa92e26342 | |||
| dd13fe6b4e | |||
| a9ed62284e | |||
| 2532c5227f | |||
| 72cc2bd60c | |||
| 2408df8bf5 | |||
| 5827bb7ab3 | |||
| 25111ac9eb | |||
| 6ae7d3c017 | |||
| bbe16db94c | |||
| f8961a4382 | |||
| 9e7ab635c6 | |||
| a024c010ce | |||
| 3555474cad | |||
| 2b3ad86869 | |||
| 075f3fa9d2 | |||
| 47c7d05499 | |||
| 4b970e879f | |||
| cc0dfd62e7 | |||
| 31e6df7234 | |||
| 2576042194 | |||
| a3ae9dca75 | |||
| 6e36111462 | |||
| b7bde3cabe | |||
| cb825864b9 | |||
| 08df153ed9 | |||
| a6f84d8513 | |||
| 729ba3e22b | |||
| 83cc0acf7b | |||
| ef6237045e | |||
| d005311235 | |||
| c66ea8162c | |||
| 40c1e8a2da | |||
| d2709c0679 | |||
| c9a46173b9 | |||
| 8a2929fb51 | |||
| 79f412790f | |||
| 5abff2970c | |||
| dcc20b6c96 | |||
| 4e541ad825 | |||
| 67a4a322f5 | |||
| 26e6a038a1 | |||
| e8a920118f | |||
| 626b3d152c | |||
| 38ed66c1b1 | |||
| 19e89bbd6a | |||
| a07be884b7 | |||
| 24d4e60c2b | |||
| 7d7142add3 | |||
| 57352f0154 | |||
| 7eea5628d3 | |||
| ea794bb9f2 | |||
| 10a0d59012 | |||
| 16fd88c419 | |||
| 1eecb2d603 | |||
| 98ba714b20 | |||
| 07aa6d7bc7 | |||
| 9df1c48079 | |||
| 977a9995fe | |||
| f978960d30 | |||
| 3f93324a85 | |||
| 3204953738 | |||
| 88b005ace3 | |||
| c761a84acd | |||
| a2bfc07ecc | |||
| b1a533a071 | |||
| fed418d9a8 | |||
| 2c2d8e9ff0 | |||
| b9b8de7ff1 | |||
| 552df0e4c6 | |||
| 12f36d5972 | |||
| f7ee643475 | |||
| 73486b2b7b | |||
| 3c27f83857 | |||
| 385c2b8e71 | |||
| 6bc9887314 | |||
| 30d3d9d26c | |||
| dfb67c88e6 | |||
| 98a79de811 | |||
| 62d4abd454 | |||
| 7b71d3ca1b | |||
| 31b4d0a2d1 | |||
| 14ee78c54d | |||
| f3d3924bb6 | |||
| 6c704352a9 | |||
| 0db0ea0977 | |||
| 3f7909641f | |||
| 5b0ad01bab | |||
| b0e8121347 | |||
| aa94ad846b | |||
| 1dd8c908c5 | |||
| 5b14fe6f34 | |||
| a737421875 | |||
| 49252b5342 | |||
| d8b0f9f3d7 | |||
| 1de99161e2 | |||
| 675f576343 | |||
| 7cf8e9eb9b | |||
| 7bcc920514 | |||
| 12653fb2b6 | |||
| eeaf31ce53 | |||
| 228a117ccb | |||
| 3f408a9a36 | |||
| 7f4e79e2a3 | |||
| 7807ed8bda | |||
| f8dd5c76d2 | |||
| 6068d3e870 | |||
| 76d161ac9a | |||
| ca42657abb | |||
| 4636a9177e | |||
| 5e662e855d | |||
| 513c80df5e | |||
| 956dda1073 | |||
| de6016de1e | |||
| 72f42a46d8 | |||
| 03a4cc46ad | |||
| 63ca064f93 | |||
| a19cb24567 | |||
| 99c44ee883 | |||
| d0c5d87a96 | |||
| 222be6983c | |||
| f3bc24e98f | |||
| 22a19e26d3 | |||
| b9628301ec | |||
| b4683c0ff5 | |||
| cb3c5ab1ce | |||
| 8c5ffc9a96 | |||
| 9ec46ddf2c | |||
| bfb04f2ddd | |||
| eecd00cd98 | |||
| b8d90286aa | |||
| 424d01d964 | |||
| 84e4552da7 | |||
| c4624cc863 | |||
| c3973589c8 | |||
| 224292ab3e | |||
| 0311f30182 | |||
| 0877445273 | |||
| 7f02447c78 | |||
| 308d658224 | |||
| 9808ad7d16 | |||
| 06fc220268 | |||
| f399f229ae | |||
| a8b38d271e | |||
| 66e901bb9b | |||
| 9edca06d3b | |||
| 47444cc671 | |||
| e1fe1ca129 | |||
| 6181387776 | |||
| 81a75508dc | |||
| f6faf6267e | |||
| 25db0b2cc0 | |||
| 2d6aec7319 | |||
| d17a49e827 | |||
| 1321e92b30 | |||
| 6e62f8b269 | |||
| 6e369aecc9 | |||
| ece33059e1 | |||
| 468ee53644 | |||
| 067b1e0020 | |||
| 96c7f36d75 | |||
| 01f88f1c4b | |||
| 9e5b06902d | |||
| 245f011fb6 | |||
| e1f623cf56 | |||
| 01fd365f67 | |||
| 875f59133b | |||
| de8537f8b0 | |||
| d9ffe2867f | |||
| b908fa78b9 | |||
| f568e8c4d3 | |||
| 8005515ec5 | |||
| 758171931d | |||
| 47671a182d | |||
| b9d49b85c3 | |||
| 5753c53ea7 | |||
| fe143ffbed | |||
| 25e406f669 | |||
| 154f86aa20 | |||
| c769e32b41 | |||
| c78639d880 | |||
| af5107f529 | |||
| d6d9cd129a | |||
| d1d4c6417c | |||
| d5630bc5cd | |||
| 8a6ac05519 | |||
| a3262740ac | |||
| fb63c8dfbc | |||
| e01c1aecbb | |||
| 6e5601db1f | |||
| 5586a81703 | |||
| be133886b7 | |||
| 6fb38179a4 | |||
| 912cb233f5 | |||
| 4042db7d50 | |||
| 4273dff33e | |||
| 5f27f487e6 | |||
| 6cbd197514 | |||
| c10894bed6 | |||
| 2291268679 | |||
| 94e9005132 | |||
| 3a1c374a13 | |||
| 2ac71fc89c | |||
| a484964d4d | |||
| 5f159d4418 | |||
| aeaedf7e5b | |||
| 2db031cec5 | |||
| 3329e75708 | |||
| cdd6c23e15 | |||
| 6dc8d3980e | |||
| a9c0dc3da4 | |||
| c04dd18440 | |||
| 794bf98a1b | |||
| e88f14a1f9 | |||
| b95c189a18 | |||
| 92fe72f83a | |||
| 01e2db1f52 | |||
| 40d447dc69 | |||
| 4cc5790c64 | |||
| fbc9db9b15 | |||
| f6b21e6ce9 | |||
| 05542f7ba8 | |||
| b06bb42d3e | |||
| e4d0f2291f | |||
| 31f84d7534 | |||
| 4c5b537825 | |||
| d9e6bfa678 | |||
| f0d98602a9 | |||
| a27ebaabae | |||
| 9b78903705 | |||
| 0151f32425 | |||
| ca09917d84 | |||
| 78924ed877 | |||
| 668a267c9b | |||
| fed08fad76 | |||
| 651b61414e | |||
| bbefad34bc | |||
| b00f58a28d | |||
| 3b3474688b | |||
| ed19bf7bc5 | |||
| eeb325abb7 | |||
| 9d7dd1a6d6 | |||
| 820f4ee711 | |||
| 271ba98ba9 | |||
| 7575c256d4 | |||
| cd0d79dd88 | |||
| d35e3405ab | |||
| 711a753533 | |||
| 4e291205d5 | |||
| b497577717 | |||
| 9077310eb2 | |||
| b4d265e997 | |||
| dd810f4803 | |||
| a6b78d8d53 | |||
| 96c4e4c49e | |||
| 5f9a4fc6d1 | |||
| 31b25b8754 | |||
| fa6066b810 | |||
| 102da7cb9a | |||
| 1c39086b5a | |||
| 651e8fcd48 | |||
| ad947132ed | |||
| 2a3525f7be | |||
| 7e2c773d21 | |||
| 6becbf61c9 | |||
| 1155f75612 | |||
| 5cd29830a3 | |||
| 34370b1525 | |||
| 1132074ae0 | |||
| e5af5a32fa | |||
| 1e8dd5dd41 | |||
| 5431c0fdd6 | |||
| a240b87ba6 | |||
| 324cf2e007 | |||
| 66142317d4 | |||
| f66c74e878 | |||
| 1862a3e254 | |||
| dadc85c4fc | |||
| d8c1094939 | |||
| ace937fcee | |||
| 1e58c0382c | |||
| ce95cc06e0 | |||
| 3803792518 | |||
| 4ece38af4f | |||
| 4baa82d3a6 | |||
| 8b01e3e209 | |||
| 4a69cc9110 | |||
| 766786e2f1 | |||
| e67e2289e2 | |||
| 5954ee18b7 | |||
| e15ddf6ad9 | |||
| 19b6495f2b | |||
| 0ba6adbf34 | |||
| 3886a55ad8 | |||
| 8f59f45183 | |||
| a12a244a89 | |||
| f3587a44dc | |||
| c76ea95f65 | |||
| bdfc300e4c | |||
| d143c6198c | |||
| 35b62a1a4a | |||
| 704fe6719f | |||
| 0b9e07a386 | |||
| 0a28c222f5 | |||
| 31dbca6c28 | |||
| f96e82f833 | |||
| f2792801c3 | |||
| a0c8b71236 | |||
| 101f6bd57b | |||
| fa394fc45b | |||
| 16ca282ae4 | |||
| be404f6666 | |||
| 89ce8870a9 | |||
| 70f46d48af | |||
| 58f4e2b1ad | |||
| 58c35ee507 | |||
| 40c347846e | |||
| 9c809b900d | |||
| a03893ee3f | |||
| 045374c604 | |||
| f3fe06be7d | |||
| 2220973adc | |||
| d8b0b2e097 | |||
| 93f6bdecb3 | |||
| 85b74edf80 | |||
| 6f5442c5ce | |||
| 18e27f3090 | |||
| 066fdf99c3 | |||
| 2c0d858833 | |||
| 0b5f9aec5e | |||
| 1b05380b60 | |||
| 072b5d5605 | |||
| 7e9f4fc5a0 | |||
| 2e410e5d94 | |||
| d81b12f389 | |||
| 58c4075ff5 | |||
| 3c9ae33be1 | |||
| c171c518c6 | |||
| 20f717ec31 | |||
| 0e01b1d93c | |||
| 3d3f93a33d | |||
| b4fe3059c3 | |||
| 57963dcf36 | |||
| 7d9fdc4f05 | |||
| 1c4f035c99 | |||
| 71e4f60fa5 | |||
| c3848ca016 | |||
| a74f6bcc3f | |||
| ba7fb7fc36 | |||
| 96b615ba8e | |||
| c83fa3a532 | |||
| d509d79472 | |||
| ffba842919 | |||
| 91c10ae213 | |||
| efdd6d2693 | |||
| 08bc563e9e | |||
| 71b5ab4d07 | |||
| 4569b25d98 | |||
| 9f7f1a98fb | |||
| 4432b332fa | |||
| d846563df3 | |||
| b453a0204e | |||
| 4c220ed030 | |||
| 037badf7a4 | |||
| fba61751d5 | |||
| 2bd4db8a23 | |||
| 4de1375a76 | |||
| 1353406b80 | |||
| e1bffaee21 | |||
| 0496ef4313 | |||
| 674605aeab | |||
| f1b20a8ea5 | |||
| d1fe27c969 | |||
| 769a627496 | |||
| aff7aefd28 | |||
| d60b9d3da2 | |||
| 8d5dc18dd3 | |||
| 49790c9f91 | |||
| 2e8b135859 | |||
| 7614bfb716 | |||
| 0407dd7eb1 | |||
| 16a638400c | |||
| be6bc444f8 | |||
| 36091a9ef3 | |||
| 1160383d71 | |||
| d7f3914673 | |||
| 1ca4377baf | |||
| ab12f2f7ca | |||
| 6889430474 | |||
| 382c573973 | |||
| 643c9a0b8e | |||
| 689bf9b4dc | |||
| 21efd60dba | |||
| 0850c0c1c9 | |||
| aee8728418 | |||
| 7c5c5a8f30 | |||
| bfa069d6c0 | |||
| de502f42c6 | |||
| dc1a34cd56 | |||
| 11a26d7fc4 | |||
| 1abbaa8607 | |||
| bae0f304f7 | |||
| 25df9a11c5 | |||
| 1f937278b2 | |||
| e2ff2daf5d | |||
| 8760ea8440 | |||
| d03d3cff17 | |||
| f88c6dff4d | |||
| d3835bb992 | |||
| 93a0fe500b | |||
| 1f369c5912 | |||
| 2289c813f1 | |||
| 0a9431d01d | |||
| f51eebb55f | |||
| 8895e532bb | |||
| 40e3a96ae3 | |||
| 8fe2b37354 | |||
| db064626fa | |||
| 0b46a7e29c | |||
| 449e8b40b8 | |||
| d7224a7ede | |||
| e0833110f2 | |||
| 3756eeb385 | |||
| dd252937c1 | |||
| ab6d039369 | |||
| 6fca1e81ed | |||
| ab0494549e | |||
| 15bf675e5e | |||
| 95637c57da | |||
| 8f90a76cb4 | |||
| 844923dd44 | |||
| 9efc6494d1 | |||
| 5191737389 | |||
| 4fdc78f565 | |||
| 5bbe022e97 | |||
| 6faf3f75e0 | |||
| 38e842fc0f | |||
| 73759fc361 | |||
| c1fda3a601 | |||
| 689f006c07 | |||
| 6f0da7e91b | |||
| ce68ad4968 | |||
| 02a929c614 | |||
| 7a660749ed | |||
| 6e82e4f14f | |||
| f0015bb10d | |||
| aa4f606171 | |||
| 3a386121a1 | |||
| 3d40a5c30c | |||
| 4c76255689 | |||
| 37d9fa784a | |||
| fa1cf32883 | |||
| 002767a146 | |||
| 5cd4462d27 | |||
| 2eb6930988 | |||
| 0246863af3 | |||
| d973fef280 | |||
| 54c6f0517f | |||
| 9a7f18c62c | |||
| f0ef37efae | |||
| d9e91344aa | |||
| 4883f3fa77 | |||
| 0d00bda0c6 | |||
| 947a1b1aeb | |||
| ece0c6d703 | |||
| 79010af9e2 | |||
| 99da0ff18d | |||
| 2045b326b9 | |||
| dfdea0cb2e | |||
| 56e3780808 | |||
| 96763aec42 | |||
| 925c5b2233 | |||
| 5242f647f3 | |||
| ed2ab3ffe6 | |||
| 30c401ac75 | |||
| bbae5364b3 | |||
| ec057cf354 | |||
| d4ad2b26cd | |||
| 9b1b67fa09 | |||
| 07f5289e7e | |||
| d65e33ca6a | |||
| 730d5a3803 | |||
| 23a232e99f | |||
| 4f79a15fa9 | |||
| dda080c497 | |||
| 11d5e56892 | |||
| 8b0d6afe4b | |||
| a18f90bfaa | |||
| 60ed367fd9 | |||
| 1157067dba | |||
| 8efdba6136 | |||
| 1029e51eb3 | |||
| 5679a45f75 | |||
| 76a7052149 | |||
| 2aecf2950d | |||
| 4bbb6bd60c | |||
| e37f65c46b | |||
| 92b4c2a469 | |||
| 20eb1db0f9 | |||
| 30e95bf992 | |||
| dd20c37f35 | |||
| ea2a27075a | |||
| c22de4c035 | |||
| f84ce6a34f | |||
| 508176a2c7 | |||
| dc9b975fc0 | |||
| ea9f79a006 | |||
| 1b92a034fd | |||
| 5a4bf780fb | |||
| c1a92bb3a9 | |||
| e3b950a9f0 | |||
| b79fdefbfd | |||
| e3ad875293 | |||
| 3f6df64386 | |||
| 3bab1c3584 | |||
| 897f1cfef1 | |||
| 1cc8292034 | |||
| d2c6a83175 | |||
| 91f8cfb48c | |||
| 2fde15da79 | |||
| 9d3fcb9290 | |||
| 171e4ef8f8 | |||
| 55d0a3cdb8 | |||
| 1f1310c797 | |||
| c9e2db1fe5 | |||
| af469f6bbd | |||
| 8a52d6f2a3 | |||
| 8ebf3c02c6 | |||
| 8fab34cc8c | |||
| 99010ed83b | |||
| 5428339b27 | |||
| 942b2f937c | |||
| 8845550e72 | |||
| 84c9280349 | |||
| 5dbd5f1adf | |||
| 1730ec5155 | |||
| 8e65099f3d | |||
| 62137e5a3e | |||
| 84dfb78e1d | |||
| 81d388a55b | |||
| 7b4f480b2a | |||
| 034bd64e2c | |||
| 328c4767a0 | |||
| 7a85b7abdc | |||
| 851784c8c0 | |||
| faa961eb7a | |||
| a7011d8ac0 | |||
| e6525c093f | |||
| 57eb225506 | |||
| e1a607b6cf | |||
| 03e1fd78a6 | |||
| a5dbfa66a7 | |||
| 6f0d3b663b | |||
| 9d464eb908 | |||
| 48f11ea025 | |||
| d9b2b53f83 | |||
| 6bcd07fd7b | |||
| bacf85d807 | |||
| b163368be0 | |||
| 5ebfd7bc55 | |||
| 0d264d209f | |||
| ea8628e210 | |||
| f4078fdf68 | |||
| c366bae428 | |||
| cf1ec862c2 | |||
| 10db61575f | |||
| 2e936702c8 | |||
| 5922fb8ff3 | |||
| f6c2a28682 | |||
| 6ee2919576 | |||
| b0f60d2bf7 | |||
| 61fb0aeafc | |||
| 9e9df163b9 | |||
| 54c037ed03 | |||
| 7d16ca54f4 | |||
| f96f55ccd9 | |||
| afa2e8063d | |||
| 25bb8e9d72 | |||
| c59ed5d877 | |||
| 396b7eff7d | |||
| a5702e92f1 | |||
| c4e45b5660 | |||
| 847bf5b974 | |||
| 0449ca89ce | |||
| 40343aa67e | |||
| 625652e895 | |||
| d78b6826b9 | |||
| e1fbfbe603 | |||
| a9a4d7b4c8 | |||
| 8e90783f1f | |||
| a0a076a895 | |||
| f7504b4ff2 | |||
| d7cbd9d218 | |||
| 5a04f5b66a | |||
| 3294a6c83a | |||
| 8d54bd92d1 | |||
| 2d3e2dab54 | |||
| 1bc044349e | |||
| 2ca6a0e91e | |||
| 19fcae4e0b | |||
| a76b6faa9f | |||
| 2785365b53 | |||
| c7d1a7db4f | |||
| 48fc80c643 | |||
| 96475b7f50 | |||
| 3d9bdd2bb4 | |||
| e5f92947e7 | |||
| 02dddb47c9 | |||
| 753606779b | |||
| 4cf0d7a18b | |||
| ec7fa76240 | |||
| 66e02f39ef | |||
| c8e05173e4 | |||
| 8e0282ac4a | |||
| 11cbf849cc | |||
| f9a19c5603 | |||
| fac7221c7e | |||
| d6300bbda7 | |||
| b80c2f7197 | |||
| 07b6425e10 | |||
| d49cb54b67 | |||
| aaccfdfea5 | |||
| ad4d24f7d0 | |||
| 66743f5e57 | |||
| 03d4a30eb4 | |||
| a82a1f505e | |||
| 98e9abd6c9 | |||
| 2a8e8c1fff | |||
| 9aa277405d | |||
| 3be5311113 | |||
| a957e70698 | |||
| 77feed2447 | |||
| 7b25a1c2f0 | |||
| bb0e50ce02 | |||
| ab3ea8c467 | |||
| 6679caecf9 | |||
| 885b2b22bd | |||
| d8b2b74a2d | |||
| 76caf7ed05 | |||
| cdc3743888 | |||
| 9b97a2ed26 | |||
| aff07c13fc | |||
| 9260942c5d | |||
| c4413c6ac3 | |||
| 4d12a78341 | |||
| a6dce1c0d7 | |||
| 09d53a52ad | |||
| 6d46e35d50 | |||
| 87653da2e3 | |||
| a38eaf08be | |||
| 786015f18c | |||
| d60ec55e30 | |||
| 99e284d8b0 | |||
| 3588b88303 | |||
| b4bbb10ba5 | |||
| 9a02d6877f | |||
| 765b95468a | |||
| 813ce6a14d | |||
| 5d68f89372 | |||
| b525002828 | |||
| ab190ad29c | |||
| 76477281c2 | |||
| 2c25103226 | |||
| 606a1510cf | |||
| ed086afe83 | |||
| 51ca5a7113 | |||
| ff4af894e4 | |||
| ec80c6ff7b | |||
| daf878fa7f | |||
| 7aa7d1ca53 | |||
| 1270cdad1a | |||
| 73b481a8fc | |||
| f25916cb5c | |||
| 637e830e85 | |||
| 04362cdc36 | |||
| fa10bbb5dd | |||
| 4164effbf9 | |||
| 917e8c291e |
@@ -1,13 +1,3 @@
|
||||
# Pass the rustflags specified to host dependencies (build scripts, proc-macros)
|
||||
# when a `--target` is passed to Cargo. Historically this was not the case, and
|
||||
# because of that, cross-compilation would not set the rustflags configured
|
||||
# below in `target.'cfg(...)'` for them, resulting in cache invalidation.
|
||||
#
|
||||
# Since this is an unstable feature (enabled at the bottom of the file), this
|
||||
# setting is unfortunately ignored on stable toolchains, but it's still better
|
||||
# to have it apply on nightly than using the old behavior for all toolchains.
|
||||
target-applies-to-host = false
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
@@ -15,42 +5,5 @@ uniffi-bindgen = "run --package uniffi-bindgen --"
|
||||
[doc.extern-map.registries]
|
||||
crates-io = "https://docs.rs/"
|
||||
|
||||
# Exclude tarpaulin, android and ios from extra lints since on stable, without
|
||||
# the nightly-only target-applies-to-host setting at the top, cross compilation
|
||||
# and otherwise changing cfg's can be very bad for caching. These should never
|
||||
# be the default either and don't have much target-specific code that would
|
||||
# benefit from the extra lints.
|
||||
[target.'cfg(not(any(tarpaulin, target_os = "android", target_os = "ios")))']
|
||||
rustflags = [
|
||||
"-Wrust_2018_idioms",
|
||||
"-Wsemicolon_in_expressions_from_macros",
|
||||
"-Wunused_extern_crates",
|
||||
"-Wunused_import_braces",
|
||||
"-Wunused_qualifications",
|
||||
"-Wtrivial_casts",
|
||||
"-Wtrivial_numeric_casts",
|
||||
"-Wclippy::cloned_instead_of_copied",
|
||||
"-Wclippy::dbg_macro",
|
||||
"-Wclippy::inefficient_to_string",
|
||||
"-Wclippy::macro_use_imports",
|
||||
"-Wclippy::mut_mut",
|
||||
"-Wclippy::needless_borrow",
|
||||
"-Wclippy::nonstandard_macro_braces",
|
||||
"-Wclippy::str_to_string",
|
||||
"-Wclippy::todo",
|
||||
"-Wclippy::unused_async",
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")']
|
||||
rustflags = [
|
||||
# We have some types that are !Send and/or !Sync only on wasm, it would be
|
||||
# slightly more efficient, but also pretty annoying, to wrap them in Rc
|
||||
# where we would use Arc on other platforms.
|
||||
"-Aclippy::arc_with_non_send_sync",
|
||||
]
|
||||
|
||||
# activate the target-applies-to-host feature.
|
||||
# Required for `target-applies-to-host` at the top to take effect.
|
||||
[unstable]
|
||||
rustdoc-map = true
|
||||
target-applies-to-host = true
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
|
||||
[graph]
|
||||
all-features = true
|
||||
exclude = [
|
||||
# dev only dependency
|
||||
"criterion"
|
||||
]
|
||||
|
||||
[advisories]
|
||||
version = 2
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
version = 2
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Zlib",
|
||||
]
|
||||
exceptions = [
|
||||
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
|
||||
{ allow = ["CDDL-1.0"], crate = "inferno" },
|
||||
{ allow = ["LicenseRef-ring"], crate = "ring" },
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "LicenseRef-ring"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[bans]
|
||||
# We should disallow this, but it's currently a PITA.
|
||||
multiple-versions = "allow"
|
||||
wildcards = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
|
||||
allow-git = [
|
||||
# A patch override for the bindings fixing a bug for Android before upstream
|
||||
# releases a new version.
|
||||
"https://github.com/element-hq/tracing.git",
|
||||
# Sam as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
"https://github.com/jplatte/async-compat",
|
||||
]
|
||||
@@ -1 +1,2 @@
|
||||
* @matrix-org/rust
|
||||
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: "weekly"
|
||||
@@ -1,13 +0,0 @@
|
||||
name: Security audit
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/audit@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
environment: matrix-rust-bot
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
|
||||
@@ -61,10 +61,77 @@ jobs:
|
||||
- name: Build library & generate bindings
|
||||
run: target/debug/xtask ci bindings
|
||||
|
||||
test-android:
|
||||
name: matrix-rust-components-kotlin
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
ref: main
|
||||
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
|
||||
- name: Install android sdk
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.4
|
||||
|
||||
- name: Install android ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
id: install-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# 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: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Install Rust dependencies
|
||||
run: |
|
||||
rustup target add x86_64-linux-android
|
||||
cargo install cargo-ndk
|
||||
|
||||
- name: Build SDK bindings for Android
|
||||
# Building for x86_64-linux-android as it's the most prone to breaking and building for every arch is too much
|
||||
run: |
|
||||
echo "Building SDK for x86_64-linux-android and creating bindings"
|
||||
target/debug/xtask kotlin build-android-library --package full-sdk --only-target x86_64-linux-android --src-dir rust-components-kotlin/sdk/sdk-android/src/main
|
||||
echo "Copying the result binary to the Android project"
|
||||
cd rust-components-kotlin
|
||||
echo "Building the Kotlin bindings"
|
||||
./gradlew :sdk:sdk-android:assembleDebug
|
||||
|
||||
test-apple:
|
||||
name: matrix-rust-components-swift
|
||||
needs: xtask
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
@@ -108,7 +175,7 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
@@ -119,12 +186,12 @@ jobs:
|
||||
|
||||
test-crypto-apple-framework-generation:
|
||||
name: Generate Crypto FFI Apple XCFramework
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
|
||||
+24
-24
@@ -26,7 +26,6 @@ jobs:
|
||||
test-matrix-sdk-features:
|
||||
name: 🐧 [m], ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -41,7 +40,6 @@ jobs:
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
- image-proc
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -50,6 +48,11 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -79,7 +82,6 @@ jobs:
|
||||
name: 🐧 [m]-examples
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
@@ -111,12 +113,16 @@ jobs:
|
||||
name: 🐧 [m]-crypto
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -141,7 +147,6 @@ jobs:
|
||||
|
||||
test-all-crates:
|
||||
name: ${{ matrix.name }}
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -169,6 +174,12 @@ jobs:
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install libsqlite
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
@@ -194,7 +205,6 @@ jobs:
|
||||
test-wasm:
|
||||
name: 🕸️ ${{ matrix.name }}
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -270,7 +280,6 @@ jobs:
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
@@ -289,20 +298,18 @@ jobs:
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.20.10
|
||||
uses: crate-ci/typos@v1.27.3
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
@@ -337,14 +344,13 @@ jobs:
|
||||
|
||||
integration-tests:
|
||||
name: Integration test
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
services:
|
||||
# sliding sync needs a postgres container
|
||||
# synapse needs a postgres container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
@@ -362,21 +368,10 @@ jobs:
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# run sliding sync and point it at the postgres container and synapse container.
|
||||
# the postgres container needs to be above this to make sure it has started prior to this service.
|
||||
slidingsync:
|
||||
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./coverage.yml
|
||||
env:
|
||||
SYNCV3_SERVER: "http://synapse:8008"
|
||||
SYNCV3_SECRET: "SUPER_CI_SECRET"
|
||||
SYNCV3_BINDADDR: ":8118"
|
||||
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
|
||||
ports:
|
||||
- 8118:8118
|
||||
# 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:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./coverage.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./coverage.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
@@ -387,6 +382,11 @@ jobs:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
|
||||
@@ -25,12 +25,10 @@ jobs:
|
||||
code_coverage:
|
||||
name: Code Coverage
|
||||
runs-on: "ubuntu-latest"
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
services:
|
||||
# sliding sync needs a postgres container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
@@ -48,21 +46,10 @@ jobs:
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# run sliding sync and point it at the postgres container and synapse container.
|
||||
# the postgres container needs to be above this to make sure it has started prior to this service.
|
||||
slidingsync:
|
||||
image: "ghcr.io/matrix-org/sliding-sync:v0.99.11" # keep in sync with ./ci.yml
|
||||
env:
|
||||
SYNCV3_SERVER: "http://synapse:8008"
|
||||
SYNCV3_SECRET: "SUPER_CI_SECRET"
|
||||
SYNCV3_BINDADDR: ":8118"
|
||||
SYNCV3_DB: "user=postgres password=postgres dbname=syncv3 sslmode=disable host=postgres"
|
||||
ports:
|
||||
- 8118:8118
|
||||
# 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:5b6a75935e560945f69af72e9768bbaac10c9b4f # keep in sync with ./ci.yml
|
||||
image: ghcr.io/matrix-org/synapse-service:v1.117.0 # keep in sync with ./ci.yml
|
||||
env:
|
||||
SYNAPSE_COMPLEMENT_DATABASE: sqlite
|
||||
SERVER_NAME: synapse
|
||||
@@ -75,6 +62,11 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -102,7 +94,7 @@ jobs:
|
||||
run: |
|
||||
rustup run stable cargo tarpaulin \
|
||||
--skip-clean --profile cov --out xml \
|
||||
--features experimental-widgets,testing,image-proc
|
||||
--features experimental-widgets,testing
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities)
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -0,0 +1,12 @@
|
||||
name: Detects unused dependencies
|
||||
on:
|
||||
pull_request: { branches: "*" }
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
@@ -23,7 +23,6 @@ jobs:
|
||||
docs:
|
||||
name: All crates
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -52,8 +51,6 @@ jobs:
|
||||
# Keep in sync with xtask docs
|
||||
- name: Build documentation
|
||||
env:
|
||||
# Work around https://github.com/rust-lang/cargo/issues/10744
|
||||
CARGO_TARGET_APPLIES_TO_HOST: "true"
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs
|
||||
|
||||
@@ -7,6 +7,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.0.0
|
||||
- uses: actions/checkout@v4
|
||||
- name: Block Fixup Commit Merge
|
||||
uses: 13rac1/block-fixup-merge-action@v2.0.0
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
|
||||
name: Codecov
|
||||
name: Upload code coverage
|
||||
|
||||
on:
|
||||
# This workflow is triggered after every successfull execution
|
||||
# This workflow is triggered after every successful execution
|
||||
# of `coverage` workflow.
|
||||
workflow_run:
|
||||
workflows: ["Code Coverage"]
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
path: repo_root
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
os-name: 🐧
|
||||
cachekey-id: linux
|
||||
|
||||
- os: macos-12
|
||||
- os: macos-14
|
||||
os-name: 🍏
|
||||
cachekey-id: macos
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
# use the cache key calculated in the step above. Bit of an awkard
|
||||
# use the cache key calculated in the step above. Bit of an awkward
|
||||
# syntax
|
||||
key: |
|
||||
${{ steps.cachekey.outputs[format('cachekey-{0}', matrix.cachekey-id)] }}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -28,6 +28,7 @@ Abl = "Abl"
|
||||
Som = "Som"
|
||||
Ba = "Ba"
|
||||
Yur = "Yur" # as found in crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs
|
||||
TYE = "TYE" # as found in testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# Architecture
|
||||
|
||||
The SDK is split into multiple layers:
|
||||
|
||||
```
|
||||
WASM (external crate matrix-rust-sdk-crypto-wasm)
|
||||
/
|
||||
/ uniffi
|
||||
/ /
|
||||
/ bindings (matrix-sdk-ffi)
|
||||
crypto |
|
||||
bindings |
|
||||
| |
|
||||
| UI (matrix-sdk-ui)
|
||||
| \
|
||||
| \
|
||||
| main (matrix-sdk)
|
||||
| / /
|
||||
crypto /
|
||||
\ /
|
||||
store (matrix-sdk-base, + all the store impls)
|
||||
|
|
||||
common (matrix-sdk-common)
|
||||
```
|
||||
|
||||
Where the store implementations are `matrix-sdk-sqlite` and `matrix-sdk-indexeddb` as well as
|
||||
`MemoryStore` which is defined in `matrix-sdk-base`.
|
||||
|
||||
## `crates/matrix-sdk`
|
||||
|
||||
This is the main crate, and one that is expected to be used by most consumers. Notable data types
|
||||
include:
|
||||
|
||||
- the `Client`, which can run room-independent requests: logging in/out, creating rooms, running
|
||||
sync, etc.
|
||||
- the `Room`, which represents a room and its state (notably via the observable `RoomInfo`), and
|
||||
allows running queries that are room-specific, notably sending events.
|
||||
|
||||
## `crates/matrix-sdk-base`
|
||||
|
||||
A *sans I/O* crate to represent the base data types persisted in the SDK. No network or storage I/O
|
||||
happens in this crate, although it defines traits (`StateStore` and `EventCacheStore`) representing
|
||||
storage backends, as well as dummy in-memory implementations of these traits.
|
||||
|
||||
## `crates/matrix-sdk-common`
|
||||
|
||||
Common helpers used by most of the other crates; almost a leaf in the dependency tree of our own
|
||||
crates (the only crate it's using is test helpers).
|
||||
|
||||
## `crates/matrix-sdk-crypto`
|
||||
|
||||
A *sans I/O* implementation of a state machine that handles end-to-end encryption for Matrix
|
||||
clients. It defines a `CryptoStore` trait representing storage backends that will perform the
|
||||
actual storage I/O later, as well as a dummy in-memory implementation of this trait.
|
||||
|
||||
## `crates/matrix-sdk-indexeddb`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
indexeddb backend (for use in Web browsers, via WebAssembly).
|
||||
|
||||
## `crates/matrix-sdk-qrcode`
|
||||
|
||||
Implementation of QR codes for interactive verifications, used in the crypto crate.
|
||||
|
||||
## `crates/matrix-sdk-sqlite`
|
||||
|
||||
Implementations of `EventCacheStore`, `StateStore` and `CryptoStore` for a
|
||||
SQLite backend.
|
||||
|
||||
## `crates/matrix-sdk-store-encryption`
|
||||
|
||||
Low-level primitives for encrypting/decrypting/hashing values. Store implementations that
|
||||
implement encryption at rest can use those primitives.
|
||||
|
||||
## `crates/matrix-sdk-ui`
|
||||
|
||||
Very high-level primitives implementing the best practices and cutting-edge Matrix tech:
|
||||
|
||||
- `EncryptionSyncService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
everything related to crypto and E2EE for the current `Client`.
|
||||
- `RoomListService`: a specialized service running simplified sliding sync (MSC4186) for
|
||||
retrieving the list of current rooms, and exposing its entries.
|
||||
- `SyncService`: a wrapper for the two previous services, coordinating their running and shutting
|
||||
down.
|
||||
- `Timeline`: a high-level view for a `Room`'s timeline of events, grouping related events
|
||||
(aggregations) into single timeline items.
|
||||
|
||||
## `bindings/matrix-sdk-crypto-ffi/`
|
||||
|
||||
FFI bindings for the crypto crate, used in a Web browser context via WebAssembly. These use
|
||||
`wasm-bindgen` to generate the bindings. These bindings are used in Element Web and the legacy
|
||||
Element apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi/`
|
||||
|
||||
FFI bindings for important concepts in `matrix-sdk-ui` and `matrix-sdk`, generated with
|
||||
[UniFFI](https://github.com/mozilla/uniffi-rs) and to be used from other languages like
|
||||
Swift/Go/Kotlin. These bindings are used in the ElementX apps, as of 2024-11-07.
|
||||
|
||||
## `bindings/matrix-sdk-ffi-macros/`
|
||||
|
||||
Macros used in `bindings/matrix-sdk-ffi`.
|
||||
|
||||
## `testing/matrix-sdk-test/`
|
||||
|
||||
Common test helpers, used by all the other crates.
|
||||
|
||||
## `testing/matrix-sdk-test-macros/`
|
||||
|
||||
Implementation of the `#[async_test]` test macro.
|
||||
|
||||
## `testing/matrix-sdk-integration-testing/`
|
||||
|
||||
Fully-fledged integration tests that require spawning a Synapse instance to run. A docker-compose
|
||||
setup is provided to ease running the tests, and it is compatible for running with Podman too.
|
||||
|
||||
# Inspiration
|
||||
|
||||
This document has been inspired by the reading of this [blog post](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html).
|
||||
+175
-37
@@ -29,54 +29,187 @@ 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*.
|
||||
## Pull requests
|
||||
|
||||
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.
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
each commit should have a *good commit message*.
|
||||
|
||||
A *good commit message* should be composed of:
|
||||
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.
|
||||
|
||||
- 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.
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`,
|
||||
that is, just the branch name.)
|
||||
|
||||
Examples of commit messages that aren't so useful:
|
||||
# Writing changelog entries
|
||||
|
||||
- “add new method“
|
||||
- “enhance performance“
|
||||
- “fix receipts“
|
||||
We aim to maintain clear and informative changelogs that accurately reflect the
|
||||
changes in our project. This guide will help you write useful changelog entries
|
||||
using git-cliff, which fetches changelog entries from commit messages.
|
||||
|
||||
Examples of good commit messages:
|
||||
## Commit message format
|
||||
|
||||
- “ffi: Add new method `frobnicate_the_foos`”
|
||||
- “indexeddb: Break up the request inside `get_inbound_group_sessions`”
|
||||
- “read_receipts: Store receipts locally, fixing #12345”
|
||||
Commit messages should be formatted as Conventional Commits. In addition, some
|
||||
git trailers are supported and have special meaning (see below).
|
||||
|
||||
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.
|
||||
### Conventional commits
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
|
||||
branch name.)
|
||||
Conventional Commits are structured as follows:
|
||||
|
||||
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.
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
```
|
||||
|
||||
The type of changes which will be included in changelogs is one of the following:
|
||||
|
||||
* `feat`: A new feature
|
||||
* `fix`: A bug fix
|
||||
* `doc`: Documentation changes
|
||||
* `refactor`: Code refactoring
|
||||
* `perf`: Performance improvements
|
||||
* `ci`: Changes to CI configuration files and scripts
|
||||
|
||||
The scope is optional and can specify the area of the codebase affected (e.g.,
|
||||
olm, cipher).
|
||||
|
||||
### Changelog trailer
|
||||
|
||||
In addition to the Conventional Commit format, you can use the `Changelog` git
|
||||
trailer to specify the changelog message explicitly. When that trailer is
|
||||
present, its value will be used as the changelog entry instead of the commit's
|
||||
leading line. The `Breaking-Change` git trailer can be used in a similar manner
|
||||
if the changelog entry should be marked as a breaking change.
|
||||
|
||||
|
||||
#### Example commit message
|
||||
|
||||
```
|
||||
feat: Add a method to encode Ed25519 public keys to Base64
|
||||
|
||||
This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to
|
||||
stringify Ed25519 and thus present them to users. It's also commonly used when
|
||||
Ed25519 keys need to be inserted into JSON.
|
||||
|
||||
Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to
|
||||
stringify the Ed25519 public key.
|
||||
```
|
||||
|
||||
In this commit message, the content specified in the `Changelog` trailer will be
|
||||
used for the changelog entry.
|
||||
|
||||
Be careful to add at least one whitespace after new lines to create a paragraph.
|
||||
|
||||
### Security fixes
|
||||
|
||||
Commits addressing security vulnerabilities must include specific trailers for
|
||||
vulnerability metadata. These commits are required to include at least the
|
||||
`Security-Impact` trailer to indicate that the commit is a security fix.
|
||||
|
||||
Security issues have some additional git-trailers:
|
||||
|
||||
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
|
||||
* `CVE`: The CVE that was assigned to this issue.
|
||||
* `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
fix(crypto): Use a constant-time Base64 encoder for secret key material
|
||||
|
||||
This patch fixes a security issue around a side-channel vulnerability[1]
|
||||
when decoding secret key material using Base64.
|
||||
|
||||
In some circumstances an attacker can obtain information about secret
|
||||
secret key material via a controlled-channel and side-channel attack.
|
||||
|
||||
This patch avoids the side-channel by switching to the base64ct crate
|
||||
for the encoding, and more importantly, the decoding of secret key
|
||||
material.
|
||||
|
||||
Security-Impact: Low
|
||||
CVE: CVE-2024-40640
|
||||
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
|
||||
|
||||
Changelog: Use a constant-time Base64 encoder for secret key material
|
||||
to mitigate side-channel attacks leaking secret key material.
|
||||
```
|
||||
|
||||
## Review process
|
||||
|
||||
To streamline the review process and make it easier for maintainers to review
|
||||
your contributions, follow these basic rules:
|
||||
|
||||
1. Do not force push after a review has started. This helps maintainers track
|
||||
incremental changes without confusion and makes it easier to follow the
|
||||
evolution of the code.
|
||||
|
||||
2. Do not mix moves and refactoring with functional changes. Keep these in
|
||||
separate commits for clarity. This ensures that the purpose of each commit is
|
||||
clear and easy to review.
|
||||
|
||||
3. Each commit must compile. If commits don’t compile, git bisect becomes
|
||||
unusable, which hampers the debugging process and makes it harder to identify
|
||||
the source of issues.
|
||||
|
||||
4. Commits should only introduce test failures if they are proving that a bug
|
||||
exists. New features should never introduce test failures. Test failures
|
||||
should only be used to demonstrate existing bugs, not as part of adding new
|
||||
functionality.
|
||||
|
||||
5. Keep PRs on topic and small. Large PRs are harder to review and more prone to
|
||||
delays. Create small, focused commits that address a single topic. Use a
|
||||
combination of [git add] -p or git checkout -p to split changes into logical
|
||||
units. This makes your work easier to review and reduces the chance of
|
||||
introducing unrelated changes.
|
||||
|
||||
[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch
|
||||
[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch
|
||||
|
||||
### Addressing review comments using fixup commits
|
||||
|
||||
So you posted a PR and the maintainers aren't quite happy with it. Here are some
|
||||
guidelines to make the maintainers life easier and increase the chances that
|
||||
your PR will be reviewed swiftly.
|
||||
|
||||
1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup
|
||||
commits. These commits mark your changes as corrections of specific previous
|
||||
commits in the PR.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
git commit --fixup=<commit-hash>
|
||||
```
|
||||
|
||||
This command creates a new commit that refers to an existing one, making it
|
||||
easier to rebase and squash later while showing reviewers the history of fixes.
|
||||
For extra points, link to the fixup commit in the thread where the change was
|
||||
requested.
|
||||
|
||||
2. After all requested changes were addressed, feel free to re-request a review.
|
||||
People might not notice that all changes were addressed.
|
||||
|
||||
3. Once the PR has been approved, rebase your PR to squash all the fixup
|
||||
commits, the [autosquash] option can help with this.
|
||||
|
||||
```bash
|
||||
git rebase main --interactive --autosquash
|
||||
```
|
||||
|
||||
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
|
||||
[autosquash]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
adopted the same lightweight approach that the [Linux Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
|
||||
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO ([Developer Certificate of Origin](http://developercertificate.org/)).
|
||||
This is a simple declaration that you wrote the contribution or otherwise have the right
|
||||
to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
@@ -123,11 +256,6 @@ include the line in your commit or pull request comment:
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
@@ -138,3 +266,13 @@ on Git 2.17+ you can mass signoff using rebase:
|
||||
```
|
||||
git rebase --signoff origin/main
|
||||
```
|
||||
|
||||
## Tips for working on the `matrix-rust-sdk` with specific IDEs
|
||||
|
||||
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
|
||||
with all features enabled, causing an error in `matrix-sdk` ("only one of the
|
||||
features 'native-tls' or 'rustls-tls' can be enabled"). To work around this,
|
||||
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
|
||||
`native-tls` or `rustls-tls` feature definitions:
|
||||
|
||||

|
||||
|
||||
Generated
+738
-1276
File diff suppressed because it is too large
Load Diff
+53
-23
@@ -10,6 +10,9 @@ members = [
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
]
|
||||
exclude = [
|
||||
"testing/data",
|
||||
]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
@@ -28,23 +31,21 @@ async-trait = "0.1.60"
|
||||
as_variant = "1.2.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"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.7.0"
|
||||
futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
futures-util = "0.3.26"
|
||||
growable-bloom-filter = "2.1.0"
|
||||
http = "1.1.0"
|
||||
imbl = "2.0.0"
|
||||
imbl = "3.0.0"
|
||||
itertools = "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 = "e5a370f7e5fcebb0da6e4945e51c5fafba9aa5f0", features = [
|
||||
ruma = { version = "0.11.1", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -53,9 +54,12 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "e5a370f7e5fcebb0da6e4945e5
|
||||
"compat-encrypted-stickers",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3266",
|
||||
"unstable-msc4075"
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "e5a370f7e5fcebb0da6e4945e51c5fafba9aa5f0" }
|
||||
ruma-common = "0.14.1"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
@@ -63,27 +67,29 @@ sha2 = "0.10.8"
|
||||
similar-asserts = "1.5.0"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
|
||||
tokio = { version = "1.39.1", 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.27.1" }
|
||||
uniffi_bindgen = { version = "0.27.1" }
|
||||
tracing-subscriber = "0.3.18"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.0"
|
||||
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "4ef989c6a8eba0bc809e285a081c56320a9bbf1e" }
|
||||
vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
|
||||
wiremock = "0.6.0"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.7.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.7.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.7.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.7.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.7.0", default-features = false }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
@@ -115,10 +121,34 @@ opt-level = 3
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Releasing and publishing the SDK
|
||||
|
||||
While the release process can be handled manually, `cargo-release` has been
|
||||
configured to make it more convenient.
|
||||
|
||||
By default, [`cargo-release`](https://github.com/crate-ci/cargo-release) assumes
|
||||
that no pull request is required to cut a release. However, since the SDK
|
||||
repo is set up so that each push requires a pull request, we need to slightly
|
||||
deviate from the default workflow. A `cargo-xtask` has been created to make the
|
||||
process as smooth as possible.
|
||||
|
||||
The procedure is as follows:
|
||||
|
||||
1. Switch to a release branch:
|
||||
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
|
||||
2. Prepare the release. This will update the `README.md`, prepend the `CHANGELOG.md`
|
||||
file using `git cliff`, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
|
||||
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
|
||||
satisfied, push the branch and open a PR.
|
||||
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
|
||||
4. Pass the review and merge the branch as you would with any other branch.
|
||||
|
||||
5. Create tags for your new release, publish the release on crates.io and push
|
||||
the tags:
|
||||
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
@@ -13,12 +13,14 @@ matrix-sdk-base = { workspace = true }
|
||||
matrix-sdk-crypto = { workspace = true }
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true }
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite"] }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
wiremock = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
|
||||
@@ -34,3 +36,6 @@ harness = false
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -3,14 +3,11 @@ use std::{ops::Deref, sync::Arc};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
use matrix_sdk_test::response_from_file;
|
||||
use matrix_sdk_test::ruma_response_from_json;
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
api::client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
};
|
||||
@@ -28,25 +25,19 @@ fn alice_device_id() -> &'static DeviceId {
|
||||
fn keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
ruma_response_from_json(&data)
|
||||
}
|
||||
|
||||
fn keys_claim_response() -> claim_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_claim.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
claim_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the `/keys/upload` response")
|
||||
ruma_response_from_json(&data)
|
||||
}
|
||||
|
||||
fn huge_keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query_2000_members.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the `/keys/query` response")
|
||||
ruma_response_from_json(&data)
|
||||
}
|
||||
|
||||
pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::utils::IntoRawStateEventContent;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
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 matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
|
||||
owned_room_id,
|
||||
owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
user_id, OwnedUserId,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
use wiremock::{
|
||||
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
|
||||
Mock, MockServer, Request, ResponseTemplate,
|
||||
};
|
||||
|
||||
pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
@@ -62,7 +74,10 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
|
||||
let base_client = BaseClient::with_store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
);
|
||||
|
||||
runtime
|
||||
.block_on(base_client.set_session_meta(
|
||||
@@ -99,6 +114,135 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
const PINNED_EVENTS_COUNT: usize = 100;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id).sender(&sender_id);
|
||||
let (client, server) = runtime.block_on(logged_in_client_with_server());
|
||||
|
||||
let mut sync_response_builder = SyncResponseBuilder::new();
|
||||
let mut joined_room_builder =
|
||||
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
|
||||
|
||||
let pinned_event_ids: Vec<OwnedEventId> = (0..PINNED_EVENTS_COUNT)
|
||||
.map(|i| EventId::parse(format!("${i}")).expect("Invalid event id"))
|
||||
.collect();
|
||||
joined_room_builder = joined_room_builder.add_state_event(StateTestEvent::Custom(json!(
|
||||
{
|
||||
"content": {
|
||||
"pinned": pinned_event_ids
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 151393755,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.pinned_events",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
}
|
||||
)));
|
||||
let response_json =
|
||||
sync_response_builder.add_joined_room(joined_room_builder).build_json_sync_response();
|
||||
runtime.block_on(mock_sync(&server, response_json, None));
|
||||
|
||||
let sync_settings = SyncSettings::default();
|
||||
runtime.block_on(client.sync_once(sync_settings)).expect("Could not sync");
|
||||
runtime.block_on(server.reset());
|
||||
|
||||
runtime.block_on(
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"/_matrix/client/r0/rooms/.*/event/.*"))
|
||||
.respond_with(move |r: &Request| {
|
||||
let segments: Vec<&str> = r.url.path_segments().expect("Invalid path").collect();
|
||||
let event_id_str = segments[6];
|
||||
let event_id = EventId::parse(event_id_str).expect("Invalid event id in response");
|
||||
let event = f
|
||||
.text_msg(format!("Message {event_id_str}"))
|
||||
.event_id(&event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
ResponseTemplate::new(200)
|
||||
.set_delay(Duration::from_millis(50))
|
||||
.set_body_json(event.json())
|
||||
})
|
||||
.mount(&server),
|
||||
);
|
||||
|
||||
let room = client.get_room(&room_id).expect("Room not found");
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
let count = PINNED_EVENTS_COUNT;
|
||||
let name = format!("{count} pinned events");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(10);
|
||||
|
||||
let client = Arc::new(client);
|
||||
|
||||
{
|
||||
let client = client.clone();
|
||||
runtime.spawn_blocking(move || {
|
||||
client.event_cache().subscribe().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
// Reset cache so it always loads the events from the mocked endpoint
|
||||
client.event_cache().empty_immutable_cache().await;
|
||||
|
||||
let timeline = Timeline::builder(&room)
|
||||
.with_focus(TimelineFocus::PinnedEvents {
|
||||
max_events_to_load: 100,
|
||||
max_concurrent_requests: 10,
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
let (items, _) = timeline.subscribe().await;
|
||||
assert_eq!(items.len(), PINNED_EVENTS_COUNT + 1);
|
||||
timeline.clear().await;
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
runtime.block_on(server.reset());
|
||||
drop(server);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option<String>) {
|
||||
let mut mock_builder = Mock::given(method("GET"))
|
||||
.and(path("/_matrix/client/r0/sync"))
|
||||
.and(header("authorization", "Bearer 1234"));
|
||||
|
||||
if let Some(since) = since {
|
||||
mock_builder = mock_builder.and(query_param("since", since));
|
||||
} else {
|
||||
mock_builder = mock_builder.and(query_param_is_missing("since"));
|
||||
}
|
||||
|
||||
mock_builder
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
@@ -114,6 +258,6 @@ fn criterion() -> Criterion {
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
targets = receive_all_members_benchmark,
|
||||
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
|
||||
}
|
||||
criterion_main!(room);
|
||||
|
||||
@@ -69,7 +69,10 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(StoreConfig::new().state_store(store.clone()))
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
@@ -96,7 +99,10 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(StoreConfig::new().state_store(store.clone()))
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
.expect("Can't build client");
|
||||
|
||||
@@ -22,10 +22,11 @@ bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
futures-util = "0.3.28"
|
||||
futures-util = { workspace = true }
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
pbkdf2 = "0.12.2"
|
||||
rand = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
@@ -33,9 +34,9 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
# keep in sync with uniffi dependency in matrix-sdk-ffi, and uniffi_bindgen in ffi CI job
|
||||
uniffi = { workspace = true , features = ["cli"]}
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
vodozemac = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
@@ -66,3 +67,6 @@ assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -5,6 +5,9 @@ use vergen::EmitBuilder;
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-ffi/build.rs] too!
|
||||
fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
@@ -18,11 +21,11 @@ fn setup_x86_64_android_workaround() {
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "14.0.7";
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/"
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
|
||||
@@ -69,7 +69,7 @@ impl BackupRecoveryKey {
|
||||
const PBKDF_ROUNDS: i32 = 500_000;
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl BackupRecoveryKey {
|
||||
/// Create a new random [`BackupRecoveryKey`].
|
||||
#[allow(clippy::new_without_default)]
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Drop for DehydratedDevices {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl DehydratedDevices {
|
||||
pub fn create(&self) -> Result<Arc<DehydratedDevice>, DehydrationError> {
|
||||
let inner = self.runtime.block_on(self.inner.create())?;
|
||||
@@ -107,7 +107,7 @@ impl Drop for RehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RehydratedDevice {
|
||||
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
|
||||
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
|
||||
@@ -133,7 +133,7 @@ impl Drop for DehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub use error::{
|
||||
use js_int::UInt;
|
||||
pub use logger::{set_logger, Logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
|
||||
use matrix_sdk_crypto::{
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
@@ -196,7 +196,7 @@ impl From<anyhow::Error> for MigrationError {
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn migrate(
|
||||
data: MigrationData,
|
||||
path: String,
|
||||
@@ -338,10 +338,6 @@ async fn save_changes(
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
// The Sessions were created with incorrect device keys, so clear the cache
|
||||
// so that they'll get recreated with correct ones.
|
||||
store.clear_caches().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -363,7 +359,7 @@ async fn save_changes(
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn migrate_sessions(
|
||||
data: SessionMigrationData,
|
||||
path: String,
|
||||
@@ -434,7 +430,7 @@ fn collect_sessions(
|
||||
// the session cache after migration) so we don't need to worry about
|
||||
// signatures.
|
||||
let device_keys = DeviceKeys::new(
|
||||
user_id.clone(),
|
||||
user_id,
|
||||
device_id.clone(),
|
||||
Default::default(),
|
||||
BTreeMap::from([
|
||||
@@ -536,7 +532,7 @@ fn collect_sessions(
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data at
|
||||
/// rest in the Sqlite store. **Warning**, if no passphrase is given, the
|
||||
/// store and all its data will remain unencrypted.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn migrate_room_settings(
|
||||
room_settings: HashMap<String, RoomSettings>,
|
||||
path: String,
|
||||
@@ -562,7 +558,7 @@ pub fn migrate_room_settings(
|
||||
}
|
||||
|
||||
/// Callback that will be passed over the FFI to report progress
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ProgressListener {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -672,6 +668,9 @@ pub struct EncryptionSettings {
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
pub only_allow_trusted_devices: bool,
|
||||
/// Should fail to send when a verified user has unverified devices, or when
|
||||
/// a previously verified user replaces their identity.
|
||||
pub error_on_verified_user_problem: bool,
|
||||
}
|
||||
|
||||
impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
@@ -681,7 +680,10 @@ impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
sharing_strategy: CollectStrategy::new_device_based(v.only_allow_trusted_devices),
|
||||
sharing_strategy: CollectStrategy::DeviceBasedStrategy {
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
error_on_verified_user_problem: v.error_on_verified_user_problem,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -726,19 +728,24 @@ pub enum ShieldColor {
|
||||
#[allow(missing_docs)]
|
||||
pub struct ShieldState {
|
||||
color: ShieldColor,
|
||||
code: Option<ShieldStateCode>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
impl From<RustShieldState> for ShieldState {
|
||||
fn from(value: RustShieldState) -> Self {
|
||||
match value {
|
||||
RustShieldState::Red { message } => {
|
||||
Self { color: ShieldColor::Red, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::Grey { message } => {
|
||||
Self { color: ShieldColor::Grey, message: Some(message.to_owned()) }
|
||||
}
|
||||
RustShieldState::None => Self { color: ShieldColor::None, message: None },
|
||||
RustShieldState::Red { code, message } => Self {
|
||||
color: ShieldColor::Red,
|
||||
code: Some(code),
|
||||
message: Some(message.to_owned()),
|
||||
},
|
||||
RustShieldState::Grey { code, message } => Self {
|
||||
color: ShieldColor::Grey,
|
||||
code: Some(code),
|
||||
message: Some(message.to_owned()),
|
||||
},
|
||||
RustShieldState::None => Self { color: ShieldColor::None, code: None, message: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,7 +794,7 @@ pub struct BackupKeys {
|
||||
backup_version: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl BackupKeys {
|
||||
/// Get the recovery key that we're holding on to.
|
||||
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
|
||||
@@ -884,7 +891,7 @@ fn parse_user_id(user_id: &str) -> Result<OwnedUserId, CryptoStoreError> {
|
||||
ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn version_info() -> VersionInfo {
|
||||
VersionInfo {
|
||||
version: matrix_sdk_crypto::VERSION.to_owned(),
|
||||
@@ -908,16 +915,73 @@ pub struct VersionInfo {
|
||||
pub git_description: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn version() -> String {
|
||||
matrix_sdk_crypto::VERSION.to_owned()
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn vodozemac_version() -> String {
|
||||
vodozemac::VERSION.to_owned()
|
||||
}
|
||||
|
||||
/// The encryption component of PkEncryption support.
|
||||
///
|
||||
/// This struct can be created using a [`Curve25519PublicKey`] corresponding to
|
||||
/// a `PkDecryption` object, allowing messages to be encrypted for the
|
||||
/// associated decryption object.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct PkEncryption {
|
||||
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl PkEncryption {
|
||||
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
|
||||
/// encoded as Base64.
|
||||
///
|
||||
/// The public key should come from an existing `PkDecryption` object.
|
||||
/// Returns a `DecodeError` if the Curve25519 key could not be decoded
|
||||
/// correctly.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_base64(key: &str) -> Result<Arc<Self>, DecodeError> {
|
||||
let key = vodozemac::Curve25519PublicKey::from_base64(key)
|
||||
.map_err(matrix_sdk_crypto::backups::DecodeError::PublicKey)?;
|
||||
let inner = vodozemac::pk_encryption::PkEncryption::from_key(key);
|
||||
|
||||
Ok(Self { inner }.into())
|
||||
}
|
||||
|
||||
/// Encrypt a message using this [`PkEncryption`] object.
|
||||
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
|
||||
use vodozemac::base64_encode;
|
||||
|
||||
let message = self.inner.encrypt(plaintext.as_ref());
|
||||
|
||||
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
|
||||
|
||||
PkMessage {
|
||||
ciphertext: base64_encode(ciphertext),
|
||||
mac: base64_encode(mac),
|
||||
ephemeral_key: ephemeral_key.to_base64(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A message that was encrypted using a [`PkEncryption`] object.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct PkMessage {
|
||||
/// The ciphertext of the message.
|
||||
pub ciphertext: String,
|
||||
/// The message authentication code of the message.
|
||||
///
|
||||
/// *Warning*: This does not authenticate the ciphertext.
|
||||
pub mac: String,
|
||||
/// The ephemeral Curve25519 key of the message which was used to derive the
|
||||
/// individual message key.
|
||||
pub ephemeral_key: String,
|
||||
}
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait Logger: Send {
|
||||
/// Called every time the Rust side wants to post a log line.
|
||||
fn log(&self, log_line: String);
|
||||
@@ -42,7 +42,7 @@ pub struct LoggerWrapper {
|
||||
}
|
||||
|
||||
/// Set the logger that should be used to forward Rust logs over FFI.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn set_logger(logger: Box<dyn Logger>) {
|
||||
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ use matrix_sdk_crypto::{
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentities,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
@@ -37,11 +38,12 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, MessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
@@ -177,7 +179,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl OlmMachine {
|
||||
/// Create a new `OlmMachine`
|
||||
///
|
||||
@@ -285,10 +287,7 @@ impl OlmMachine {
|
||||
if let Some(identity) =
|
||||
self.runtime.block_on(self.inner.get_identity(&user_id, None))?
|
||||
{
|
||||
match identity {
|
||||
UserIdentities::Own(i) => i.is_verified(),
|
||||
UserIdentities::Other(i) => i.is_verified(),
|
||||
}
|
||||
identity.is_verified()
|
||||
} else {
|
||||
false
|
||||
},
|
||||
@@ -316,8 +315,8 @@ impl OlmMachine {
|
||||
|
||||
if let Some(user_identity) = user_identity {
|
||||
Ok(match user_identity {
|
||||
UserIdentities::Own(i) => self.runtime.block_on(i.verify())?,
|
||||
UserIdentities::Other(i) => self.runtime.block_on(i.verify())?,
|
||||
SdkUserIdentity::Own(i) => self.runtime.block_on(i.verify())?,
|
||||
SdkUserIdentity::Other(i) => self.runtime.block_on(i.verify())?,
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
@@ -530,11 +529,11 @@ impl OlmMachine {
|
||||
) -> Result<SyncChangesResult, CryptoStoreError> {
|
||||
let to_device: ToDevice = serde_json::from_str(&events)?;
|
||||
let device_changes: RumaDeviceLists = device_changes.into();
|
||||
let key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = key_counts
|
||||
let key_counts: BTreeMap<OneTimeKeyAlgorithm, UInt> = key_counts
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
DeviceKeyAlgorithm::from(k),
|
||||
OneTimeKeyAlgorithm::from(k),
|
||||
v.clamp(0, i32::MAX)
|
||||
.try_into()
|
||||
.expect("Couldn't convert key counts into an UInt"),
|
||||
@@ -542,8 +541,8 @@ impl OlmMachine {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect());
|
||||
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
|
||||
|
||||
let (to_device_events, room_key_infos) = self.runtime.block_on(
|
||||
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
@@ -863,12 +862,14 @@ impl OlmMachine {
|
||||
/// * `strict_shields` - If `true`, messages will be decorated with strict
|
||||
/// warnings (use `false` to match legacy behaviour where unsafe keys have
|
||||
/// lower severity warnings and unverified identities are not decorated).
|
||||
/// * `decryption_settings` - The setting for decrypting messages.
|
||||
pub fn decrypt_room_event(
|
||||
&self,
|
||||
event: String,
|
||||
room_id: String,
|
||||
handle_verification_events: bool,
|
||||
strict_shields: bool,
|
||||
decryption_settings: DecryptionSettings,
|
||||
) -> Result<DecryptedEvent, DecryptionError> {
|
||||
// Element Android wants only the content and the type and will create a
|
||||
// decrypted event with those two itself, this struct makes sure we
|
||||
@@ -884,10 +885,14 @@ impl OlmMachine {
|
||||
let event: Raw<_> = serde_json::from_str(&event)?;
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?;
|
||||
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(
|
||||
&event,
|
||||
&room_id,
|
||||
&decryption_settings,
|
||||
))?;
|
||||
|
||||
if handle_verification_events {
|
||||
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
|
||||
if let Ok(e) = decrypted.event.deserialize() {
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
|
||||
original_event,
|
||||
@@ -905,8 +910,7 @@ impl OlmMachine {
|
||||
}
|
||||
}
|
||||
|
||||
let encryption_info =
|
||||
decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info");
|
||||
let encryption_info = decrypted.encryption_info;
|
||||
|
||||
let event_json: Event<'_> = serde_json::from_str(decrypted.event.json().get())?;
|
||||
|
||||
@@ -1528,17 +1532,6 @@ 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 {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentities};
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
|
||||
|
||||
use crate::CryptoStoreError;
|
||||
|
||||
/// Enum representing cross signing identities of our own user or some other
|
||||
/// Enum representing cross signing identity of our own user or some other
|
||||
/// user.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum UserIdentity {
|
||||
@@ -18,6 +18,8 @@ pub enum UserIdentity {
|
||||
user_signing_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
/// The user identity of other users.
|
||||
Other {
|
||||
@@ -27,13 +29,15 @@ pub enum UserIdentity {
|
||||
master_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
/// True if this identity was verified at some point but is not anymore.
|
||||
has_verification_violation: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl UserIdentity {
|
||||
pub(crate) async fn from_rust(i: UserIdentities) -> Result<Self, CryptoStoreError> {
|
||||
pub(crate) async fn from_rust(i: SdkUserIdentity) -> Result<Self, CryptoStoreError> {
|
||||
Ok(match i {
|
||||
UserIdentities::Own(i) => {
|
||||
SdkUserIdentity::Own(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let user_signing: CrossSigningKey = i.user_signing_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
@@ -44,9 +48,10 @@ impl UserIdentity {
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
user_signing_key: serde_json::to_string(&user_signing)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
UserIdentities::Other(i) => {
|
||||
SdkUserIdentity::Other(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
|
||||
@@ -54,6 +59,7 @@ impl UserIdentity {
|
||||
user_id: i.user_id().to_string(),
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
has_verification_violation: i.has_verification_violation(),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{CryptoStoreError, OutgoingVerificationRequest, SignatureUploadReques
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a SAS
|
||||
/// verification.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SasListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -82,7 +82,7 @@ pub struct Verification {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Verification {
|
||||
/// Try to represent the `Verification` as an `Sas` verification object,
|
||||
/// returns `None` if the verification is not a `Sas` verification.
|
||||
@@ -112,7 +112,7 @@ pub struct Sas {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Sas {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -276,7 +276,7 @@ impl Sas {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a QrCode
|
||||
/// verification.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrCodeListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -328,7 +328,7 @@ pub struct QrCode {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCode {
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> String {
|
||||
@@ -522,7 +522,7 @@ pub struct ConfirmVerificationResult {
|
||||
|
||||
/// Listener that will be passed over the FFI to report changes to a
|
||||
/// verification request.
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationRequestListener: Send {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
@@ -562,7 +562,7 @@ pub struct VerificationRequest {
|
||||
pub(crate) runtime: Handle,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl VerificationRequest {
|
||||
/// The id of the other user that is participating in this verification
|
||||
/// request.
|
||||
@@ -752,7 +752,7 @@ impl VerificationRequest {
|
||||
RustVerificationRequestState::Ready {
|
||||
their_methods,
|
||||
our_methods,
|
||||
other_device_id: _,
|
||||
other_device_data: _,
|
||||
} => VerificationRequestState::Ready {
|
||||
their_methods: their_methods.iter().map(|m| m.to_string()).collect(),
|
||||
our_methods: our_methods.iter().map(|m| m.to_string()).collect(),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
description = "Helper macros to write FFI bindings"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-ffi-macros"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.86"
|
||||
quote = "1.0.18"
|
||||
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,14 @@
|
||||
[](https://travis-ci.org/matrix-org/matrix-rust-sdk)
|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
|
||||
# matrix-sdk-ffi-macros
|
||||
|
||||
Internal macros used for the FFI layer (bindings) of the Rust Matrix SDK.
|
||||
|
||||
**NOTE:** These are just macros that help build the matrix-rust-sdk bindings, you're probably
|
||||
interested in the main [rust-sdk](https://github.com/matrix-org/matrix-rust-sdk/) crate.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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 proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ImplItem, Item, TraitItem};
|
||||
|
||||
/// Attribute to specify the async runtime parameter for the `uniffi`
|
||||
/// export macros if there any `async fn`s in the input.
|
||||
#[proc_macro_attribute]
|
||||
pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let has_async_fn = |item| {
|
||||
if let Item::Fn(fun) = &item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
} else if let Item::Impl(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let ImplItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Item::Trait(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let TraitItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
};
|
||||
|
||||
let attr2 = proc_macro2::TokenStream::from(attr);
|
||||
let item2 = proc_macro2::TokenStream::from(item.clone());
|
||||
|
||||
let res = match syn::parse(item) {
|
||||
Ok(item) => match has_async_fn(item) {
|
||||
true => quote! { #[uniffi::export(async_runtime = "tokio", #attr2)] },
|
||||
false => quote! { #[uniffi::export(#attr2)] },
|
||||
},
|
||||
Err(e) => e.into_compile_error(),
|
||||
};
|
||||
|
||||
quote! {
|
||||
#res
|
||||
#item2
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -2,13 +2,34 @@
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
|
||||
`SendingFromUnverifiedDevice`. These indicate that your own device is not
|
||||
properly cross-signed, which is a requirement when using the identity-based
|
||||
strategy, and can only be returned when using the identity-based strategy.
|
||||
|
||||
In addition, the `VerifiedUserHasUnsignedDevice` and
|
||||
`VerifiedUserChangedIdentity` variants can be returned when using the
|
||||
identity-based strategy, in addition to when using the device-based strategy
|
||||
with `error_on_verified_user_problem` is set.
|
||||
|
||||
- `EventSendState` now has two additional variants: `VerifiedUserHasUnsignedDevice` and
|
||||
`VerifiedUserChangedIdentity`. These reflect problems with verified users in the room
|
||||
and as such can only be returned when the room key recipient strategy has
|
||||
`error_on_verified_user_problem` set.
|
||||
|
||||
- The `AuthenticationService` has been removed:
|
||||
- Instead of calling `configure_homeserver`, build your own client with the `serverNameOrHomeserverUrl` builder method to keep the same behaviour.
|
||||
- The parts of `AuthenticationError` related to discovery will be represented in the `ClientBuildError` returned when calling `build()`.
|
||||
- The remaining methods can be found on the built `Client`.
|
||||
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (or fails to present).
|
||||
- The rest of `AuthenticationError` is now found in the OidcError type.
|
||||
- Instead of calling `configure_homeserver`, build your own client with the `serverNameOrHomeserverUrl` builder
|
||||
method to keep the same behaviour.
|
||||
- The parts of `AuthenticationError` related to discovery will be represented in the `ClientBuildError` returned
|
||||
when calling `build()`.
|
||||
- The remaining methods can be found on the built `Client`.
|
||||
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (
|
||||
or fails to present).
|
||||
- The rest of `AuthenticationError` is now found in the OidcError type.
|
||||
- `OidcAuthenticationData` is now called `OidcAuthorizationData`.
|
||||
- The `get_element_call_required_permissions` function now requires the device_id.
|
||||
|
||||
Additions:
|
||||
|
||||
- Add `Encryption::get_user_identity` which returns `UserIdentity`
|
||||
- Add `ClientBuilder::room_key_recipient_strategy`
|
||||
|
||||
@@ -28,19 +28,19 @@ eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-util = { workspace = true }
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-core = { workspace = true }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
url = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
@@ -82,3 +82,6 @@ features = [
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -5,6 +5,9 @@ use vergen::EmitBuilder;
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-crypto-ffi/build.rs] too!
|
||||
fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
@@ -18,11 +21,11 @@ fn setup_x86_64_android_workaround() {
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "14.0.7";
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib64/clang/{clang_version}/lib/linux/"
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
oidc::{
|
||||
@@ -15,25 +19,27 @@ use matrix_sdk::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverLoginDetails {
|
||||
pub(crate) url: String,
|
||||
pub(crate) sliding_sync_proxy: Option<String>,
|
||||
pub(crate) sliding_sync_version: SlidingSyncVersion,
|
||||
pub(crate) supports_oidc_login: bool,
|
||||
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
|
||||
pub(crate) supports_password_login: bool,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl HomeserverLoginDetails {
|
||||
/// The URL of the currently configured homeserver.
|
||||
pub fn url(&self) -> String {
|
||||
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()
|
||||
/// The sliding sync version.
|
||||
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
|
||||
self.sliding_sync_version.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using OIDC.
|
||||
@@ -41,12 +47,66 @@ impl HomeserverLoginDetails {
|
||||
self.supports_oidc_login
|
||||
}
|
||||
|
||||
/// The prompts advertised by the authentication issuer for use in the login
|
||||
/// URL.
|
||||
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
|
||||
self.supported_oidc_prompts.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports the password login flow.
|
||||
pub fn supports_password_login(&self) -> bool {
|
||||
self.supports_password_login
|
||||
}
|
||||
}
|
||||
|
||||
/// An object encapsulating the SSO login flow
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SsoHandler {
|
||||
/// The wrapped Client.
|
||||
pub(crate) client: Arc<Client>,
|
||||
|
||||
/// The underlying URL for authentication.
|
||||
pub(crate) url: String,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SsoHandler {
|
||||
/// Returns the URL for starting SSO authentication. The URL should be
|
||||
/// opened in a web view. Once the web view succeeds, call `finish` with
|
||||
/// the callback URL.
|
||||
pub fn url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
/// Completes the SSO login process.
|
||||
pub async fn finish(&self, callback_url: String) -> Result<(), SsoError> {
|
||||
let auth = self.client.inner.matrix_auth();
|
||||
let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
let builder =
|
||||
auth.login_with_sso_callback(url).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SsoHandler {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.debug_struct("SsoHandler").field("url", &self.url).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum SsoError {
|
||||
#[error("The supplied callback URL used to complete SSO is invalid.")]
|
||||
CallbackUrlInvalid,
|
||||
#[error("Logging in with the token from the supplied callback URL failed.")]
|
||||
LoginWithTokenFailed,
|
||||
|
||||
#[error("An error occurred: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
/// The configuration to use when authenticating with OIDC.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OidcConfiguration {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
mem::ManuallyDrop,
|
||||
fmt::Debug,
|
||||
path::Path,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use matrix_sdk::{
|
||||
media::{MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSize},
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaThumbnailSettings,
|
||||
},
|
||||
oidc::{
|
||||
registrations::{ClientId, OidcRegistrations},
|
||||
requests::account_management::AccountManagementActionFull,
|
||||
@@ -16,12 +19,13 @@ use matrix_sdk::{
|
||||
registration::{
|
||||
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
|
||||
},
|
||||
requests::Prompt as SdkOidcPrompt,
|
||||
},
|
||||
OidcAuthorizationData, OidcSession,
|
||||
},
|
||||
reqwest::StatusCode,
|
||||
ruma::{
|
||||
api::client::{
|
||||
media::get_content_thumbnail::v3::Method,
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
session::get_login_types,
|
||||
@@ -36,7 +40,8 @@ use matrix_sdk::{
|
||||
serde::Raw,
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
@@ -44,31 +49,37 @@ use matrix_sdk_ui::notification_client::{
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::{alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo},
|
||||
api::client::{
|
||||
alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo,
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
|
||||
room::{join_rules::RoomJoinRulesEventContent, power_levels::RoomPowerLevelsEventContent},
|
||||
GlobalAccountDataEventType,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Value};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
|
||||
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
|
||||
use crate::{
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError},
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClient,
|
||||
notification_settings::NotificationSettings,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::AuthData,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utils::AsyncRuntimeDropped,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
@@ -132,25 +143,25 @@ impl From<PushFormat> for RumaPushFormat {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientDelegate: Sync + Send {
|
||||
fn did_receive_auth_error(&self, is_soft_logout: bool);
|
||||
fn did_refresh_tokens(&self);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientSessionDelegate: Sync + Send {
|
||||
fn retrieve_session_from_keychain(&self, user_id: String) -> Result<Session, ClientError>;
|
||||
fn save_session_in_keychain(&self, session: Session);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
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)]
|
||||
#[matrix_sdk_ffi_macros::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.
|
||||
@@ -174,58 +185,52 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Client {
|
||||
pub(crate) inner: ManuallyDrop<MatrixClient>,
|
||||
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
|
||||
delegate: RwLock<Option<Arc<dyn ClientDelegate>>>,
|
||||
session_verification_controller:
|
||||
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
|
||||
}
|
||||
|
||||
impl Drop for Client {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the inner OlmMachine must happen within a tokio context
|
||||
// because deadpool drops sqlite connections in the DB pool on tokio's
|
||||
// blocking threadpool to avoid blocking async worker threads.
|
||||
let _guard = RUNTIME.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn new(
|
||||
sdk_client: MatrixClient,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
) -> Result<Self, ClientError> {
|
||||
let session_verification_controller: Arc<
|
||||
tokio::sync::RwLock<Option<SessionVerificationController>>,
|
||||
> = Default::default();
|
||||
let ctrl = session_verification_controller.clone();
|
||||
let controller = session_verification_controller.clone();
|
||||
|
||||
sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
|
||||
if let Some(session_verification_controller) = &*ctrl.clone().read().await {
|
||||
if let Some(session_verification_controller) = &*controller.clone().read().await {
|
||||
session_verification_controller.process_to_device_message(ev).await;
|
||||
} else {
|
||||
debug!("received to-device message, but verification controller isn't ready");
|
||||
}
|
||||
});
|
||||
|
||||
let cross_process_store_locks_holder_name =
|
||||
sdk_client.cross_process_store_locks_holder_name().to_owned();
|
||||
|
||||
let client = Client {
|
||||
inner: ManuallyDrop::new(sdk_client),
|
||||
inner: AsyncRuntimeDropped::new(sdk_client),
|
||||
delegate: RwLock::new(None),
|
||||
session_verification_controller,
|
||||
};
|
||||
|
||||
if let Some(process_id) = cross_process_refresh_lock_id {
|
||||
if enable_oidc_refresh_lock {
|
||||
if session_delegate.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"missing session delegates when enabling the cross-process lock"
|
||||
))?;
|
||||
}
|
||||
client.inner.oidc().enable_cross_process_refresh_lock(process_id.clone()).await?;
|
||||
|
||||
client
|
||||
.inner
|
||||
.oidc()
|
||||
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(session_delegate) = session_delegate {
|
||||
@@ -252,18 +257,43 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Information about login options for the client's homeserver.
|
||||
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
|
||||
let supports_oidc_login = self.inner.oidc().fetch_authentication_issuer().await.is_ok();
|
||||
let oidc = self.inner.oidc();
|
||||
let (supports_oidc_login, supported_oidc_prompts) = match oidc
|
||||
.fetch_authentication_issuer()
|
||||
.await
|
||||
{
|
||||
Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await {
|
||||
Ok(metadata) => {
|
||||
let prompts = metadata
|
||||
.prompt_values_supported
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect());
|
||||
|
||||
(true, prompts)
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed to fetch OIDC provider metadata: {error}");
|
||||
(true, Default::default())
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Failed to fetch authentication issuer: {error}");
|
||||
(false, Default::default())
|
||||
}
|
||||
};
|
||||
|
||||
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
|
||||
let sliding_sync_proxy = self.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string());
|
||||
let sliding_sync_version = self.sliding_sync_version();
|
||||
|
||||
Arc::new(HomeserverLoginDetails {
|
||||
url: self.homeserver(),
|
||||
sliding_sync_proxy,
|
||||
sliding_sync_version,
|
||||
supports_oidc_login,
|
||||
supported_oidc_prompts,
|
||||
supports_password_login,
|
||||
})
|
||||
}
|
||||
@@ -287,13 +317,79 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Requests the URL needed for login in a web view using OIDC. Once the web
|
||||
/// Login using JWT
|
||||
/// This is an implementation of the custom_login https://docs.rs/matrix-sdk/latest/matrix_sdk/matrix_auth/struct.MatrixAuth.html#method.login_custom
|
||||
/// For more information on logging in with JWT: https://element-hq.github.io/synapse/latest/jwt.html
|
||||
pub async fn custom_login_with_jwt(
|
||||
&self,
|
||||
jwt: String,
|
||||
initial_device_name: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let data = json!({ "token": jwt }).as_object().unwrap().clone();
|
||||
|
||||
let mut builder = self.inner.matrix_auth().login_custom("org.matrix.login.jwt", data)?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Login using an email and password.
|
||||
pub async fn login_with_email(
|
||||
&self,
|
||||
email: String,
|
||||
password: String,
|
||||
initial_device_name: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut builder = self
|
||||
.inner
|
||||
.matrix_auth()
|
||||
.login_identifier(UserIdentifier::Email { address: email }, &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(())
|
||||
}
|
||||
|
||||
/// Returns a handler to start the SSO login process.
|
||||
pub(crate) async fn start_sso_login(
|
||||
self: &Arc<Self>,
|
||||
redirect_url: String,
|
||||
idp_id: Option<String>,
|
||||
) -> Result<Arc<SsoHandler>, SsoError> {
|
||||
let auth = self.inner.matrix_auth();
|
||||
let url = auth
|
||||
.get_sso_login_url(redirect_url.as_str(), idp_id.as_deref())
|
||||
.await
|
||||
.map_err(|e| SsoError::Generic { message: e.to_string() })?;
|
||||
Ok(Arc::new(SsoHandler { client: Arc::clone(self), url }))
|
||||
}
|
||||
|
||||
/// Requests the URL needed for opening a web view using OIDC. Once the web
|
||||
/// view has succeeded, call `login_with_oidc_callback` with the callback it
|
||||
/// returns. If a failure occurs and a callback isn't available, make sure
|
||||
/// to call `abort_oidc_login` to inform the client of this.
|
||||
pub async fn url_for_oidc_login(
|
||||
/// to call `abort_oidc_auth` to inform the client of this.
|
||||
pub async fn url_for_oidc(
|
||||
&self,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
prompt: OidcPrompt,
|
||||
) -> Result<Arc<OidcAuthorizationData>, OidcError> {
|
||||
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
|
||||
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
|
||||
@@ -314,14 +410,15 @@ impl Client {
|
||||
static_registrations,
|
||||
)?;
|
||||
|
||||
let data = self.inner.oidc().url_for_oidc_login(oidc_metadata, registrations).await?;
|
||||
let data =
|
||||
self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?;
|
||||
|
||||
Ok(Arc::new(data))
|
||||
}
|
||||
|
||||
/// Aborts an existing OIDC login operation that might have been cancelled,
|
||||
/// failed etc.
|
||||
pub async fn abort_oidc_login(&self, authorization_data: Arc<OidcAuthorizationData>) {
|
||||
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OidcAuthorizationData>) {
|
||||
self.inner.oidc().abort_authorization(&authorization_data.state).await;
|
||||
}
|
||||
|
||||
@@ -341,7 +438,7 @@ impl Client {
|
||||
pub async fn get_media_file(
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
body: Option<String>,
|
||||
filename: Option<String>,
|
||||
mime_type: String,
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
@@ -353,8 +450,8 @@ impl Client {
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequest { source, format: MediaFormat::File },
|
||||
body,
|
||||
&MediaRequestParameters { source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
@@ -366,17 +463,11 @@ impl Client {
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
pub async fn restore_session(&self, session: Session) -> Result<(), ClientError> {
|
||||
let sliding_sync_proxy = session.sliding_sync_proxy.clone();
|
||||
let sliding_sync_version = session.sliding_sync_version.clone();
|
||||
let auth_session: AuthSession = session.try_into()?;
|
||||
|
||||
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)
|
||||
.map_err(|error| ClientError::Generic { msg: error.to_string() })?;
|
||||
|
||||
self.inner.set_sliding_sync_proxy(Some(sliding_sync_proxy));
|
||||
}
|
||||
self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -407,7 +498,7 @@ impl Client {
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// Respawn tasks for rooms that had unsent events. At this point we've just
|
||||
// created the subscriber, so it'll be notified about errors.
|
||||
q.respawn_tasks_for_rooms_with_unsent_events().await;
|
||||
q.respawn_tasks_for_rooms_with_unsent_requests().await;
|
||||
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
@@ -427,6 +518,15 @@ impl Client {
|
||||
let http_client = self.inner.http_client();
|
||||
Ok(http_client.get(url).send().await?.text().await?)
|
||||
}
|
||||
|
||||
/// Empty the server version and unstable features cache.
|
||||
///
|
||||
/// Since the SDK caches server capabilities (versions and unstable
|
||||
/// features), it's possible to have a stale entry in the cache. This
|
||||
/// functions makes it possible to force reset it.
|
||||
pub async fn reset_server_capabilities(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_server_capabilities().await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -439,13 +539,6 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Whether or not the client's homeserver supports the password login flow.
|
||||
pub(crate) async fn supports_password_login(&self) -> anyhow::Result<bool> {
|
||||
let login_types = self.inner.matrix_auth().get_login_types().await?;
|
||||
@@ -457,8 +550,24 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// The sliding sync version.
|
||||
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
|
||||
self.inner.sliding_sync_version().into()
|
||||
}
|
||||
|
||||
/// Find all sliding sync versions that are available.
|
||||
///
|
||||
/// Be careful: This method may hit the store and will send new requests for
|
||||
/// each call. It can be costly to call it repeatedly.
|
||||
///
|
||||
/// If `.well-known` or `/versions` is unreachable, it will simply move
|
||||
/// potential sliding sync versions aside. No error will be reported.
|
||||
pub async fn available_sliding_sync_versions(&self) -> Vec<SlidingSyncVersion> {
|
||||
self.inner.available_sliding_sync_versions().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
pub fn set_delegate(
|
||||
self: Arc<Self>,
|
||||
delegate: Option<Box<dyn ClientDelegate>>,
|
||||
@@ -492,6 +601,10 @@ impl Client {
|
||||
&self,
|
||||
action: Option<AccountManagementAction>,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
|
||||
Ok(url) => Ok(url.map(|u| u.to_string())),
|
||||
Err(e) => {
|
||||
@@ -557,7 +670,7 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn create_room(&self, request: CreateRoomParameters) -> Result<String, ClientError> {
|
||||
let response = self.inner.create_room(request.into()).await?;
|
||||
let response = self.inner.create_room(request.try_into()?).await?;
|
||||
Ok(String::from(response.room_id()))
|
||||
}
|
||||
|
||||
@@ -590,7 +703,7 @@ impl Client {
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<String, ClientError> {
|
||||
let mime_type: mime::Mime = mime_type.parse().context("Parsing mime type")?;
|
||||
let request = self.inner.media().upload(&mime_type, data);
|
||||
let request = self.inner.media().upload(&mime_type, data, None);
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
@@ -612,10 +725,11 @@ impl Client {
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
|
||||
debug!(?source, "requesting media file");
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
|
||||
.get_media_content(&MediaRequestParameters { source, format: MediaFormat::File }, true)
|
||||
.await?)
|
||||
}
|
||||
|
||||
@@ -627,17 +741,17 @@ impl Client {
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
|
||||
debug!(source = ?media_source, width, height, "requesting media thumbnail");
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_content(
|
||||
&MediaRequest {
|
||||
&MediaRequestParameters {
|
||||
source,
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
method: Method::Scale,
|
||||
width: UInt::new(width).unwrap(),
|
||||
height: UInt::new(height).unwrap(),
|
||||
}),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
|
||||
UInt::new(width).unwrap(),
|
||||
UInt::new(height).unwrap(),
|
||||
)),
|
||||
},
|
||||
true,
|
||||
)
|
||||
@@ -734,6 +848,21 @@ impl Client {
|
||||
self.inner.homeserver().to_string()
|
||||
}
|
||||
|
||||
/// The URL of the server.
|
||||
///
|
||||
/// Not to be confused with the `Self::homeserver`. `server` is usually
|
||||
/// the server part in a user ID, e.g. with `@mnt_io:matrix.org`, here
|
||||
/// `matrix.org` is the server, whilst `matrix-client.matrix.org` is the
|
||||
/// homeserver (at the time of writing — 2024-08-28).
|
||||
///
|
||||
/// This value is optional depending on how the `Client` has been built.
|
||||
/// If it's been built from a homeserver URL directly, we don't know the
|
||||
/// server. However, if the `Client` has been built from a server URL or
|
||||
/// name, then the homeserver has been discovered, and we know both.
|
||||
pub fn server(&self) -> Option<String> {
|
||||
self.inner.server().map(ToString::to_string)
|
||||
}
|
||||
|
||||
pub fn rooms(&self) -> Vec<Arc<Room>> {
|
||||
self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
|
||||
}
|
||||
@@ -872,6 +1001,20 @@ impl Client {
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
}
|
||||
|
||||
/// Knock on a room to join it using its ID or alias.
|
||||
pub async fn knock(
|
||||
&self,
|
||||
room_id_or_alias: String,
|
||||
reason: Option<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(ServerName::parse).collect::<Result<Vec<_>, _>>()?;
|
||||
let room = self.inner.knock(room_id, reason, server_names).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
}
|
||||
|
||||
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
|
||||
Ok(self
|
||||
.inner
|
||||
@@ -894,10 +1037,21 @@ impl Client {
|
||||
pub async fn resolve_room_alias(
|
||||
&self,
|
||||
room_alias: String,
|
||||
) -> Result<ResolvedRoomAlias, ClientError> {
|
||||
) -> Result<Option<ResolvedRoomAlias>, ClientError> {
|
||||
let room_alias = RoomAliasId::parse(&room_alias)?;
|
||||
let response = self.inner.resolve_room_alias(&room_alias).await?;
|
||||
Ok(response.into())
|
||||
match self.inner.resolve_room_alias(&room_alias).await {
|
||||
Ok(response) => Ok(Some(response.into())),
|
||||
Err(HttpError::Reqwest(http_error)) => match http_error.status() {
|
||||
Some(StatusCode::NOT_FOUND) => Ok(None),
|
||||
_ => Err(http_error.into()),
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a room alias exists in the current homeserver.
|
||||
pub async fn room_alias_exists(&self, room_alias: String) -> Result<bool, ClientError> {
|
||||
self.resolve_room_alias(room_alias).await.map(|ret| ret.is_some())
|
||||
}
|
||||
|
||||
/// Given a room id, get the preview of a room, to interact with it.
|
||||
@@ -909,7 +1063,7 @@ impl Client {
|
||||
&self,
|
||||
room_id: String,
|
||||
via_servers: Vec<String>,
|
||||
) -> Result<RoomPreview, ClientError> {
|
||||
) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
let room_id = RoomId::parse(&room_id).context("room_id is not a valid room id")?;
|
||||
|
||||
let via_servers = via_servers
|
||||
@@ -922,16 +1076,16 @@ impl Client {
|
||||
// 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?;
|
||||
let room_preview = self.inner.get_room_preview(room_id.into(), via_servers).await?;
|
||||
|
||||
Ok(RoomPreview::from_sdk(sdk_room_preview))
|
||||
Ok(Arc::new(RoomPreview::new(self.inner.clone(), 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> {
|
||||
) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
let room_alias =
|
||||
RoomAliasId::parse(&room_alias).context("room_alias is not a valid room alias")?;
|
||||
|
||||
@@ -939,13 +1093,76 @@ impl Client {
|
||||
// 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?;
|
||||
let room_preview = self.inner.get_room_preview(room_alias.into(), Vec::new()).await?;
|
||||
|
||||
Ok(RoomPreview::from_sdk(sdk_room_preview))
|
||||
Ok(Arc::new(RoomPreview::new(self.inner.clone(), room_preview)))
|
||||
}
|
||||
|
||||
/// Waits until an at least partially synced room is received, and returns
|
||||
/// it.
|
||||
///
|
||||
/// **Note: this function will loop endlessly until either it finds the room
|
||||
/// or an externally set timeout happens.**
|
||||
pub async fn await_room_remote_echo(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
Ok(Arc::new(Room::new(self.inner.await_room_remote_echo(&room_id).await)))
|
||||
}
|
||||
|
||||
/// Lets the user know whether this is an `m.login.password` based
|
||||
/// auth and if the account can actually be deactivated
|
||||
pub fn can_deactivate_account(&self) -> bool {
|
||||
matches!(self.inner.auth_api(), Some(AuthApi::Matrix(_)))
|
||||
}
|
||||
|
||||
/// Deactivate this account definitively.
|
||||
/// Similarly to `encryption::reset_identity` this
|
||||
/// will only work with password-based authentication (`m.login.password`)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `auth_data` - This request uses the [User-Interactive Authentication
|
||||
/// API][uiaa]. The first request needs to set this to `None` and will
|
||||
/// always fail and the same request needs to be made but this time with
|
||||
/// some `auth_data` provided.
|
||||
pub async fn deactivate_account(
|
||||
&self,
|
||||
auth_data: Option<AuthData>,
|
||||
erase_data: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
if let Some(auth_data) = auth_data {
|
||||
_ = self.inner.account().deactivate(None, Some(auth_data.into()), erase_data).await?;
|
||||
} else {
|
||||
_ = self.inner.account().deactivate(None, None, erase_data).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if a room alias is not in use yet.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(true)` if the room alias is available.
|
||||
/// - `Ok(false)` if it's not (the resolve alias request returned a `404`
|
||||
/// status code).
|
||||
/// - An `Err` otherwise.
|
||||
pub async fn is_room_alias_available(&self, alias: String) -> Result<bool, ClientError> {
|
||||
let alias = RoomAliasId::parse(alias)?;
|
||||
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Creates a new room alias associated with the provided room id.
|
||||
pub async fn create_room_alias(
|
||||
&self,
|
||||
room_alias: String,
|
||||
room_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let room_alias = RoomAliasId::parse(room_alias)?;
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: Sync + Send {
|
||||
fn call(&self, ignored_user_ids: Vec<String>);
|
||||
}
|
||||
@@ -1055,9 +1272,9 @@ impl Client {
|
||||
let auth_api = client.auth_api().context("Missing authentication API")?;
|
||||
|
||||
let homeserver_url = client.homeserver().into();
|
||||
let sliding_sync_proxy = client.sliding_sync_proxy().map(|url| url.to_string());
|
||||
let sliding_sync_version = client.sliding_sync_version();
|
||||
|
||||
Session::new(auth_api, homeserver_url, sliding_sync_proxy)
|
||||
Session::new(auth_api, homeserver_url, sliding_sync_version.into())
|
||||
}
|
||||
|
||||
fn save_session(
|
||||
@@ -1166,16 +1383,23 @@ pub struct CreateRoomParameters {
|
||||
pub avatar: Option<String>,
|
||||
#[uniffi(default = None)]
|
||||
pub power_level_content_override: Option<PowerLevels>,
|
||||
#[uniffi(default = None)]
|
||||
pub join_rule_override: Option<JoinRule>,
|
||||
#[uniffi(default = None)]
|
||||
pub canonical_alias: Option<String>,
|
||||
}
|
||||
|
||||
impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
fn from(value: CreateRoomParameters) -> create_room::v3::Request {
|
||||
impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: CreateRoomParameters) -> Result<create_room::v3::Request, Self::Error> {
|
||||
let mut request = create_room::v3::Request::new();
|
||||
request.name = value.name;
|
||||
request.topic = value.topic;
|
||||
request.is_direct = value.is_direct;
|
||||
request.visibility = value.visibility.into();
|
||||
request.preset = Some(value.preset.into());
|
||||
request.room_alias_name = value.canonical_alias;
|
||||
request.invite = match value.invite {
|
||||
Some(invite) => invite
|
||||
.iter()
|
||||
@@ -1203,6 +1427,12 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
content.url = Some(url.into());
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
}
|
||||
|
||||
if let Some(join_rule_override) = value.join_rule_override {
|
||||
let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
}
|
||||
|
||||
request.initial_state = initial_state;
|
||||
|
||||
if let Some(power_levels) = value.power_level_content_override {
|
||||
@@ -1211,12 +1441,14 @@ impl From<CreateRoomParameters> for create_room::v3::Request {
|
||||
request.power_level_content_override = Some(power_levels);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize power levels, error: {e}");
|
||||
return Err(ClientError::Generic {
|
||||
msg: format!("Failed to serialize power levels, error: {e}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1284,15 +1516,15 @@ pub struct Session {
|
||||
/// Additional data for this session if OpenID Connect was used for
|
||||
/// authentication.
|
||||
pub oidc_data: Option<String>,
|
||||
/// The URL for the sliding sync proxy used for this session.
|
||||
pub sliding_sync_proxy: Option<String>,
|
||||
/// The sliding sync version used for this session.
|
||||
pub sliding_sync_version: SlidingSyncVersion,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
fn new(
|
||||
auth_api: AuthApi,
|
||||
homeserver_url: String,
|
||||
sliding_sync_proxy: Option<String>,
|
||||
sliding_sync_version: SlidingSyncVersion,
|
||||
) -> Result<Session, ClientError> {
|
||||
match auth_api {
|
||||
// Build the session from the regular Matrix Auth Session.
|
||||
@@ -1310,7 +1542,7 @@ impl Session {
|
||||
device_id: device_id.to_string(),
|
||||
homeserver_url,
|
||||
oidc_data: None,
|
||||
sliding_sync_proxy,
|
||||
sliding_sync_version,
|
||||
})
|
||||
}
|
||||
// Build the session from the OIDC UserSession.
|
||||
@@ -1347,7 +1579,7 @@ impl Session {
|
||||
device_id: device_id.to_string(),
|
||||
homeserver_url,
|
||||
oidc_data,
|
||||
sliding_sync_proxy,
|
||||
sliding_sync_version,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("Unknown authentication API").into()),
|
||||
@@ -1365,7 +1597,7 @@ impl TryFrom<Session> for AuthSession {
|
||||
device_id,
|
||||
homeserver_url: _,
|
||||
oidc_data,
|
||||
sliding_sync_proxy: _,
|
||||
sliding_sync_version: _,
|
||||
} = value;
|
||||
|
||||
if let Some(oidc_data) = oidc_data {
|
||||
@@ -1502,7 +1734,7 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn gen_transaction_id() -> String {
|
||||
TransactionId::new().to_string()
|
||||
}
|
||||
@@ -1520,7 +1752,7 @@ impl MediaFileHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaFileHandle {
|
||||
/// Get the media file's path.
|
||||
pub fn path(&self) -> Result<String, ClientError> {
|
||||
@@ -1553,3 +1785,187 @@ impl MediaFileHandle {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SlidingSyncVersion {
|
||||
None,
|
||||
Proxy { url: String },
|
||||
Native,
|
||||
}
|
||||
|
||||
impl From<SdkSlidingSyncVersion> for SlidingSyncVersion {
|
||||
fn from(value: SdkSlidingSyncVersion) -> Self {
|
||||
match value {
|
||||
SdkSlidingSyncVersion::None => Self::None,
|
||||
SdkSlidingSyncVersion::Proxy { url } => Self::Proxy { url: url.to_string() },
|
||||
SdkSlidingSyncVersion::Native => Self::Native,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: SlidingSyncVersion) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SlidingSyncVersion::None => Self::None,
|
||||
SlidingSyncVersion::Proxy { url } => Self::Proxy {
|
||||
url: Url::parse(&url).map_err(|e| ClientError::Generic { msg: e.to_string() })?,
|
||||
},
|
||||
SlidingSyncVersion::Native => Self::Native,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum OidcPrompt {
|
||||
/// The Authorization Server must not display any authentication or consent
|
||||
/// user interface pages.
|
||||
None,
|
||||
|
||||
/// The Authorization Server should prompt the End-User for
|
||||
/// reauthentication.
|
||||
Login,
|
||||
|
||||
/// The Authorization Server should prompt the End-User for consent before
|
||||
/// returning information to the Client.
|
||||
Consent,
|
||||
|
||||
/// The Authorization Server should prompt the End-User to select a user
|
||||
/// account.
|
||||
///
|
||||
/// This enables an End-User who has multiple accounts at the Authorization
|
||||
/// Server to select amongst the multiple accounts that they might have
|
||||
/// current sessions for.
|
||||
SelectAccount,
|
||||
|
||||
/// The Authorization Server should prompt the End-User to create a user
|
||||
/// account.
|
||||
///
|
||||
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
|
||||
Create,
|
||||
|
||||
/// An unknown value.
|
||||
Unknown { value: String },
|
||||
}
|
||||
|
||||
impl From<&SdkOidcPrompt> for OidcPrompt {
|
||||
fn from(value: &SdkOidcPrompt) -> Self {
|
||||
match value {
|
||||
SdkOidcPrompt::None => Self::None,
|
||||
SdkOidcPrompt::Login => Self::Login,
|
||||
SdkOidcPrompt::Consent => Self::Consent,
|
||||
SdkOidcPrompt::SelectAccount => Self::SelectAccount,
|
||||
SdkOidcPrompt::Create => Self::Create,
|
||||
SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() },
|
||||
_ => Self::Unknown { value: value.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcPrompt> for SdkOidcPrompt {
|
||||
fn from(value: OidcPrompt) -> Self {
|
||||
match value {
|
||||
OidcPrompt::None => Self::None,
|
||||
OidcPrompt::Login => Self::Login,
|
||||
OidcPrompt::Consent => Self::Consent,
|
||||
OidcPrompt::SelectAccount => Self::SelectAccount,
|
||||
OidcPrompt::Create => Self::Create,
|
||||
OidcPrompt::Unknown { value } => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The rule used for users wishing to join this room.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum JoinRule {
|
||||
/// Anyone can join the room without any prior action.
|
||||
Public,
|
||||
|
||||
/// A user who wishes to join the room must first receive an invite to the
|
||||
/// room from someone already inside of the room.
|
||||
Invite,
|
||||
|
||||
/// Users can join the room if they are invited, or they can request an
|
||||
/// invite to the room.
|
||||
///
|
||||
/// They can be allowed (invited) or denied (kicked/banned) access.
|
||||
Knock,
|
||||
|
||||
/// Reserved but not yet implemented by the Matrix specification.
|
||||
Private,
|
||||
|
||||
/// Users can join the room if they are invited, or if they meet any of the
|
||||
/// conditions described in a set of [`AllowRule`]s.
|
||||
Restricted { rules: Vec<AllowRule> },
|
||||
|
||||
/// Users can join the room if they are invited, or if they meet any of the
|
||||
/// conditions described in a set of [`AllowRule`]s, or they can request
|
||||
/// an invite to the room.
|
||||
KnockRestricted { rules: Vec<AllowRule> },
|
||||
|
||||
/// A custom join rule, up for interpretation by the consumer.
|
||||
Custom {
|
||||
/// The string representation for this custom rule.
|
||||
repr: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// An allow rule which defines a condition that allows joining a room.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum AllowRule {
|
||||
/// Only a member of the `room_id` Room can join the one this rule is used
|
||||
/// in.
|
||||
RoomMembership { room_id: String },
|
||||
}
|
||||
|
||||
impl TryFrom<JoinRule> for ruma::events::room::join_rules::JoinRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: JoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
JoinRule::Public => Ok(Self::Public),
|
||||
JoinRule::Invite => Ok(Self::Invite),
|
||||
JoinRule::Knock => Ok(Self::Knock),
|
||||
JoinRule::Private => Ok(Self::Private),
|
||||
JoinRule::Restricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::KnockRestricted { rules } => {
|
||||
let rules = allow_rules_from(rules)?;
|
||||
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_rules_from(
|
||||
value: Vec<AllowRule>,
|
||||
) -> Result<Vec<ruma::events::room::join_rules::AllowRule>, ClientError> {
|
||||
let mut ret = Vec::with_capacity(value.len());
|
||||
for rule in value {
|
||||
let rule: Result<ruma::events::room::join_rules::AllowRule, ClientError> = rule.try_into();
|
||||
match rule {
|
||||
Ok(rule) => ret.push(rule),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
impl TryFrom<AllowRule> for ruma::events::room::join_rules::AllowRule {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: AllowRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
AllowRule::RoomMembership { room_id } => {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new(
|
||||
room_id,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
crypto::{
|
||||
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
CollectStrategy, TrustRequirement,
|
||||
},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
reqwest::Certificate,
|
||||
ruma::{
|
||||
api::{error::UnknownVersionError, MatrixVersion},
|
||||
ServerName, UserId,
|
||||
ruma::{ServerName, UserId},
|
||||
sliding_sync::{
|
||||
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError,
|
||||
@@ -43,7 +47,7 @@ pub struct QrCodeData {
|
||||
inner: qrcode::QrCodeData,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl QrCodeData {
|
||||
/// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
|
||||
///
|
||||
@@ -79,7 +83,7 @@ pub enum HumanQrLoginError {
|
||||
Declined,
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown,
|
||||
#[error("The homeserver doesn't provide a sliding sync proxy in its configuration.")]
|
||||
#[error("The homeserver doesn't provide sliding sync in its configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
OidcMetadataInvalid,
|
||||
@@ -155,7 +159,7 @@ pub enum QrLoginProgress {
|
||||
Done,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait QrLoginProgressListener: Sync + Send {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
@@ -191,12 +195,13 @@ pub enum ClientBuildError {
|
||||
WellKnownLookupFailed(RumaApiError),
|
||||
#[error(transparent)]
|
||||
WellKnownDeserializationError(DeserializationError),
|
||||
#[error("The homeserver doesn't provide a trusted sliding sync proxy in its well-known configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
|
||||
#[error(transparent)]
|
||||
#[allow(dead_code)] // rustc's drunk, this is used
|
||||
SlidingSync(MatrixSlidingSyncError),
|
||||
#[error(transparent)]
|
||||
SlidingSyncVersion(VersionBuilderError),
|
||||
#[error(transparent)]
|
||||
Sdk(MatrixClientBuildError),
|
||||
|
||||
#[error("Failed to build the client: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
@@ -212,10 +217,9 @@ impl From<MatrixClientBuildError> for ClientBuildError {
|
||||
MatrixClientBuildError::AutoDiscovery(FromHttpResponseError::Deserialization(e)) => {
|
||||
ClientBuildError::WellKnownDeserializationError(e)
|
||||
}
|
||||
MatrixClientBuildError::SlidingSyncNotAvailable => {
|
||||
ClientBuildError::SlidingSyncNotAvailable
|
||||
MatrixClientBuildError::SlidingSyncVersion(e) => {
|
||||
ClientBuildError::SlidingSyncVersion(e)
|
||||
}
|
||||
|
||||
_ => ClientBuildError::Sdk(e),
|
||||
}
|
||||
}
|
||||
@@ -247,59 +251,69 @@ impl From<ClientError> for ClientBuildError {
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ClientBuilder {
|
||||
session_path: Option<String>,
|
||||
session_paths: Option<SessionPaths>,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
server_versions: Option<Vec<String>>,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
user_agent: Option<String>,
|
||||
requires_sliding_sync: bool,
|
||||
sliding_sync_proxy: Option<String>,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder,
|
||||
proxy: Option<String>,
|
||||
disable_ssl_verification: bool,
|
||||
disable_automatic_token_refresh: bool,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
cross_process_store_locks_holder_name: Option<String>,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
disable_built_in_root_certificates: bool,
|
||||
encryption_settings: EncryptionSettings,
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
request_config: Option<RequestConfig>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
session_path: None,
|
||||
session_paths: None,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
server_versions: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
requires_sliding_sync: false,
|
||||
sliding_sync_proxy: None,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
cross_process_refresh_lock_id: None,
|
||||
cross_process_store_locks_holder_name: None,
|
||||
enable_oidc_refresh_lock: false,
|
||||
session_delegate: None,
|
||||
additional_root_certificates: Default::default(),
|
||||
disable_built_in_root_certificates: false,
|
||||
encryption_settings: EncryptionSettings {
|
||||
auto_enable_cross_signing: false,
|
||||
backup_download_strategy:
|
||||
matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
auto_enable_backups: false,
|
||||
},
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
request_config: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable_cross_process_refresh_lock(
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
self: Arc<Self>,
|
||||
process_id: String,
|
||||
session_delegate: Box<dyn ClientSessionDelegate>,
|
||||
holder_name: String,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.cross_process_refresh_lock_id = Some(process_id);
|
||||
builder.session_delegate = Some(session_delegate.into());
|
||||
builder.cross_process_store_locks_holder_name = Some(holder_name);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.enable_oidc_refresh_lock = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -312,14 +326,15 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Sets the paths that the client will use to store its data and caches.
|
||||
/// Both paths **must** be unique per session as the SDK stores aren't
|
||||
/// capable of handling multiple users, however it is valid to use the
|
||||
/// same path for both stores on a single session.
|
||||
///
|
||||
/// Leaving this unset tells the client to use an in-memory data store.
|
||||
pub fn session_path(self: Arc<Self>, path: String) -> Arc<Self> {
|
||||
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_path = Some(path);
|
||||
builder.session_paths = Some(SessionPaths { data_path, cache_path });
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -329,12 +344,6 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn server_versions(self: Arc<Self>, versions: Vec<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.server_versions = Some(versions);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name));
|
||||
@@ -365,15 +374,12 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn requires_sliding_sync(self: Arc<Self>) -> Arc<Self> {
|
||||
pub fn sliding_sync_version_builder(
|
||||
self: Arc<Self>,
|
||||
version_builder: SlidingSyncVersionBuilder,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.requires_sliding_sync = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn sliding_sync_proxy(self: Arc<Self>, sliding_sync_proxy: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.sliding_sync_proxy = sliding_sync_proxy;
|
||||
builder.sliding_sync_version_builder = version_builder;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -405,6 +411,15 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Don't trust any system root certificates, only trust the certificates
|
||||
/// provided through
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn auto_enable_cross_signing(
|
||||
self: Arc<Self>,
|
||||
auto_enable_cross_signing: bool,
|
||||
@@ -434,20 +449,58 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the strategy to be used for picking recipient devices when sending
|
||||
/// an encrypted message.
|
||||
pub fn room_key_recipient_strategy(self: Arc<Self>, strategy: CollectStrategy) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.room_key_recipient_strategy = strategy;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the trust requirement to be used when decrypting events.
|
||||
pub fn room_decryption_trust_requirement(
|
||||
self: Arc<Self>,
|
||||
trust_requirement: TrustRequirement,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.decryption_trust_requirement = trust_requirement;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Add a default request config to this client.
|
||||
pub fn request_config(self: Arc<Self>, config: RequestConfig) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.request_config = Some(config);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
|
||||
if let Some(session_path) = &builder.session_path {
|
||||
let data_path = PathBuf::from(session_path);
|
||||
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
|
||||
inner_builder =
|
||||
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
|
||||
}
|
||||
|
||||
if let Some(session_paths) = &builder.session_paths {
|
||||
let data_path = PathBuf::from(&session_paths.data_path);
|
||||
let cache_path = PathBuf::from(&session_paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
"Creating directory and using it as the store path."
|
||||
cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(&data_path)?;
|
||||
inner_builder = inner_builder.sqlite_store(&data_path, builder.passphrase.as_deref());
|
||||
fs::create_dir_all(&cache_path)?;
|
||||
|
||||
inner_builder = inner_builder.sqlite_store_with_cache_path(
|
||||
&data_path,
|
||||
&cache_path,
|
||||
builder.passphrase.as_deref(),
|
||||
);
|
||||
} else {
|
||||
debug!("Not using a store path.");
|
||||
}
|
||||
@@ -479,19 +532,27 @@ impl ClientBuilder {
|
||||
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:?}"),
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
certificates.push(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
|
||||
if builder.disable_built_in_root_certificates {
|
||||
inner_builder = inner_builder.disable_built_in_root_certificates();
|
||||
}
|
||||
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
@@ -508,48 +569,64 @@ impl ClientBuilder {
|
||||
inner_builder = inner_builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
if let Some(server_versions) = builder.server_versions {
|
||||
inner_builder = inner_builder.server_versions(
|
||||
server_versions
|
||||
.iter()
|
||||
.map(|s| MatrixVersion::try_from(s.as_str()))
|
||||
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()
|
||||
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
|
||||
);
|
||||
inner_builder = inner_builder
|
||||
.with_encryption_settings(builder.encryption_settings)
|
||||
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy)
|
||||
.with_decryption_trust_requirement(builder.decryption_trust_requirement);
|
||||
|
||||
match builder.sliding_sync_version_builder {
|
||||
SlidingSyncVersionBuilder::None => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
|
||||
}
|
||||
SlidingSyncVersionBuilder::Proxy { url } => {
|
||||
inner_builder = inner_builder.sliding_sync_version_builder(
|
||||
MatrixSlidingSyncVersionBuilder::Proxy {
|
||||
url: Url::parse(&url)
|
||||
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
|
||||
},
|
||||
)
|
||||
}
|
||||
SlidingSyncVersionBuilder::Native => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
|
||||
}
|
||||
SlidingSyncVersionBuilder::DiscoverProxy => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverProxy)
|
||||
}
|
||||
SlidingSyncVersionBuilder::DiscoverNative => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
|
||||
}
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.with_encryption_settings(builder.encryption_settings);
|
||||
|
||||
if builder.requires_sliding_sync {
|
||||
inner_builder = inner_builder.requires_sliding_sync();
|
||||
if let Some(config) = builder.request_config {
|
||||
let mut updated_config = matrix_sdk::config::RequestConfig::default();
|
||||
if let Some(retry_limit) = config.retry_limit {
|
||||
updated_config = updated_config.retry_limit(retry_limit);
|
||||
}
|
||||
if let Some(timeout) = config.timeout {
|
||||
updated_config = updated_config.timeout(Duration::from_millis(timeout));
|
||||
}
|
||||
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
|
||||
if max_concurrent_requests > 0 {
|
||||
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
|
||||
max_concurrent_requests as usize,
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(retry_timeout) = config.retry_timeout {
|
||||
updated_config = updated_config.retry_timeout(Duration::from_millis(retry_timeout));
|
||||
}
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
}
|
||||
|
||||
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
|
||||
// `.well-known` file is filled as expected).
|
||||
//
|
||||
// If `builder.sliding_sync_proxy` contains `Some(_)`, it means one wants to
|
||||
// overwrite this value. It would be an error to call
|
||||
// `sdk_client.set_sliding_sync_proxy()` with `None`, as it would erase the
|
||||
// `sliding_sync_proxy` if any, and it's not the intended behavior.
|
||||
//
|
||||
// So let's call `sdk_client.set_sliding_sync_proxy()` if and only if there is
|
||||
// `Some(_)` value in `builder.sliding_sync_proxy`. That's really important: It
|
||||
// might not break an existing app session, but it is likely to break a new
|
||||
// session, which not immediate to detect if there is no test.
|
||||
if let Some(sliding_sync_proxy) = builder.sliding_sync_proxy {
|
||||
sdk_client.set_sliding_sync_proxy(Some(Url::parse(&sliding_sync_proxy)?));
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(
|
||||
sdk_client,
|
||||
builder.cross_process_refresh_lock_id,
|
||||
builder.session_delegate,
|
||||
)
|
||||
.await?,
|
||||
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -578,7 +655,7 @@ impl ClientBuilder {
|
||||
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
|
||||
|
||||
let client = builder.build().await.map_err(|e| match e {
|
||||
ClientBuildError::SlidingSyncNotAvailable => HumanQrLoginError::SlidingSyncNotAvailable,
|
||||
ClientBuildError::SlidingSync(_) => HumanQrLoginError::SlidingSyncNotAvailable,
|
||||
_ => {
|
||||
error!("Couldn't build the client {e:?}");
|
||||
HumanQrLoginError::Unknown
|
||||
@@ -606,3 +683,35 @@ impl ClientBuilder {
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// The store paths the client will use when built.
|
||||
struct SessionPaths {
|
||||
/// The path that the client will use to store its data.
|
||||
data_path: String,
|
||||
/// The path that the client will use to store its caches. This path can be
|
||||
/// the same as the data path if you prefer to keep everything in one place.
|
||||
cache_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
/// The config to use for HTTP requests by default in this client.
|
||||
pub struct RequestConfig {
|
||||
/// Max number of retries.
|
||||
retry_limit: Option<u64>,
|
||||
/// Timeout for a request in milliseconds.
|
||||
timeout: Option<u64>,
|
||||
/// Max number of concurrent requests. No value means no limits.
|
||||
max_concurrent_requests: Option<u64>,
|
||||
/// Base delay between retries.
|
||||
retry_timeout: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SlidingSyncVersionBuilder {
|
||||
None,
|
||||
Proxy { url: String },
|
||||
Native,
|
||||
DiscoverProxy,
|
||||
DiscoverNative,
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ pub struct ElementCallWellKnown {
|
||||
/// Element specific well-known settings
|
||||
#[derive(Deserialize, uniffi::Record)]
|
||||
pub struct ElementWellKnown {
|
||||
call: ElementCallWellKnown,
|
||||
call: Option<ElementCallWellKnown>,
|
||||
registration_helper_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Helper function to parse a string into a ElementWellKnown struct
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn make_element_well_known(string: String) -> Result<ElementWellKnown, ClientError> {
|
||||
serde_json::from_str(&string).map_err(ClientError::new)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ use matrix_sdk::{
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{client::Client, error::ClientError, task_handle::TaskHandle};
|
||||
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
@@ -23,22 +24,22 @@ pub struct Encryption {
|
||||
pub(crate) _client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait BackupSteadyStateListener: Sync + Send {
|
||||
fn on_update(&self, status: BackupUploadState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RecoveryStateListener: Sync + Send {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait VerificationStateListener: Sync + Send {
|
||||
fn on_update(&self, status: VerificationState);
|
||||
}
|
||||
@@ -162,7 +163,7 @@ impl From<recovery::RecoveryState> for RecoveryState {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait EnableRecoveryProgressListener: Sync + Send {
|
||||
fn on_update(&self, status: EnableRecoveryProgress);
|
||||
}
|
||||
@@ -212,7 +213,7 @@ impl From<encryption::VerificationState> for VerificationState {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Encryption {
|
||||
/// Get the public ed25519 key of our own device. This is usually what is
|
||||
/// called the fingerprint of the device.
|
||||
@@ -315,6 +316,7 @@ impl Encryption {
|
||||
pub async fn enable_recovery(
|
||||
&self,
|
||||
wait_for_backups_to_upload: bool,
|
||||
mut passphrase: Option<String>,
|
||||
progress_listener: Box<dyn EnableRecoveryProgressListener>,
|
||||
) -> Result<String> {
|
||||
let recovery = self.inner.recovery();
|
||||
@@ -325,6 +327,12 @@ impl Encryption {
|
||||
recovery.enable()
|
||||
};
|
||||
|
||||
let enable = if let Some(passphrase) = &passphrase {
|
||||
enable.with_passphrase(passphrase)
|
||||
} else {
|
||||
enable
|
||||
};
|
||||
|
||||
let mut progress_stream = enable.subscribe_to_progress();
|
||||
|
||||
let task = RUNTIME.spawn(async move {
|
||||
@@ -337,6 +345,7 @@ impl Encryption {
|
||||
let ret = enable.await?;
|
||||
|
||||
task.abort();
|
||||
passphrase.zeroize();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
@@ -357,6 +366,22 @@ impl Encryption {
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
/// Completely reset the current user's crypto identity: reset the cross
|
||||
/// signing keys, delete the existing backup and recovery key.
|
||||
pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
|
||||
if let Some(reset_handle) = self
|
||||
.inner
|
||||
.recovery()
|
||||
.reset_identity()
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })?
|
||||
{
|
||||
return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
|
||||
let result = self.inner.recovery().recover(&recovery_key).await;
|
||||
|
||||
@@ -374,6 +399,7 @@ impl Encryption {
|
||||
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());
|
||||
@@ -386,4 +412,155 @@ impl Encryption {
|
||||
pub async fn wait_for_e2ee_initialization_tasks(&self) {
|
||||
self.inner.wait_for_e2ee_initialization_tasks().await;
|
||||
}
|
||||
|
||||
/// Get the E2EE identity of a user.
|
||||
///
|
||||
/// This method always tries to fetch the identity from the store, which we
|
||||
/// only have if the user is tracked, meaning that we are both members
|
||||
/// of the same encrypted room. If no user is found locally, a request will
|
||||
/// be made to the homeserver.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user that the identity belongs to.
|
||||
///
|
||||
/// Returns a `UserIdentity` if one is found. Returns an error if there
|
||||
/// was an issue with the crypto store or with the request to the
|
||||
/// homeserver.
|
||||
///
|
||||
/// This will always return `None` if the client hasn't been logged in.
|
||||
pub async fn user_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
|
||||
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
|
||||
Ok(Some(identity)) => {
|
||||
return Ok(Some(Arc::new(UserIdentity { inner: identity })));
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("No identity found in the store.");
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed fetching identity from the store: {}", error);
|
||||
}
|
||||
};
|
||||
|
||||
info!("Requesting identity from the server.");
|
||||
|
||||
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
|
||||
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
|
||||
}
|
||||
}
|
||||
|
||||
/// The E2EE identity of a user.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct UserIdentity {
|
||||
inner: matrix_sdk::encryption::identities::UserIdentity,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl UserIdentity {
|
||||
/// Remember this identity, ensuring it does not result in a pin violation.
|
||||
///
|
||||
/// When we first see a user, we assume their cryptographic identity has not
|
||||
/// been tampered with by the homeserver or another entity with
|
||||
/// man-in-the-middle capabilities. We remember this identity and call this
|
||||
/// action "pinning".
|
||||
///
|
||||
/// If the identity presented for the user changes later on, the newly
|
||||
/// presented identity is considered to be in "pin violation". This
|
||||
/// method explicitly accepts the new identity, allowing it to replace
|
||||
/// the previously pinned one and bringing it out of pin violation.
|
||||
///
|
||||
/// UIs should display a warning to the user when encountering an identity
|
||||
/// which is not verified and is in pin violation.
|
||||
pub(crate) async fn pin(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.pin().await?)
|
||||
}
|
||||
|
||||
/// Get the public part of the Master key of this user identity.
|
||||
///
|
||||
/// The public part of the Master key is usually used to uniquely identify
|
||||
/// the identity.
|
||||
///
|
||||
/// Returns None if the master key does not actually contain any keys.
|
||||
pub(crate) fn master_key(&self) -> Option<String> {
|
||||
self.inner.master_key().get_first_key().map(|k| k.to_base64())
|
||||
}
|
||||
|
||||
/// Is the user identity considered to be verified.
|
||||
///
|
||||
/// If the identity belongs to another user, our own user identity needs to
|
||||
/// be verified as well for the identity to be considered to be verified.
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct IdentityResetHandle {
|
||||
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl IdentityResetHandle {
|
||||
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
|
||||
/// process is using.
|
||||
pub fn auth_type(&self) -> CrossSigningResetAuthType {
|
||||
self.inner.auth_type().into()
|
||||
}
|
||||
|
||||
/// This method starts the identity reset process and
|
||||
/// will go through the following steps:
|
||||
///
|
||||
/// 1. Disable backing up room keys and delete the active backup
|
||||
/// 2. Disable recovery and delete secret storage
|
||||
/// 3. Go through the cross-signing key reset flow
|
||||
/// 4. Finally, re-enable key backups only if they were enabled before
|
||||
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
|
||||
if let Some(auth) = auth {
|
||||
self.inner
|
||||
.reset(Some(auth.into()))
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
} else {
|
||||
self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cancel(&self) {
|
||||
self.inner.cancel().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum CrossSigningResetAuthType {
|
||||
/// The homeserver requires user-interactive authentication.
|
||||
Uiaa,
|
||||
// /// OIDC is used for authentication and the user needs to open a URL to
|
||||
// /// approve the upload of cross-signing keys.
|
||||
Oidc {
|
||||
info: OidcCrossSigningResetInfo,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningResetAuthType {
|
||||
fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
|
||||
match value {
|
||||
encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
|
||||
encryption::CrossSigningResetAuthType::Oidc(info) => Self::Oidc { info: info.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OidcCrossSigningResetInfo {
|
||||
/// The URL where the user can approve the reset of the cross-signing keys.
|
||||
pub approval_url: String,
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
|
||||
fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
|
||||
Self { approval_url: value.approval_url.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::fmt::Display;
|
||||
use std::{collections::HashMap, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
|
||||
send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::room_list::RoomListError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
@@ -128,18 +131,120 @@ impl From<RoomError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomListError> for ClientError {
|
||||
fn from(e: RoomListError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheError> for ClientError {
|
||||
fn from(e: EventCacheError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EditError> for ClientError {
|
||||
fn from(e: EditError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomSendQueueError> for ClientError {
|
||||
fn from(e: RoomSendQueueError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
/// Represent a failed to send unrecoverable error of an event sent via the
|
||||
/// send_queue. It is a serializable representation of a client error, see
|
||||
/// `From` implementation for more details. These errors can not be
|
||||
/// automatically retried, but yet some manual action can be taken before retry
|
||||
/// sending. If not the only solution is to delete the local event.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum QueueWedgeError {
|
||||
/// This error occurs when there are some insecure devices in the room, and
|
||||
/// the current encryption setting prohibit sharing with them.
|
||||
InsecureDevices {
|
||||
/// The insecure devices as a Map of userID to deviceID.
|
||||
user_device_map: HashMap<String, Vec<String>>,
|
||||
},
|
||||
|
||||
/// This error occurs when a previously verified user is not anymore, and
|
||||
/// the current encryption setting prohibit sharing when it happens.
|
||||
IdentityViolations {
|
||||
/// The users that are expected to be verified but are not.
|
||||
users: Vec<String>,
|
||||
},
|
||||
|
||||
/// It is required to set up cross-signing and properly erify the current
|
||||
/// session before sending.
|
||||
CrossVerificationRequired,
|
||||
|
||||
/// Some media content to be sent has disappeared from the cache.
|
||||
MissingMediaContent,
|
||||
|
||||
/// Some mime type couldn't be parsed.
|
||||
InvalidMimeType { mime_type: String },
|
||||
|
||||
/// Other errors.
|
||||
GenericApiError { msg: String },
|
||||
}
|
||||
|
||||
/// Simple display implementation that strips out userIds/DeviceIds to avoid
|
||||
/// accidental logging.
|
||||
impl Display for QueueWedgeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
QueueWedgeError::InsecureDevices { .. } => {
|
||||
f.write_str("There are insecure devices in the room")
|
||||
}
|
||||
QueueWedgeError::IdentityViolations { .. } => {
|
||||
f.write_str("Some users that were previously verified are not anymore")
|
||||
}
|
||||
QueueWedgeError::CrossVerificationRequired => {
|
||||
f.write_str("Own verification is required")
|
||||
}
|
||||
QueueWedgeError::MissingMediaContent => {
|
||||
f.write_str("Media to be sent disappeared from local storage")
|
||||
}
|
||||
QueueWedgeError::InvalidMimeType { mime_type } => {
|
||||
write!(f, "Invalid mime type '{mime_type}' for media upload")
|
||||
}
|
||||
QueueWedgeError::GenericApiError { msg } => f.write_str(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SdkQueueWedgeError> for QueueWedgeError {
|
||||
fn from(value: SdkQueueWedgeError) -> Self {
|
||||
match value {
|
||||
SdkQueueWedgeError::InsecureDevices { user_device_map } => Self::InsecureDevices {
|
||||
user_device_map: user_device_map
|
||||
.iter()
|
||||
.map(|(user_id, devices)| {
|
||||
(
|
||||
user_id.to_string(),
|
||||
devices.iter().map(|device_id| device_id.to_string()).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
SdkQueueWedgeError::IdentityViolations { users } => Self::IdentityViolations {
|
||||
users: users.iter().map(ruma::OwnedUserId::to_string).collect(),
|
||||
},
|
||||
SdkQueueWedgeError::CrossVerificationRequired => Self::CrossVerificationRequired,
|
||||
SdkQueueWedgeError::MissingMediaContent => Self::MissingMediaContent,
|
||||
SdkQueueWedgeError::InvalidMimeType { mime_type } => {
|
||||
Self::InvalidMimeType { mime_type }
|
||||
}
|
||||
SdkQueueWedgeError::GenericApiError { msg } => Self::GenericApiError { msg },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum RoomError {
|
||||
@@ -211,3 +316,8 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
|
||||
Self::Generic { msg: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Something has not been implemented yet.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("not implemented yet")]
|
||||
pub struct NotYetImplemented;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use anyhow::{bail, Context};
|
||||
use ruma::events::{
|
||||
room::message::Relation, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
AnyTimelineEvent, MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{message::Relation, redaction::SyncRoomRedactionEvent},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -14,7 +20,7 @@ use crate::{
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEvent(pub(crate) AnySyncTimelineEvent);
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEvent {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.0.event_id().to_string()
|
||||
@@ -99,7 +105,7 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
let original_content = get_state_event_original_content(content)?;
|
||||
StateEventContent::RoomMemberContent {
|
||||
user_id: state_key,
|
||||
membership_state: original_content.membership.into(),
|
||||
membership_state: original_content.membership.try_into()?,
|
||||
}
|
||||
}
|
||||
AnySyncStateEvent::RoomName(_) => StateEventContent::RoomName,
|
||||
@@ -135,7 +141,7 @@ pub enum MessageLikeEventContent {
|
||||
ReactionContent { related_event_id: String },
|
||||
RoomEncrypted,
|
||||
RoomMessage { message_type: MessageType, in_reply_to_event_id: Option<String> },
|
||||
RoomRedaction,
|
||||
RoomRedaction { redacted_event_id: Option<String>, reason: Option<String> },
|
||||
Sticker,
|
||||
}
|
||||
|
||||
@@ -200,7 +206,17 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
in_reply_to_event_id,
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::RoomRedaction(_) => MessageLikeEventContent::RoomRedaction,
|
||||
AnySyncMessageLikeEvent::RoomRedaction(c) => {
|
||||
let (redacted_event_id, reason) = match c {
|
||||
SyncRoomRedactionEvent::Original(o) => {
|
||||
let id =
|
||||
if o.content.redacts.is_some() { o.content.redacts } else { o.redacts };
|
||||
(id.map(|id| id.to_string()), o.content.reason)
|
||||
}
|
||||
SyncRoomRedactionEvent::Redacted(_) => (None, None),
|
||||
};
|
||||
MessageLikeEventContent::RoomRedaction { redacted_event_id, reason }
|
||||
}
|
||||
AnySyncMessageLikeEvent::Sticker(_) => MessageLikeEventContent::Sticker,
|
||||
_ => bail!("Unsupported Event Type"),
|
||||
};
|
||||
@@ -339,3 +355,38 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the 2 possible identifiers of an event, either it has a remote
|
||||
/// event id or a local transaction id, never both or none.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EventOrTransactionId {
|
||||
EventId { event_id: String },
|
||||
TransactionId { transaction_id: String },
|
||||
}
|
||||
|
||||
impl From<TimelineEventItemId> for EventOrTransactionId {
|
||||
fn from(value: TimelineEventItemId) -> Self {
|
||||
match value {
|
||||
TimelineEventItemId::EventId(event_id) => {
|
||||
EventOrTransactionId::EventId { event_id: event_id.to_string() }
|
||||
}
|
||||
TimelineEventItemId::TransactionId(transaction_id) => {
|
||||
EventOrTransactionId::TransactionId { transaction_id: transaction_id.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EventOrTransactionId> for TimelineEventItemId {
|
||||
type Error = IdParseError;
|
||||
fn try_from(value: EventOrTransactionId) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
EventOrTransactionId::EventId { event_id } => {
|
||||
Ok(TimelineEventItemId::EventId(EventId::parse(event_id)?))
|
||||
}
|
||||
EventOrTransactionId::TransactionId { transaction_id } => {
|
||||
Ok(TimelineEventItemId::TransactionId(transaction_id.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 matrix_sdk::crypto::IdentityState;
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct IdentityStatusChange {
|
||||
/// The user ID of the user whose identity status changed
|
||||
pub user_id: String,
|
||||
|
||||
/// The new state of the identity of the user.
|
||||
pub changed_to: IdentityState,
|
||||
}
|
||||
@@ -2,24 +2,6 @@
|
||||
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
|
||||
macro_rules! unwrap_or_clone_arc_into_variant {
|
||||
(
|
||||
$arc:ident $(, .$field:tt)?, $pat:pat => $body:expr
|
||||
) => {
|
||||
#[allow(unused_variables)]
|
||||
match &(*$arc)$(.$field)? {
|
||||
$pat => {
|
||||
#[warn(unused_variables)]
|
||||
match crate::helpers::unwrap_or_clone_arc($arc)$(.$field)? {
|
||||
$pat => Some($body),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod authentication;
|
||||
mod chunk_iterator;
|
||||
mod client;
|
||||
@@ -29,10 +11,12 @@ mod encryption;
|
||||
mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod identity_status_change;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod room;
|
||||
mod room_alias;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
@@ -61,7 +45,7 @@ use self::{
|
||||
|
||||
uniffi::include_scaffolding!("api");
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn sdk_git_sha() -> String {
|
||||
env!("VERGEN_GIT_SHA").to_owned()
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub struct NotificationClient {
|
||||
pub(crate) _client: Arc<Client>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl NotificationClient {
|
||||
/// See also documentation of
|
||||
/// `MatrixNotificationClient::get_notification`.
|
||||
|
||||
@@ -49,7 +49,7 @@ impl From<RoomNotificationMode> for SdkRoomNotificationMode {
|
||||
}
|
||||
|
||||
/// Delegate to notify of changes in push rules
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait NotificationSettingsDelegate: Sync + Send {
|
||||
fn settings_did_change(&self);
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl Drop for NotificationSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl NotificationSettings {
|
||||
pub fn set_delegate(&self, delegate: Option<Box<dyn NotificationSettingsDelegate>>) {
|
||||
if let Some(delegate) = delegate {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, time::FormatTime, FormatEvent, FormatFields, FormattedFields},
|
||||
field::RecordFields,
|
||||
fmt::{
|
||||
self,
|
||||
format::{DefaultFields, Writer},
|
||||
time::FormatTime,
|
||||
FormatEvent, FormatFields, FormattedFields,
|
||||
},
|
||||
layer::SubscriberExt,
|
||||
registry::LookupSpan,
|
||||
util::SubscriberInitExt,
|
||||
@@ -97,17 +103,19 @@ where
|
||||
|
||||
if let Some(scope) = ctx.event_scope() {
|
||||
writer.write_str(" | spans: ")?;
|
||||
|
||||
let mut first = true;
|
||||
|
||||
for span in scope.from_root() {
|
||||
if !first {
|
||||
writer.write_str(" > ")?;
|
||||
}
|
||||
first = false;
|
||||
write!(writer, "{}", span.metadata().name())?;
|
||||
|
||||
let ext = span.extensions();
|
||||
if let Some(fields) = &ext.get::<FormattedFields<N>>() {
|
||||
first = false;
|
||||
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
@@ -133,7 +141,25 @@ where
|
||||
|
||||
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
|
||||
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterForFiles(DefaultFields);
|
||||
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterForFiles {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
@@ -145,8 +171,26 @@ where
|
||||
Layer::and_then(
|
||||
file_layer,
|
||||
config.write_to_stdout_or_system.then(|| {
|
||||
// Another fields formatter is necessary because of this bug
|
||||
// https://github.com/tokio-rs/tracing/issues/1372. Using a new
|
||||
// formatter for the fields forces to record them in different span
|
||||
// extensions, and thus remove the duplicated fields in the span.
|
||||
#[derive(Default)]
|
||||
struct FieldsFormatterFormStdoutOrSystem(DefaultFields);
|
||||
|
||||
impl<'writer> FormatFields<'writer> for FieldsFormatterFormStdoutOrSystem {
|
||||
fn format_fields<R: RecordFields>(
|
||||
&self,
|
||||
writer: Writer<'writer>,
|
||||
fields: R,
|
||||
) -> std::fmt::Result {
|
||||
self.0.format_fields(writer, fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
return fmt::layer()
|
||||
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
|
||||
.event_format(EventFormatter::new())
|
||||
// See comment above.
|
||||
.with_ansi(false)
|
||||
@@ -154,6 +198,7 @@ where
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
return fmt::layer()
|
||||
.fmt_fields(FieldsFormatterFormStdoutOrSystem::default())
|
||||
.event_format(EventFormatter::for_logcat())
|
||||
// See comment above.
|
||||
.with_ansi(false)
|
||||
@@ -164,24 +209,40 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Configuration to save logs to (rotated) log-files.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingFileConfiguration {
|
||||
/// Base location for all the log files.
|
||||
path: String,
|
||||
|
||||
/// Prefix for the log files' names.
|
||||
file_prefix: String,
|
||||
|
||||
/// Optional suffix for the log file's names.
|
||||
file_suffix: Option<String>,
|
||||
|
||||
/// Maximum number of rotated files.
|
||||
///
|
||||
/// If not set, there's no max limit, i.e. the number of log files is
|
||||
/// unlimited.
|
||||
max_files: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
/// A filter line following the [RUST_LOG format].
|
||||
///
|
||||
/// [RUST_LOG format]: https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/config_log.html
|
||||
filter: String,
|
||||
/// Controls whether to print to stdout or, equivalent, the system logs on
|
||||
/// Android.
|
||||
|
||||
/// Whether to log to stdout, or in the logcat on Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
|
||||
/// If set, configures rotated log files where to write additional logs.
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
log_panics();
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
event_cache::paginator::PaginatorError,
|
||||
room::{power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole},
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
};
|
||||
@@ -16,11 +20,12 @@ use ruma::{
|
||||
call::notify,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
message::RoomMessageEventContentWithoutRelation,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
TimelineEventType,
|
||||
},
|
||||
EventId, Int, RoomAliasId, UserId,
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
@@ -30,19 +35,21 @@ use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
identity_status_change::IdentityStatusChange,
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
ruma::{ImageInfo, Mentions, NotifyType},
|
||||
timeline::{FocusEventError, ReceiptType, Timeline},
|
||||
timeline::{FocusEventError, ReceiptType, SendHandle, Timeline},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum Membership {
|
||||
Invited,
|
||||
Joined,
|
||||
Left,
|
||||
Knocked,
|
||||
}
|
||||
|
||||
impl From<RoomState> for Membership {
|
||||
@@ -51,6 +58,7 @@ impl From<RoomState> for Membership {
|
||||
RoomState::Invited => Membership::Invited,
|
||||
RoomState::Joined => Membership::Joined,
|
||||
RoomState::Left => Membership::Left,
|
||||
RoomState::Knocked => Membership::Knocked,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +81,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Room {
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
@@ -154,7 +162,12 @@ impl Room {
|
||||
/// the user who invited the logged-in user to a room.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
if self.inner.state() == RoomState::Invited {
|
||||
self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into())
|
||||
self.inner
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.inviter)
|
||||
.and_then(|m| m.try_into().ok())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -225,6 +238,28 @@ impl Room {
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub async fn pinned_events_timeline(
|
||||
&self,
|
||||
internal_id_prefix: Option<String>,
|
||||
max_events_to_load: u16,
|
||||
max_concurrent_requests: u16,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
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 = builder
|
||||
.with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
|
||||
}
|
||||
@@ -242,7 +277,7 @@ impl Room {
|
||||
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
Ok(member.into())
|
||||
Ok(member.try_into().context("Unknown state membership")?)
|
||||
}
|
||||
|
||||
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
@@ -519,6 +554,11 @@ impl Room {
|
||||
Ok(self.inner.can_user_send_message(&user_id, message.into()).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_pin_unpin(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_pin_unpin(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_trigger_room_notification(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -550,6 +590,31 @@ impl Room {
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn subscribe_to_identity_status_changes(
|
||||
&self,
|
||||
listener: Box<dyn IdentityStatusChangeListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let status_changes = room.subscribe_to_identity_status_changes().await;
|
||||
if let Ok(status_changes) = status_changes {
|
||||
// TODO: what to do with failures?
|
||||
let mut status_changes = pin!(status_changes);
|
||||
while let Some(identity_status_changes) = status_changes.next().await {
|
||||
listener.call(
|
||||
identity_status_changes
|
||||
.into_iter()
|
||||
.map(|change| {
|
||||
let user_id = change.user_id.to_string();
|
||||
IdentityStatusChange { user_id, changed_to: change.changed_to }
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -568,7 +633,7 @@ impl Room {
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
let power_levels = self.inner.room_power_levels().await?;
|
||||
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
|
||||
Ok(RoomPowerLevels::from(power_levels))
|
||||
}
|
||||
|
||||
@@ -690,10 +755,95 @@ impl Room {
|
||||
pub async fn clear_composer_draft(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.clear_composer_draft().await?)
|
||||
}
|
||||
|
||||
/// Edit an event given its event id.
|
||||
///
|
||||
/// Useful outside the context of a timeline, or when a timeline doesn't
|
||||
/// have the full content of an event.
|
||||
pub async fn edit(
|
||||
&self,
|
||||
event_id: String,
|
||||
new_content: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
let replacement_event = self
|
||||
.inner
|
||||
.make_edit_event(&event_id, EditedContent::RoomMessage((*new_content).clone()))
|
||||
.await?;
|
||||
|
||||
self.inner.send_queue().send(replacement_event).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove verification requirements for the given users and
|
||||
/// resend messages that failed to send because their identities were no
|
||||
/// longer verified (in response to
|
||||
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity`)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_ids` - The list of users identifiers received in the error
|
||||
/// * `transaction_id` - The send queue transaction identifier of the local
|
||||
/// echo the send error applies to
|
||||
pub async fn withdraw_verification_and_resend(
|
||||
&self,
|
||||
user_ids: Vec<String>,
|
||||
send_handle: Arc<SendHandle>,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_ids: Vec<OwnedUserId> =
|
||||
user_ids.iter().map(UserId::parse).collect::<Result<_, _>>()?;
|
||||
|
||||
let encryption = self.inner.client().encryption();
|
||||
|
||||
for user_id in user_ids {
|
||||
if let Some(user_identity) = encryption.get_user_identity(&user_id).await? {
|
||||
user_identity.withdraw_verification().await?;
|
||||
}
|
||||
}
|
||||
|
||||
send_handle.try_resend().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the local trust for the given devices to `LocalTrust::Ignored`
|
||||
/// and resend messages that failed to send because said devices are
|
||||
/// unverified (in response to
|
||||
/// `SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice`).
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `devices` - The map of users identifiers to device identifiers
|
||||
/// received in the error
|
||||
/// * `transaction_id` - The send queue transaction identifier of the local
|
||||
/// echo the send error applies to
|
||||
pub async fn ignore_device_trust_and_resend(
|
||||
&self,
|
||||
devices: HashMap<String, Vec<String>>,
|
||||
send_handle: Arc<SendHandle>,
|
||||
) -> Result<(), ClientError> {
|
||||
let encryption = self.inner.client().encryption();
|
||||
|
||||
for (user_id, device_ids) in devices.iter() {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
|
||||
for device_id in device_ids {
|
||||
let device_id: OwnedDeviceId = device_id.as_str().into();
|
||||
|
||||
if let Some(device) = encryption.get_device(&user_id, &device_id).await? {
|
||||
device.set_local_trust(LocalTrust::Ignored).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_handle.try_resend().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given room alias.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn matrix_to_room_alias_permalink(
|
||||
room_alias: String,
|
||||
) -> std::result::Result<String, ClientError> {
|
||||
@@ -749,16 +899,21 @@ impl From<RumaPowerLevels> for RoomPowerLevels {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomInfoListener: Sync + Send {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TypingNotificationsListener: Sync + Send {
|
||||
fn call(&self, typing_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IdentityStatusChangeListener: Sync + Send {
|
||||
fn call(&self, identity_status_change: Vec<IdentityStatusChange>);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMembersIterator {
|
||||
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
|
||||
@@ -770,7 +925,7 @@ impl RoomMembersIterator {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomMembersIterator {
|
||||
fn len(&self) -> u32 {
|
||||
self.chunk_iterator.len()
|
||||
@@ -779,7 +934,7 @@ impl RoomMembersIterator {
|
||||
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
|
||||
self.chunk_iterator
|
||||
.next(chunk_size)
|
||||
.map(|members| members.into_iter().map(|m| m.into()).collect())
|
||||
.map(|members| members.into_iter().filter_map(|m| m.try_into().ok()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use matrix_sdk::RoomDisplayName;
|
||||
|
||||
/// Verifies the passed `String` matches the expected room alias format:
|
||||
///
|
||||
/// This means it's lowercase, with no whitespace chars, has a single leading
|
||||
/// `#` char and a single `:` separator between the local and domain parts, and
|
||||
/// the local part only contains characters that can't be percent encoded.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn is_room_alias_format_valid(alias: String) -> bool {
|
||||
matrix_sdk::utils::is_room_alias_format_valid(alias)
|
||||
}
|
||||
|
||||
/// Transforms a Room's display name into a valid room alias name.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn room_alias_name_from_room_display_name(room_name: String) -> String {
|
||||
RoomDisplayName::Named(room_name).to_room_alias_name()
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use std::{fmt::Debug, sync::Arc};
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use ruma::ServerName;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::RUNTIME;
|
||||
@@ -68,6 +69,12 @@ impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescriptio
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for performing room searches in the room directory.
|
||||
/// The way this is intended to be used is:
|
||||
///
|
||||
/// 1. Register a callback using [`RoomDirectorySearch::results`].
|
||||
/// 2. Start the room search with [`RoomDirectorySearch::search`].
|
||||
/// 3. To get more results, use [`RoomDirectorySearch::next_page`].
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomDirectorySearch {
|
||||
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
|
||||
@@ -79,30 +86,51 @@ impl RoomDirectorySearch {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomDirectorySearch {
|
||||
/// Asks the server for the next page of the current search.
|
||||
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> {
|
||||
/// Starts a filtered search for the server.
|
||||
///
|
||||
/// If the `filter` is not provided it will search for all the rooms.
|
||||
/// You can specify a `batch_size` to control the number of rooms to fetch
|
||||
/// per request.
|
||||
///
|
||||
/// If the `via_server` is not provided it will search in the current
|
||||
/// homeserver by default.
|
||||
///
|
||||
/// This method will clear the current search results and start a new one.
|
||||
pub async fn search(
|
||||
&self,
|
||||
filter: Option<String>,
|
||||
batch_size: u32,
|
||||
via_server_name: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let server = via_server_name.map(ServerName::parse).transpose()?;
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.search(filter, batch_size).await?;
|
||||
inner.search(filter, batch_size, server).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the number of pages that have been loaded so far.
|
||||
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.loaded_pages() as u32)
|
||||
}
|
||||
|
||||
/// Get whether the search is at the last page.
|
||||
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.is_at_last_page())
|
||||
}
|
||||
|
||||
/// Registers a callback to receive new search results when starting a
|
||||
/// search or getting new paginated results.
|
||||
pub async fn results(
|
||||
&self,
|
||||
listener: Box<dyn RoomDirectorySearchEntriesListener>,
|
||||
@@ -169,7 +197,7 @@ impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
creator: 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>,
|
||||
@@ -23,7 +24,6 @@ pub struct RoomInfo {
|
||||
is_space: bool,
|
||||
is_tombstoned: bool,
|
||||
is_favourite: bool,
|
||||
is_encrypted: bool,
|
||||
canonical_alias: Option<String>,
|
||||
alternative_aliases: Vec<String>,
|
||||
membership: Membership,
|
||||
@@ -40,7 +40,7 @@ pub struct RoomInfo {
|
||||
user_power_levels: HashMap<String, i64>,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
cached_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
|
||||
@@ -54,6 +54,8 @@ pub struct RoomInfo {
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
/// The currently pinned event ids
|
||||
pinned_event_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
@@ -65,9 +67,12 @@ impl RoomInfo {
|
||||
for (id, level) in power_levels_map.iter() {
|
||||
user_power_levels.insert(id.to_string(), *level);
|
||||
}
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
creator: room.creator().as_ref().map(ToString::to_string),
|
||||
display_name: room.cached_display_name().map(|name| name.to_string()),
|
||||
raw_name: room.name(),
|
||||
topic: room.topic(),
|
||||
@@ -77,7 +82,6 @@ impl RoomInfo {
|
||||
is_space: room.is_space(),
|
||||
is_tombstoned: room.is_tombstoned(),
|
||||
is_favourite: room.is_favourite(),
|
||||
is_encrypted: room.is_encrypted().await.unwrap_or(false),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
|
||||
membership: room.state().into(),
|
||||
@@ -87,7 +91,10 @@ impl RoomInfo {
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|details| details.inviter)
|
||||
.map(Into::into),
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten(),
|
||||
_ => None,
|
||||
},
|
||||
heroes: room.heroes().into_iter().map(Into::into).collect(),
|
||||
@@ -97,9 +104,8 @@ impl RoomInfo {
|
||||
user_power_levels,
|
||||
highlight_count: unread_notification_counts.highlight_count,
|
||||
notification_count: unread_notification_counts.notification_count,
|
||||
user_defined_notification_mode: room
|
||||
.user_defined_notification_mode()
|
||||
.await
|
||||
cached_user_defined_notification_mode: room
|
||||
.cached_user_defined_notification_mode()
|
||||
.map(Into::into),
|
||||
has_room_call: room.has_active_room_call(),
|
||||
active_room_call_participants: room
|
||||
@@ -111,6 +117,7 @@ impl RoomInfo {
|
||||
num_unread_messages: room.num_unread_messages(),
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::{
|
||||
v4::RoomSubscription as RumaRoomSubscription,
|
||||
UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
assign, RoomId,
|
||||
},
|
||||
RoomListEntry as MatrixRoomListEntry,
|
||||
use matrix_sdk::ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
};
|
||||
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,
|
||||
room_list_service::filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
|
||||
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_joined,
|
||||
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
|
||||
new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
},
|
||||
timeline::default_event_filter,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
room::Room,
|
||||
room::{Membership, Room},
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
timeline_event_filter::TimelineEventTypeFilter,
|
||||
utils::AsyncRuntimeDropped,
|
||||
TaskHandle, RUNTIME,
|
||||
};
|
||||
|
||||
@@ -56,6 +52,8 @@ pub enum RoomListError {
|
||||
InitializingTimeline { error: String },
|
||||
#[error("Event cache ran into an error: {error}")]
|
||||
EventCache { error: String },
|
||||
#[error("The requested room doesn't match the membership requirements {expected:?}, observed {actual:?}")]
|
||||
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
@@ -65,7 +63,6 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
match value {
|
||||
SlidingSync(error) => Self::SlidingSync { error: error.to_string() },
|
||||
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() }
|
||||
@@ -84,34 +81,13 @@ impl From<ruma::IdParseError> for RoomListError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomListRange {
|
||||
pub start: u32,
|
||||
pub end_inclusive: u32,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListInput {
|
||||
Viewport { ranges: Vec<RoomListRange> },
|
||||
}
|
||||
|
||||
impl From<RoomListInput> for matrix_sdk_ui::room_list_service::Input {
|
||||
fn from(value: RoomListInput) -> Self {
|
||||
match value {
|
||||
RoomListInput::Viewport { ranges } => Self::Viewport(
|
||||
ranges.iter().map(|range| range.start..=range.end_inclusive).collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListService {
|
||||
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
|
||||
let state_stream = self.inner.state();
|
||||
@@ -141,10 +117,6 @@ impl RoomListService {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn apply_input(&self, input: RoomListInput) -> Result<(), RoomListError> {
|
||||
self.inner.apply_input(input.into()).await.map(|_| ()).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn sync_indicator(
|
||||
&self,
|
||||
delay_before_showing_in_ms: u32,
|
||||
@@ -164,6 +136,19 @@ impl RoomListService {
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
let room_ids = room_ids
|
||||
.into_iter()
|
||||
.map(|room_id| {
|
||||
RoomId::parse(&room_id).map_err(|_| RoomListError::InvalidRoomId { error: room_id })
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -172,7 +157,7 @@ pub struct RoomList {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::RoomList>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomList {
|
||||
fn loading_state(
|
||||
&self,
|
||||
@@ -192,45 +177,98 @@ impl RoomList {
|
||||
})
|
||||
}
|
||||
|
||||
fn entries(&self, listener: Box<dyn RoomListEntriesListener>) -> RoomListEntriesResult {
|
||||
let (entries, entries_stream) = self.inner.entries();
|
||||
|
||||
RoomListEntriesResult {
|
||||
entries: entries.into_iter().map(Into::into).collect(),
|
||||
entries_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
while let Some(diff) = entries_stream.next().await {
|
||||
listener.on_update(diff.into_iter().map(Into::into).collect());
|
||||
}
|
||||
}))),
|
||||
}
|
||||
}
|
||||
|
||||
fn entries_with_dynamic_adapters(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> RoomListEntriesWithDynamicAdaptersResult {
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
self.inner.entries_with_dynamic_adapters(
|
||||
page_size.try_into().unwrap(),
|
||||
self.room_list_service.inner.client().roominfo_update_receiver(),
|
||||
);
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self.clone();
|
||||
let utd_hook = self.room_list_service.utd_hook.clone();
|
||||
|
||||
RoomListEntriesWithDynamicAdaptersResult {
|
||||
controller: Arc::new(RoomListDynamicEntriesController::new(
|
||||
dynamic_entries_controller,
|
||||
self.room_list_service.inner.client(),
|
||||
)),
|
||||
entries_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
// The following code deserves a bit of explanation.
|
||||
// `matrix_sdk_ui::room_list_service::RoomList::entries_with_dynamic_adapters`
|
||||
// returns a `Stream` with a lifetime bounds to its `self` (`RoomList`). This is
|
||||
// problematic here as this `Stream` is returned as part of
|
||||
// `RoomListEntriesWithDynamicAdaptersResult` but it is not possible to store
|
||||
// `RoomList` with it inside the `Future` that is run inside the `TaskHandle`
|
||||
// that consumes this `Stream`. We have a lifetime issue: `RoomList` doesn't
|
||||
// live long enough!
|
||||
//
|
||||
// To solve this issue, the trick is to store the `RoomList` inside the
|
||||
// `RoomListEntriesWithDynamicAdaptersResult`. Alright, but then we have another
|
||||
// lifetime issue! `RoomList` cannot move inside this struct because it is
|
||||
// borrowed by `entries_with_dynamic_adapters`. Indeed, the struct is built
|
||||
// after the `Stream` is obtained.
|
||||
//
|
||||
// To solve this issue, we need to build the struct field by field, starting
|
||||
// with `this`, and use a reference to `this` to call
|
||||
// `entries_with_dynamic_adapters`. This is unsafe because a couple of
|
||||
// invariants must hold, but all this is legal and correct if the invariants are
|
||||
// properly fulfilled.
|
||||
|
||||
while let Some(diff) = entries_stream.next().await {
|
||||
listener.on_update(diff.into_iter().map(Into::into).collect());
|
||||
}
|
||||
}))),
|
||||
// Create the struct result with uninitialized fields.
|
||||
let mut result = MaybeUninit::<RoomListEntriesWithDynamicAdaptersResult>::uninit();
|
||||
let ptr = result.as_mut_ptr();
|
||||
|
||||
// Initialize the first field `this`.
|
||||
//
|
||||
// SAFETY: `ptr` is correctly aligned, this is guaranteed by `MaybeUninit`.
|
||||
unsafe {
|
||||
addr_of_mut!((*ptr).this).write(this);
|
||||
}
|
||||
|
||||
// Get a reference to `this`. It is only borrowed, it's not moved.
|
||||
let this =
|
||||
// SAFETY: `ptr` is correct aligned, the `this` field is correctly aligned,
|
||||
// is dereferenceable and points to a correctly initialized value as done
|
||||
// in the previous line.
|
||||
unsafe { addr_of_mut!((*ptr).this).as_ref() }
|
||||
// SAFETY: `this` contains a non null value.
|
||||
.unwrap();
|
||||
|
||||
// Now we can create `entries_stream` and `dynamic_entries_controller` by
|
||||
// borrowing `this`, which is going to live long enough since it will live as
|
||||
// long as `entries_stream` and `dynamic_entries_controller`.
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
|
||||
// FFI dance to make those values consumable by foreign language, nothing fancy
|
||||
// here, that's the real code for this method.
|
||||
let dynamic_entries_controller =
|
||||
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
|
||||
|
||||
let entries_stream = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
while let Some(diffs) = entries_stream.next().await {
|
||||
listener.on_update(
|
||||
diffs
|
||||
.into_iter()
|
||||
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
})));
|
||||
|
||||
// Initialize the second field `controller`.
|
||||
//
|
||||
// SAFETY: `ptr` is correctly aligned.
|
||||
unsafe {
|
||||
addr_of_mut!((*ptr).controller).write(dynamic_entries_controller);
|
||||
}
|
||||
|
||||
// Initialize the third and last field `entries_stream`.
|
||||
//
|
||||
// SAFETY: `ptr` is correctly aligned.
|
||||
unsafe {
|
||||
addr_of_mut!((*ptr).entries_stream).write(entries_stream);
|
||||
}
|
||||
|
||||
// The result is complete, let's return it!
|
||||
//
|
||||
// SAFETY: `result` is fully initialized, all its fields have received a valid
|
||||
// value.
|
||||
Arc::new(unsafe { result.assume_init() })
|
||||
}
|
||||
|
||||
fn room(&self, room_id: String) -> Result<Arc<RoomListItem>, RoomListError> {
|
||||
@@ -238,16 +276,22 @@ impl RoomList {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomListEntriesResult {
|
||||
pub entries: Vec<RoomListEntry>,
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListEntriesWithDynamicAdaptersResult {
|
||||
this: Arc<RoomList>,
|
||||
controller: Arc<RoomListDynamicEntriesController>,
|
||||
entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomListEntriesWithDynamicAdaptersResult {
|
||||
pub controller: Arc<RoomListDynamicEntriesController>,
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListEntriesWithDynamicAdaptersResult {
|
||||
fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
|
||||
self.controller.clone()
|
||||
}
|
||||
|
||||
fn entries_stream(&self) -> Arc<TaskHandle> {
|
||||
self.entries_stream.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -317,65 +361,80 @@ impl From<matrix_sdk_ui::room_list_service::RoomListLoadingState> for RoomListLo
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceStateListener: Send + Sync + Debug {
|
||||
fn on_update(&self, state: RoomListServiceState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListLoadingStateListener: Send + Sync + Debug {
|
||||
fn on_update(&self, state: RoomListLoadingState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListServiceSyncIndicatorListener: Send + Sync + Debug {
|
||||
fn on_update(&self, sync_indicator: RoomListServiceSyncIndicator);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListEntriesUpdate {
|
||||
Append { values: Vec<RoomListEntry> },
|
||||
Append { values: Vec<Arc<RoomListItem>> },
|
||||
Clear,
|
||||
PushFront { value: RoomListEntry },
|
||||
PushBack { value: RoomListEntry },
|
||||
PushFront { value: Arc<RoomListItem> },
|
||||
PushBack { value: Arc<RoomListItem> },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: RoomListEntry },
|
||||
Set { index: u32, value: RoomListEntry },
|
||||
Insert { index: u32, value: Arc<RoomListItem> },
|
||||
Set { index: u32, value: Arc<RoomListItem> },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<RoomListEntry> },
|
||||
Reset { values: Vec<Arc<RoomListItem>> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<matrix_sdk::RoomListEntry>> for RoomListEntriesUpdate {
|
||||
fn from(other: VectorDiff<matrix_sdk::RoomListEntry>) -> Self {
|
||||
match other {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
impl RoomListEntriesUpdate {
|
||||
fn from(
|
||||
vector_diff: VectorDiff<matrix_sdk_ui::room_list_service::Room>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
) -> Self {
|
||||
match vector_diff {
|
||||
VectorDiff::Append { values } => Self::Append {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
|
||||
.collect(),
|
||||
},
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PushFront { value } => {
|
||||
Self::PushFront { value: Arc::new(RoomListItem::from(value, utd_hook)) }
|
||||
}
|
||||
VectorDiff::PushBack { value } => {
|
||||
Self::PushBack { value: Arc::new(RoomListItem::from(value, utd_hook)) }
|
||||
}
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: u32::try_from(index).unwrap(), value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: u32::try_from(index).unwrap(), value: value.into() }
|
||||
}
|
||||
VectorDiff::Insert { index, value } => Self::Insert {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: Arc::new(RoomListItem::from(value, utd_hook)),
|
||||
},
|
||||
VectorDiff::Set { index, value } => Self::Set {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: Arc::new(RoomListItem::from(value, utd_hook)),
|
||||
},
|
||||
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
|
||||
VectorDiff::Truncate { length } => {
|
||||
Self::Truncate { length: u32::try_from(length).unwrap() }
|
||||
}
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
VectorDiff::Reset { values } => Self::Reset {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|value| Arc::new(RoomListItem::from(value, utd_hook.clone())))
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomListEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomListEntriesUpdate>);
|
||||
}
|
||||
@@ -383,23 +442,20 @@ pub trait RoomListEntriesListener: Send + Sync + Debug {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListDynamicEntriesController {
|
||||
inner: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
|
||||
client: matrix_sdk::Client,
|
||||
}
|
||||
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn new(
|
||||
dynamic_entries_controller: matrix_sdk_ui::room_list_service::RoomListDynamicEntriesController,
|
||||
client: &matrix_sdk::Client,
|
||||
) -> Self {
|
||||
Self { inner: dynamic_entries_controller, client: client.clone() }
|
||||
Self { inner: dynamic_entries_controller }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
|
||||
let FilterWrapper(filter) = FilterWrapper::from(&self.client, kind);
|
||||
self.inner.set_filter(filter)
|
||||
self.inner.set_filter(kind.into())
|
||||
}
|
||||
|
||||
fn add_one_page(&self) {
|
||||
@@ -441,33 +497,29 @@ impl From<RoomListFilterCategory> for RoomCategory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom internal type to transform a `RoomListEntriesDynamicFilterKind` into
|
||||
/// a `BoxedFilterFn`.
|
||||
struct FilterWrapper(BoxedFilterFn);
|
||||
|
||||
impl FilterWrapper {
|
||||
fn from(client: &matrix_sdk::Client, value: RoomListEntriesDynamicFilterKind) -> Self {
|
||||
impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
fn from(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::All { filters } => Box::new(new_filter_all(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::Any { filters } => Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::NonLeft => Box::new(new_filter_non_left()),
|
||||
Kind::Joined => Box::new(new_filter_joined()),
|
||||
Kind::Unread => Box::new(new_filter_unread()),
|
||||
Kind::Favourite => Box::new(new_filter_favourite()),
|
||||
Kind::Invite => Box::new(new_filter_invite()),
|
||||
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
|
||||
Kind::None => Box::new(new_filter_none()),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
Self(Box::new(new_filter_normalized_match_room_name(client, &pattern)))
|
||||
Box::new(new_filter_normalized_match_room_name(&pattern))
|
||||
}
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
Self(Box::new(new_filter_fuzzy_match_room_name(client, &pattern)))
|
||||
Box::new(new_filter_fuzzy_match_room_name(&pattern))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,7 +531,16 @@ pub struct RoomListItem {
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl RoomListItem {
|
||||
fn from(
|
||||
value: matrix_sdk_ui::room_list_service::Room,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
) -> Self {
|
||||
Self { inner: Arc::new(value), utd_hook }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListItem {
|
||||
fn id(&self) -> String {
|
||||
self.inner.id().to_string()
|
||||
@@ -504,14 +565,82 @@ impl RoomListItem {
|
||||
self.inner.inner_room().canonical_alias().map(|alias| alias.to_string())
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
Ok(RoomInfo::new(self.inner.inner_room()).await?)
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
fn membership(&self) -> Membership {
|
||||
self.inner.inner_room().state().into()
|
||||
}
|
||||
|
||||
/// Builds a `Room` FFI from an invited room without initializing its
|
||||
/// internal timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than invited.
|
||||
///
|
||||
/// ⚠️ Holding on to this room instance after it has been joined is not
|
||||
/// safe. Use `full_room` instead.
|
||||
#[deprecated(note = "Please use `preview_room` instead.")]
|
||||
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Invited) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited or knocked rooms.
|
||||
///
|
||||
/// An error will be returned if the room is in a state other than invited
|
||||
/// or knocked.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Validate internal room state.
|
||||
let membership = self.membership();
|
||||
if !matches!(membership, Membership::Invited | Membership::Knocked) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited, Membership::Knocked],
|
||||
actual: membership,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, server_names) = if let Some(alias) = self.inner.canonical_alias() {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
|
||||
/// Build a full `Room` FFI object, filling its associated timeline.
|
||||
///
|
||||
/// If its internal timeline hasn't been initialized, it'll fail.
|
||||
/// An error will be returned if the room is a state different than joined
|
||||
/// or if its internal timeline hasn't been initialized.
|
||||
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Joined) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Joined],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(timeline) = self.inner.timeline() {
|
||||
Ok(Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
@@ -574,66 +703,8 @@ impl RoomListItem {
|
||||
self.inner.is_encrypted().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn subscribe(&self, settings: Option<RoomSubscription>) {
|
||||
self.inner.subscribe(settings.map(Into::into));
|
||||
}
|
||||
|
||||
fn unsubscribe(&self) {
|
||||
self.inner.unsubscribe();
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
|
||||
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, uniffi::Enum)]
|
||||
pub enum RoomListEntry {
|
||||
Empty,
|
||||
Invalidated { room_id: String },
|
||||
Filled { room_id: String },
|
||||
}
|
||||
|
||||
impl From<MatrixRoomListEntry> for RoomListEntry {
|
||||
fn from(value: MatrixRoomListEntry) -> Self {
|
||||
(&value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MatrixRoomListEntry> for RoomListEntry {
|
||||
fn from(value: &MatrixRoomListEntry) -> Self {
|
||||
match value {
|
||||
MatrixRoomListEntry::Empty => Self::Empty,
|
||||
MatrixRoomListEntry::Filled(room_id) => Self::Filled { room_id: room_id.to_string() },
|
||||
MatrixRoomListEntry::Invalidated(room_id) => {
|
||||
Self::Invalidated { room_id: room_id.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RequiredState {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomSubscription {
|
||||
pub required_state: Option<Vec<RequiredState>>,
|
||||
pub timeline_limit: Option<u32>,
|
||||
pub include_heroes: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<RoomSubscription> for RumaRoomSubscription {
|
||||
fn from(val: RoomSubscription) -> Self {
|
||||
assign!(RumaRoomSubscription::default(), {
|
||||
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()),
|
||||
include_heroes: val.include_heroes,
|
||||
})
|
||||
async fn latest_event(&self) -> Option<EventTimelineItem> {
|
||||
self.inner.latest_event().await.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,7 +714,7 @@ pub struct UnreadNotificationsCount {
|
||||
notification_count: u32,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl UnreadNotificationsCount {
|
||||
fn highlight_count(&self) -> u32 {
|
||||
self.highlight_count
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::UserId;
|
||||
|
||||
use crate::error::ClientError;
|
||||
use crate::error::{ClientError, NotYetImplemented};
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MembershipState {
|
||||
@@ -19,43 +19,58 @@ pub enum MembershipState {
|
||||
|
||||
/// The user has left.
|
||||
Leave,
|
||||
|
||||
/// A custom membership state value.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
|
||||
fn from(m: matrix_sdk::ruma::events::room::member::MembershipState) -> Self {
|
||||
impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for MembershipState {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(
|
||||
m: matrix_sdk::ruma::events::room::member::MembershipState,
|
||||
) -> Result<Self, Self::Error> {
|
||||
match m {
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Ban => MembershipState::Ban,
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
|
||||
MembershipState::Invite
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Ban => {
|
||||
Ok(MembershipState::Ban)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Invite => {
|
||||
Ok(MembershipState::Invite)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Join => {
|
||||
Ok(MembershipState::Join)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Join => MembershipState::Join,
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Knock => {
|
||||
MembershipState::Knock
|
||||
Ok(MembershipState::Knock)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
|
||||
MembershipState::Leave
|
||||
Ok(MembershipState::Leave)
|
||||
}
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::_Custom(_) => {
|
||||
Ok(MembershipState::Custom { value: m.to_string() })
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Other membership state change not yet implemented");
|
||||
Err(NotYetImplemented)
|
||||
}
|
||||
_ => unimplemented!(
|
||||
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::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]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
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()
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given userID.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::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())
|
||||
@@ -74,18 +89,20 @@ pub struct RoomMember {
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
}
|
||||
|
||||
impl From<SdkRoomMember> for RoomMember {
|
||||
fn from(m: SdkRoomMember) -> Self {
|
||||
RoomMember {
|
||||
impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(m: SdkRoomMember) -> Result<Self, Self::Error> {
|
||||
Ok(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(),
|
||||
membership: m.membership().clone().try_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(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,73 @@
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, RoomState};
|
||||
use ruma::space::SpaceRoomJoinRule;
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
/// A room preview for a room. It's intended to be used to represent rooms that
|
||||
/// aren't joined yet.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomPreview {
|
||||
inner: SdkRoomPreview,
|
||||
client: AsyncRuntimeDropped<Client>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPreview {
|
||||
/// Returns the room info the preview contains.
|
||||
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
|
||||
let info = &self.inner;
|
||||
Ok(RoomPreviewInfo {
|
||||
room_id: info.room_id.to_string(),
|
||||
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
|
||||
name: info.name.clone(),
|
||||
topic: info.topic.clone(),
|
||||
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
num_joined_members: info.num_joined_members,
|
||||
num_active_members: info.num_active_members,
|
||||
room_type: info.room_type.as_ref().into(),
|
||||
is_history_world_readable: info.is_world_readable,
|
||||
membership: info.state.map(|state| state.into()),
|
||||
join_rule: info
|
||||
.join_rule
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
})
|
||||
}
|
||||
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
/// knocked.
|
||||
///
|
||||
/// Will return an error otherwise.
|
||||
pub async fn leave(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.leave().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
let invite_details = room.invite_details().await.ok()?;
|
||||
invite_details.inviter.and_then(|m| m.try_into().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
|
||||
Self { client, inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// The preview of a room, be it invited/joined/left, or not.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPreview {
|
||||
pub struct RoomPreviewInfo {
|
||||
/// The room id for this room.
|
||||
pub room_id: String,
|
||||
/// The canonical alias for the room.
|
||||
@@ -16,38 +80,60 @@ pub struct RoomPreview {
|
||||
pub avatar_url: Option<String>,
|
||||
/// The number of joined members.
|
||||
pub num_joined_members: u64,
|
||||
/// The number of active members, if known (joined + invited).
|
||||
pub num_active_members: Option<u64>,
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: Option<String>,
|
||||
pub room_type: RoomType,
|
||||
/// 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,
|
||||
/// The membership state for the current user, if known.
|
||||
pub membership: Option<Membership>,
|
||||
/// The join rule for this room (private, public, knock, etc.).
|
||||
pub join_rule: JoinRule,
|
||||
/// Whether the room is direct or not, if known.
|
||||
pub is_direct: Option<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
|
||||
),
|
||||
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(join_rule: SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
Ok(match join_rule {
|
||||
SpaceRoomJoinRule::Invite => JoinRule::Invite,
|
||||
SpaceRoomJoinRule::Knock => JoinRule::Knock,
|
||||
SpaceRoomJoinRule::Private => JoinRule::Private,
|
||||
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::Public => JoinRule::Public,
|
||||
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
|
||||
_ => {
|
||||
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
|
||||
return Err(());
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of room for a [`RoomPreviewInfo`].
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum RoomType {
|
||||
/// It's a plain chat room.
|
||||
Room,
|
||||
/// It's a space that can group several rooms.
|
||||
Space,
|
||||
/// It's a custom implementation.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<Option<&RumaRoomType>> for RoomType {
|
||||
fn from(value: Option<&RumaRoomType>) -> Self {
|
||||
match value {
|
||||
Some(RumaRoomType::Space) => RoomType::Space,
|
||||
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
|
||||
// SAFETY: this was checked in the match branch above
|
||||
value: value.unwrap().to_string(),
|
||||
},
|
||||
_ => RoomType::Room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,12 +54,43 @@ use tracing::info;
|
||||
use crate::{
|
||||
error::{ClientError, MediaInfoError},
|
||||
helpers::unwrap_or_clone_arc,
|
||||
timeline::MessageContent,
|
||||
utils::u64_to_uint,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AuthData {
|
||||
/// Password-based authentication (`m.login.password`).
|
||||
Password { password_details: AuthDataPasswordDetails },
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct AuthDataPasswordDetails {
|
||||
/// One of the user's identifiers.
|
||||
identifier: String,
|
||||
|
||||
/// The plaintext password.
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl From<AuthData> for ruma::api::client::uiaa::AuthData {
|
||||
fn from(value: AuthData) -> ruma::api::client::uiaa::AuthData {
|
||||
match value {
|
||||
AuthData::Password { password_details } => {
|
||||
let user_id = ruma::UserId::parse(password_details.identifier).unwrap();
|
||||
|
||||
ruma::api::client::uiaa::AuthData::Password(ruma::api::client::uiaa::Password::new(
|
||||
user_id.into(),
|
||||
password_details.password,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a matrix entity from a given URI, be it either
|
||||
/// a `matrix.to` link or a `matrix:` URI
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
|
||||
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
|
||||
return Some(MatrixEntity {
|
||||
@@ -123,33 +154,33 @@ impl From<&RumaMatrixId> for MatrixId {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_new(
|
||||
msgtype: MessageType,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_markdown(
|
||||
md: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_markdown_as_emote(
|
||||
md: String,
|
||||
) -> Arc<RoomMessageEventContentWithoutRelation> {
|
||||
Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_html(
|
||||
body: String,
|
||||
html_body: String,
|
||||
@@ -159,7 +190,7 @@ pub fn message_event_content_from_html(
|
||||
)))
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_from_html_as_emote(
|
||||
body: String,
|
||||
html_body: String,
|
||||
@@ -197,6 +228,7 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Mentions {
|
||||
pub user_ids: Vec<String>,
|
||||
pub room: bool,
|
||||
@@ -230,6 +262,23 @@ pub enum MessageType {
|
||||
Other { msgtype: String, body: String },
|
||||
}
|
||||
|
||||
/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
|
||||
/// If the filename field is present in a media message, clients should treat
|
||||
/// body as a caption instead of a file name. Otherwise, the body is the
|
||||
/// file name.
|
||||
///
|
||||
/// So:
|
||||
/// - if a media has a filename and a caption, the body is the caption, filename
|
||||
/// is its own field.
|
||||
/// - if a media only has a filename, then body is the filename.
|
||||
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
|
||||
if let Some(caption) = caption {
|
||||
(caption, Some(filename))
|
||||
} else {
|
||||
(filename, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
@@ -241,35 +290,39 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
}))
|
||||
}
|
||||
MessageType::Image { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaImageMessageEventContent::new(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;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::Image(event_content)
|
||||
}
|
||||
MessageType::Audio { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaAudioMessageEventContent::new(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;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::Audio(event_content)
|
||||
}
|
||||
MessageType::Video { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaVideoMessageEventContent::new(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;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::Video(event_content)
|
||||
}
|
||||
MessageType::File { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
|
||||
RumaFileMessageEventContent::new(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;
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
Self::File(event_content)
|
||||
}
|
||||
MessageType::Notice { content } => {
|
||||
@@ -303,18 +356,18 @@ 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(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
@@ -323,18 +376,18 @@ 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(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
},
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
content: FileMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
@@ -408,18 +461,20 @@ pub struct EmoteMessageContent {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ImageMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
pub formatted_caption: Option<FormattedBody>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<ImageInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct AudioMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
pub formatted_caption: Option<FormattedBody>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<AudioInfo>,
|
||||
pub audio: Option<UnstableAudioDetailsContent>,
|
||||
@@ -428,18 +483,20 @@ pub struct AudioMessageContent {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct VideoMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
pub formatted_caption: Option<FormattedBody>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<VideoInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct FileMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
/// The computed filename, for use in a client.
|
||||
pub filename: String,
|
||||
pub caption: Option<String>,
|
||||
pub formatted_caption: Option<FormattedBody>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<FileInfo>,
|
||||
}
|
||||
@@ -804,7 +861,7 @@ impl From<&RumaFileInfo> for FileInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum PollKind {
|
||||
Disclosed,
|
||||
Undisclosed,
|
||||
@@ -831,3 +888,13 @@ impl From<RumaPollKind> for PollKind {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
|
||||
/// [`MessageContent`] value.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn content_without_relation_from_message(
|
||||
message: MessageContent,
|
||||
) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
|
||||
let msg_type = message.msg_type.try_into()?;
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msg_type)))
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption::{
|
||||
identities::UserIdentity,
|
||||
verification::{SasState, SasVerification, VerificationRequest},
|
||||
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
|
||||
Encryption,
|
||||
},
|
||||
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
|
||||
};
|
||||
use ruma::UserId;
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::error::ClientError;
|
||||
@@ -20,7 +21,7 @@ pub struct SessionVerificationEmoji {
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SessionVerificationEmoji {
|
||||
pub fn symbol(&self) -> String {
|
||||
self.symbol.clone()
|
||||
@@ -37,8 +38,20 @@ pub enum SessionVerificationData {
|
||||
Decimals { values: Vec<u16> },
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
/// Details about the incoming verification request
|
||||
#[derive(Debug, uniffi::Record)]
|
||||
pub struct SessionVerificationRequestDetails {
|
||||
sender_id: String,
|
||||
flow_id: String,
|
||||
device_id: String,
|
||||
display_name: Option<String>,
|
||||
/// First time this device was seen in milliseconds since epoch.
|
||||
first_seen_timestamp: u64,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SessionVerificationControllerDelegate: Sync + Send {
|
||||
fn did_receive_verification_request(&self, details: SessionVerificationRequestDetails);
|
||||
fn did_accept_verification_request(&self);
|
||||
fn did_start_sas_verification(&self);
|
||||
fn did_receive_verification_data(&self, data: SessionVerificationData);
|
||||
@@ -58,19 +71,53 @@ pub struct SessionVerificationController {
|
||||
sas_verification: Arc<RwLock<Option<SasVerification>>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SessionVerificationController {
|
||||
pub async fn is_verified(&self) -> Result<bool, ClientError> {
|
||||
let device =
|
||||
self.encryption.get_own_device().await?.context("Our own device is missing")?;
|
||||
|
||||
Ok(device.is_cross_signed_by_owner())
|
||||
}
|
||||
|
||||
pub fn set_delegate(&self, delegate: Option<Box<dyn SessionVerificationControllerDelegate>>) {
|
||||
*self.delegate.write().unwrap() = delegate;
|
||||
}
|
||||
|
||||
/// Set this particular request as the currently active one and register for
|
||||
/// events pertaining it.
|
||||
/// * `sender_id` - The user requesting verification.
|
||||
/// * `flow_id` - - The ID that uniquely identifies the verification flow.
|
||||
pub async fn acknowledge_verification_request(
|
||||
&self,
|
||||
sender_id: String,
|
||||
flow_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let sender_id = UserId::parse(sender_id.clone())?;
|
||||
|
||||
let verification_request = self
|
||||
.encryption
|
||||
.get_verification_request(&sender_id, flow_id)
|
||||
.await
|
||||
.ok_or(ClientError::new("Unknown session verification request"))?;
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accept the previously acknowledged verification request
|
||||
pub async fn accept_verification_request(&self) -> Result<(), ClientError> {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
if let Some(verification_request) = verification_request {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
verification_request.accept_with_methods(methods).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request verification for the current device
|
||||
pub async fn request_verification(&self) -> Result<(), ClientError> {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
let verification_request = self
|
||||
@@ -78,30 +125,41 @@ impl SessionVerificationController {
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
*self.verification_request.write().unwrap() = Some(verification_request);
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transition the current verification request into a SAS verification
|
||||
/// flow.
|
||||
pub async fn start_sas_verification(&self) -> Result<(), ClientError> {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
|
||||
if let Some(verification) = verification_request {
|
||||
match verification.start_sas().await {
|
||||
Ok(Some(verification)) => {
|
||||
*self.sas_verification.write().unwrap() = Some(verification.clone());
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
match verification_request.start_sas().await {
|
||||
Ok(Some(verification)) => {
|
||||
*self.sas_verification.write().unwrap() = Some(verification.clone());
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_changes(delegate, verification));
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
_ => {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
}
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
|
||||
}
|
||||
_ => {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,31 +167,37 @@ impl SessionVerificationController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Confirm that the short auth strings match on both sides.
|
||||
pub async fn approve_verification(&self) -> Result<(), ClientError> {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
if let Some(sas_verification) = sas_verification {
|
||||
sas_verification.confirm().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
};
|
||||
|
||||
Ok(sas_verification.confirm().await?)
|
||||
}
|
||||
|
||||
/// Reject the short auth string
|
||||
pub async fn decline_verification(&self) -> Result<(), ClientError> {
|
||||
let sas_verification = self.sas_verification.read().unwrap().clone();
|
||||
if let Some(sas_verification) = sas_verification {
|
||||
sas_verification.mismatch().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let Some(sas_verification) = sas_verification else {
|
||||
return Err(ClientError::new("SAS verification missing"));
|
||||
};
|
||||
|
||||
Ok(sas_verification.mismatch().await?)
|
||||
}
|
||||
|
||||
/// Cancel the current verification request
|
||||
pub async fn cancel_verification(&self) -> Result<(), ClientError> {
|
||||
let verification_request = self.verification_request.read().unwrap().clone();
|
||||
if let Some(verification) = verification_request {
|
||||
verification.cancel().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let Some(verification_request) = verification_request else {
|
||||
return Err(ClientError::new("Verification request missing."));
|
||||
};
|
||||
|
||||
Ok(verification_request.cancel().await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,58 +213,88 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
|
||||
match event {
|
||||
// TODO: Use the changes stream for this as well once we expose
|
||||
// VerificationRequest::changes() in the main crate.
|
||||
AnyToDeviceEvent::KeyVerificationStart(event) => {
|
||||
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
|
||||
return;
|
||||
}
|
||||
if let Some(verification) = self
|
||||
.encryption
|
||||
.get_verification(
|
||||
self.user_identity.user_id(),
|
||||
event.content.transaction_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(sas_verification) = verification.sas() {
|
||||
*self.sas_verification.write().unwrap() = Some(sas_verification.clone());
|
||||
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
|
||||
info!("Received verification request: {:}", event.sender);
|
||||
|
||||
if sas_verification.accept().await.is_ok() {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
let Some(request) = self
|
||||
.encryption
|
||||
.get_verification_request(&event.sender, &event.content.transaction_id)
|
||||
.await
|
||||
else {
|
||||
error!("Failed retrieving verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_changes(delegate, sas_verification));
|
||||
} else if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
if !request.is_self_verification() {
|
||||
info!("Received non-self verification request. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
|
||||
else {
|
||||
error!("Received key verification event but the request is in the wrong state.");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_id: request.other_user_id().into(),
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
display_name: other_device_data.display_name().map(str::to_string),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().get().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_verification_request_changes(
|
||||
verification_request: VerificationRequest,
|
||||
sas_verification: Arc<RwLock<Option<SasVerification>>>,
|
||||
delegate: Delegate,
|
||||
) {
|
||||
let mut stream = verification_request.changes();
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
match state {
|
||||
VerificationRequestState::Transitioned { verification } => {
|
||||
let Some(verification) = verification.sas() else {
|
||||
error!("Invalid, non-sas verification flow. Returning.");
|
||||
return;
|
||||
};
|
||||
|
||||
*sas_verification.write().unwrap() = Some(verification.clone());
|
||||
|
||||
if verification.accept().await.is_ok() {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_start_sas_verification()
|
||||
}
|
||||
|
||||
let delegate = delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
|
||||
verification,
|
||||
delegate,
|
||||
));
|
||||
} else if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationReady(event) => {
|
||||
if !self.is_transaction_id_valid(event.content.transaction_id.to_string()) {
|
||||
return;
|
||||
VerificationRequestState::Ready { .. } => {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_accept_verification_request()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_accept_verification_request()
|
||||
VerificationRequestState::Cancelled(..) => {
|
||||
if let Some(delegate) = &*delegate.read().unwrap() {
|
||||
delegate.did_cancel();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_transaction_id_valid(&self, transaction_id: String) -> bool {
|
||||
match &*self.verification_request.read().unwrap() {
|
||||
Some(verification) => verification.flow_id() == transaction_id,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_changes(delegate: Delegate, sas: SasVerification) {
|
||||
async fn listen_to_sas_verification_changes(sas: SasVerification, delegate: Delegate) {
|
||||
let mut stream = sas.changes();
|
||||
|
||||
while let Some(state) = stream.next().await {
|
||||
|
||||
@@ -51,7 +51,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SyncServiceStateObserver: Send + Sync + Debug {
|
||||
fn on_update(&self, state: SyncServiceState);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ pub struct SyncService {
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SyncService {
|
||||
pub fn room_list_service(&self) -> Arc<RoomListService> {
|
||||
Arc::new(RoomListService {
|
||||
@@ -110,11 +110,11 @@ impl SyncServiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SyncServiceBuilder {
|
||||
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
|
||||
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_cross_process_lock(app_identifier);
|
||||
let builder = this.builder.with_cross_process_lock();
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ impl SyncServiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait UnableToDecryptDelegate: Sync + Send {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ impl TaskHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TaskHandle {
|
||||
// Cancel a task handle.
|
||||
pub fn cancel(&self) {
|
||||
|
||||
@@ -15,52 +15,53 @@
|
||||
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 matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
|
||||
use ruma::events::{room::MediaSource, FullStateEventContent};
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::ruma::{ImageInfo, MessageType, PollKind};
|
||||
use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent);
|
||||
|
||||
#[uniffi::export]
|
||||
impl TimelineItemContent {
|
||||
pub fn kind(&self) -> TimelineItemContentKind {
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
|
||||
match &self.0 {
|
||||
Content::Message(_) => TimelineItemContentKind::Message,
|
||||
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
|
||||
match value {
|
||||
Content::Message(message) => TimelineItemContent::Message { content: message.into() },
|
||||
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
TimelineItemContentKind::Sticker {
|
||||
TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
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::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::CallNotify => TimelineItemContent::CallNotify,
|
||||
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
|
||||
TimelineItemContent::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::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
FullStateEventContent::Original { content, .. } => content.reason.clone(),
|
||||
_ => None,
|
||||
};
|
||||
TimelineItemContent::RoomMembership {
|
||||
user_id: membership.user_id().to_string(),
|
||||
user_display_name: membership.display_name(),
|
||||
change: membership.change().map(Into::into),
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
Content::ProfileChange(profile) => {
|
||||
let (display_name, prev_display_name) = profile
|
||||
.displayname_change()
|
||||
@@ -75,42 +76,74 @@ impl TimelineItemContent {
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
TimelineItemContentKind::ProfileChange {
|
||||
TimelineItemContent::ProfileChange {
|
||||
display_name: display_name.flatten(),
|
||||
prev_display_name: prev_display_name.flatten(),
|
||||
avatar_url: avatar_url.flatten(),
|
||||
prev_avatar_url: prev_avatar_url.flatten(),
|
||||
}
|
||||
}
|
||||
Content::OtherState(state) => TimelineItemContentKind::State {
|
||||
|
||||
Content::OtherState(state) => TimelineItemContent::State {
|
||||
state_key: state.state_key().to_owned(),
|
||||
content: state.content().into(),
|
||||
},
|
||||
|
||||
Content::FailedToParseMessageLike { event_type, error } => {
|
||||
TimelineItemContentKind::FailedToParseMessageLike {
|
||||
TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: event_type.to_string(),
|
||||
error: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
Content::FailedToParseState { event_type, state_key, error } => {
|
||||
TimelineItemContentKind::FailedToParseState {
|
||||
TimelineItemContent::FailedToParseState {
|
||||
event_type: event_type.to_string(),
|
||||
state_key: state_key.to_string(),
|
||||
state_key,
|
||||
error: error.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct MessageContent {
|
||||
pub msg_type: MessageType,
|
||||
pub body: String,
|
||||
pub in_reply_to: Option<Arc<InReplyToDetails>>,
|
||||
pub thread_root: Option<String>,
|
||||
pub is_edited: bool,
|
||||
pub mentions: Option<Mentions>,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
|
||||
Self {
|
||||
msg_type: value.msgtype().clone().into(),
|
||||
body: value.body().to_owned(),
|
||||
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
|
||||
is_edited: value.is_edited(),
|
||||
thread_root: value.thread_root().map(|id| id.to_string()),
|
||||
mentions: value.mentions().cloned().map(|m| m.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineItemContentKind {
|
||||
Message,
|
||||
impl From<ruma::events::Mentions> for Mentions {
|
||||
fn from(value: ruma::events::Mentions) -> Self {
|
||||
Self {
|
||||
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
|
||||
room: value.room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum TimelineItemContent {
|
||||
Message {
|
||||
content: MessageContent,
|
||||
},
|
||||
RedactedMessage,
|
||||
Sticker {
|
||||
body: String,
|
||||
@@ -135,6 +168,7 @@ pub enum TimelineItemContentKind {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
change: Option<MembershipChange>,
|
||||
reason: Option<String>,
|
||||
},
|
||||
ProfileChange {
|
||||
display_name: Option<String>,
|
||||
@@ -158,36 +192,6 @@ pub enum TimelineItemContentKind {
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct Message(matrix_sdk_ui::timeline::Message);
|
||||
|
||||
#[uniffi::export]
|
||||
impl Message {
|
||||
pub fn msgtype(&self) -> MessageType {
|
||||
self.0.msgtype().clone().into()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> String {
|
||||
self.0.msgtype().body().to_owned()
|
||||
}
|
||||
|
||||
pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
|
||||
self.0.in_reply_to().map(InReplyToDetails::from)
|
||||
}
|
||||
|
||||
pub fn is_threaded(&self) -> bool {
|
||||
self.0.is_threaded()
|
||||
}
|
||||
|
||||
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)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: RepliedToEventDetails,
|
||||
@@ -199,14 +203,25 @@ impl InReplyToDetails {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl InReplyToDetails {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.event_id.clone()
|
||||
}
|
||||
|
||||
pub fn event(&self) -> RepliedToEventDetails {
|
||||
self.event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
let event_id = inner.event_id.to_string();
|
||||
let event = match &inner.event {
|
||||
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
|
||||
TimelineDetails::Pending => RepliedToEventDetails::Pending,
|
||||
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
|
||||
content: Arc::new(TimelineItemContent(event.content().to_owned())),
|
||||
content: event.content().clone().into(),
|
||||
sender: event.sender().to_string(),
|
||||
sender_profile: event.sender_profile().into(),
|
||||
},
|
||||
@@ -219,11 +234,11 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RepliedToEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: Arc<TimelineItemContent>, sender: String, sender_profile: ProfileDetails },
|
||||
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
@@ -267,7 +282,6 @@ impl EncryptedMessage {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct Reaction {
|
||||
pub key: String,
|
||||
pub count: u64,
|
||||
pub senders: Vec<ReactionSenderData>,
|
||||
}
|
||||
|
||||
@@ -337,7 +351,7 @@ pub enum OtherState {
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomName { name: Option<String> },
|
||||
RoomPinnedEvents,
|
||||
RoomPinnedEvents { change: RoomPinnedEventsChange },
|
||||
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite { display_name: Option<String> },
|
||||
@@ -380,7 +394,7 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
};
|
||||
Self::RoomName { name }
|
||||
}
|
||||
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
|
||||
Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
|
||||
Content::RoomPowerLevels(c) => match c {
|
||||
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
|
||||
users: power_level_user_changes(content, prev_content)
|
||||
@@ -418,15 +432,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct PollAnswer {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl From<PollResult> for TimelineItemContentKind {
|
||||
impl From<PollResult> for TimelineItemContent {
|
||||
fn from(value: PollResult) -> Self {
|
||||
TimelineItemContentKind::Poll {
|
||||
TimelineItemContent::Poll {
|
||||
question: value.question,
|
||||
kind: PollKind::from(value.kind),
|
||||
max_selections: value.max_selections,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
#[uniffi::constructor]
|
||||
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
|
||||
@@ -19,7 +19,7 @@ use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
/// level + target) it is called with. Please make sure that the number of
|
||||
/// different combinations of those parameters this can be called with is
|
||||
/// constant in the final executable.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn log_event(file: String, line: Option<u32>, level: LogLevel, target: String, message: String) {
|
||||
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
@@ -96,7 +96,7 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Span(tracing::Span);
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Span {
|
||||
/// Create a span originating at the given callsite (file, line and column).
|
||||
///
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use ruma::UInt;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -21,3 +24,45 @@ pub(crate) fn u64_to_uint(u: u64) -> UInt {
|
||||
UInt::MAX
|
||||
})
|
||||
}
|
||||
|
||||
/// Tiny wrappers for data types that must be dropped in the context of an async
|
||||
/// runtime.
|
||||
///
|
||||
/// This is useful whenever such a data type may transitively call some
|
||||
/// runtime's `block_on` function in their `Drop` impl (since we lack async drop
|
||||
/// at the moment), like done in some `deadpool` drop impls.
|
||||
pub(crate) struct AsyncRuntimeDropped<T>(ManuallyDrop<T>);
|
||||
|
||||
impl<T> AsyncRuntimeDropped<T> {
|
||||
/// Create a new wrapper for this type that will be dropped under an async
|
||||
/// runtime.
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(ManuallyDrop::new(val))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for AsyncRuntimeDropped<T> {
|
||||
fn drop(&mut self) {
|
||||
let _guard = RUNTIME.enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What is an `AsyncRuntimeDropped<T>`, if not a `T` in disguise?
|
||||
impl<T> Deref for AsyncRuntimeDropped<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for AsyncRuntimeDropped<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use matrix_sdk::{
|
||||
async_trait,
|
||||
widget::{MessageLikeEventFilter, StateEventFilter},
|
||||
};
|
||||
use ruma::events::MessageLikeEventType;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{room::Room, RUNTIME};
|
||||
@@ -15,7 +16,7 @@ pub struct WidgetDriverAndHandle {
|
||||
pub handle: Arc<WidgetDriverHandle>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHandle, ParseError> {
|
||||
let (driver, handle) = matrix_sdk::widget::WidgetDriver::new(settings.try_into()?);
|
||||
Ok(WidgetDriverAndHandle {
|
||||
@@ -29,7 +30,7 @@ pub fn make_widget_driver(settings: WidgetSettings) -> Result<WidgetDriverAndHan
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct WidgetDriver(Mutex<Option<matrix_sdk::widget::WidgetDriver>>);
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl WidgetDriver {
|
||||
pub async fn run(
|
||||
&self,
|
||||
@@ -96,7 +97,7 @@ impl From<matrix_sdk::widget::WidgetSettings> for WidgetSettings {
|
||||
/// * `room` - A matrix room which is used to query the logged in username
|
||||
/// * `props` - Properties from the client that can be used by a widget to adapt
|
||||
/// to the client. e.g. language, font-scale...
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub async fn generate_webview_url(
|
||||
widget_settings: WidgetSettings,
|
||||
room: Arc<Room>,
|
||||
@@ -241,7 +242,7 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
///
|
||||
/// * `props` - A struct containing the configuration parameters for a element
|
||||
/// call widget.
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn new_virtual_element_call_widget(
|
||||
props: VirtualElementCallWidgetOptions,
|
||||
) -> Result<WidgetSettings, ParseError> {
|
||||
@@ -261,42 +262,84 @@ pub fn new_virtual_element_call_widget(
|
||||
/// Editing and extending the capabilities from this function is also possible,
|
||||
/// but should only be done as temporal workarounds until this function is
|
||||
/// adjusted
|
||||
#[uniffi::export]
|
||||
pub fn get_element_call_required_permissions(own_user_id: String) -> WidgetCapabilities {
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn get_element_call_required_permissions(
|
||||
own_user_id: String,
|
||||
own_device_id: String,
|
||||
) -> WidgetCapabilities {
|
||||
use ruma::events::StateEventType;
|
||||
|
||||
let read_send = vec![
|
||||
// To read and send rageshake requests from other room members
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
// To read and send encryption keys
|
||||
// TODO change this to the appropriate to-device version once ready
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
// To read and send custom EC reactions. They are different to normal `m.reaction`
|
||||
// because they can be send multiple times to the same event.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.reaction".to_owned(),
|
||||
},
|
||||
// This allows send raise hand reactions.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::Reaction.to_string(),
|
||||
},
|
||||
// This allows to detect if someone does not raise their hand anymore.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::RoomRedaction.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
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() },
|
||||
// To compute the current state of the matrixRTC session.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::CallMember.to_string() },
|
||||
// To detect leaving/kicked room members during a call.
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomMember.to_string() },
|
||||
// To decide whether to encrypt the call streams based on the room encryption setting.
|
||||
WidgetEventFilter::StateWithType {
|
||||
event_type: StateEventType::RoomEncryption.to_string(),
|
||||
},
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
],
|
||||
// This allows the widget to check the room version, so it can know about
|
||||
// version-specific auth rules (namely MSC3779).
|
||||
WidgetEventFilter::StateWithType { event_type: StateEventType::RoomCreate.to_string() },
|
||||
]
|
||||
.into_iter()
|
||||
.chain(read_send.clone())
|
||||
.collect(),
|
||||
send: vec, with no leading underscore.
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
// The same as above but with an underscore.
|
||||
// To work around the issue that state events starting with `@` have to be matrix id's
|
||||
// but we use mxId+deviceId.
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("_{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.chain(read_send)
|
||||
.collect(),
|
||||
requires_client: true,
|
||||
update_delayed_event: true,
|
||||
send_delayed_event: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +369,7 @@ impl From<ClientProperties> for matrix_sdk::widget::ClientProperties {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct WidgetDriverHandle(matrix_sdk::widget::WidgetDriverHandle);
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl WidgetDriverHandle {
|
||||
/// Receive a message from the widget driver.
|
||||
///
|
||||
@@ -358,6 +401,10 @@ pub struct WidgetCapabilities {
|
||||
/// This means clients should not offer to open the widget in a separate
|
||||
/// browser/tab/webview that is not connected to the postmessage widget-api.
|
||||
pub requires_client: bool,
|
||||
/// This allows the widget to ask the client to update delayed events.
|
||||
pub update_delayed_event: bool,
|
||||
/// This allows the widget to send events with a delay.
|
||||
pub send_delayed_event: bool,
|
||||
}
|
||||
|
||||
impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
|
||||
@@ -366,6 +413,8 @@ impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
|
||||
read: value.read.into_iter().map(Into::into).collect(),
|
||||
send: value.send.into_iter().map(Into::into).collect(),
|
||||
requires_client: value.requires_client,
|
||||
update_delayed_event: value.update_delayed_event,
|
||||
send_delayed_event: value.send_delayed_event,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,12 +425,14 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
|
||||
read: value.read.into_iter().map(Into::into).collect(),
|
||||
send: value.send.into_iter().map(Into::into).collect(),
|
||||
requires_client: value.requires_client,
|
||||
update_delayed_event: value.update_delayed_event,
|
||||
send_delayed_event: value.send_delayed_event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Different kinds of filters that could be applied to the timeline events.
|
||||
#[derive(uniffi::Enum)]
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum WidgetEventFilter {
|
||||
/// Matches message-like events with the given `type`.
|
||||
MessageLikeWithType { event_type: String },
|
||||
@@ -433,7 +484,7 @@ impl From<matrix_sdk::widget::EventFilter> for WidgetEventFilter {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait WidgetCapabilitiesProvider: Send + Sync {
|
||||
fn acquire_capabilities(&self, capabilities: WidgetCapabilities) -> WidgetCapabilities;
|
||||
}
|
||||
@@ -504,3 +555,54 @@ impl From<url::ParseError> for ParseError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk::widget::Capabilities;
|
||||
|
||||
use super::get_element_call_required_permissions;
|
||||
|
||||
#[test]
|
||||
fn element_call_permissions_are_correct() {
|
||||
let widget_cap = get_element_call_required_permissions(
|
||||
"@my_user:my_domain.org".to_owned(),
|
||||
"ABCDEFGHI".to_owned(),
|
||||
);
|
||||
|
||||
// We test two things:
|
||||
|
||||
// Converting the WidgetCapability (ffi struct) to Capabilities (rust sdk
|
||||
// struct)
|
||||
let cap = Into::<Capabilities>::into(widget_cap);
|
||||
// Converting Capabilities (rust sdk struct) to a json list.
|
||||
let cap_json_repr = serde_json::to_string(&cap).unwrap();
|
||||
|
||||
// Converting to a Vec<String> allows to check if the required elements exist
|
||||
// without breaking the test each time the order of permissions might
|
||||
// change.
|
||||
let permission_array: Vec<String> = serde_json::from_str(&cap_json_repr).unwrap();
|
||||
|
||||
let cap_assert = |capability: &str| {
|
||||
assert!(
|
||||
permission_array.contains(&capability.to_owned()),
|
||||
"The \"{}\" capability was missing from the element call capability list.",
|
||||
capability
|
||||
);
|
||||
};
|
||||
|
||||
cap_assert("io.element.requires_client");
|
||||
cap_assert("org.matrix.msc4157.update_delayed_event");
|
||||
cap_assert("org.matrix.msc4157.send.delayed_event");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:org.matrix.msc3401.call.member");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.member");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.encryption");
|
||||
cap_assert("org.matrix.msc2762.receive.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.receive.event:io.element.call.encryption_keys");
|
||||
cap_assert("org.matrix.msc2762.receive.state_event:m.room.create");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI");
|
||||
cap_assert("org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI");
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# This git-cliff configuration file is used to generate weekly reports for This
|
||||
# Week in Matrix amongst others.
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }})
|
||||
"""
|
||||
body = """
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {{ commit_message | upper_first }}
|
||||
{% endfor %}
|
||||
"""
|
||||
trim = true
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
filter_commits = true
|
||||
tag_pattern = "[0-9]*"
|
||||
skip_tags = ""
|
||||
ignore_tags = ""
|
||||
date_order = false
|
||||
sort_commits = "newest"
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# This git-cliff configuration file is used to generate release reports.
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% set_global breaking = commit.breaking -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "security-impact" -%}
|
||||
{% set_global security_impact = footer.value -%}
|
||||
{% elif footer.token | lower == "cve" -%}
|
||||
{% set_global cve = footer.value -%}
|
||||
{% elif footer.token | lower == "github-advisory" -%}
|
||||
{% set_global github_advisory = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }}
|
||||
{% if security_impact -%}
|
||||
(\
|
||||
*{{ security_impact | upper_first }}*\
|
||||
{% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\
|
||||
{% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%}
|
||||
)
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ footer = "Security-Impact:", group = "Security" },
|
||||
{ footer = "CVE:", group = "Security" },
|
||||
{ footer = "GitHub-Advisory:", group = "Security" },
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
# forbid parsers from skipping breaking changes
|
||||
protect_breaking_commits = true
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = true
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = ""
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags chronologically
|
||||
date_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
@@ -1,15 +1,81 @@
|
||||
# unreleased
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add more invalid characters for room aliases.
|
||||
|
||||
- Use the `DisplayName` struct to protect against homoglyph attacks.
|
||||
|
||||
|
||||
### Features
|
||||
- Add `BaseClient::room_key_recipient_strategy` field
|
||||
|
||||
- `AmbiguityCache` contains the room member's user ID.
|
||||
|
||||
- [**breaking**] `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to
|
||||
request an animated thumbnail They both take a `MediaThumbnailSettings`
|
||||
instead of `MediaThumbnailSize`.
|
||||
|
||||
- Consider knocked members to be part of the room for display name
|
||||
disambiguation.
|
||||
|
||||
- `Client::cross_process_store_locks_holder_name` is used everywhere:
|
||||
- `StoreConfig::new()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `StoreConfig` no longer implements `Default`.
|
||||
- `BaseClient::new()` has been removed.
|
||||
- `BaseClient::clone_with_in_memory_state_store()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `BaseClient` no longer implements `Default`.
|
||||
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
|
||||
- `BuilderStoreConfig` no longer has
|
||||
`cross_process_store_locks_holder_name` field for `Sqlite` and
|
||||
`IndexedDb`.
|
||||
|
||||
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
|
||||
|
||||
- Allow aborting media uploads.
|
||||
|
||||
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges`
|
||||
by a custom one.
|
||||
|
||||
- Introduce a `DisplayName` struct which normalizes and sanitizes
|
||||
display names.
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Rename `DisplayName` to `RoomDisplayName`.
|
||||
|
||||
- Rename `AmbiguityMap` to `DisplayNameUsers`.
|
||||
|
||||
- Move `event_cache_store/` to `event_cache/store/` in `matrix-sdk-base`.
|
||||
|
||||
- Move `linked_chunk` from `matrix-sdk` to `matrix-sdk-common`.
|
||||
|
||||
- Move `Event` and `Gap` into `matrix_sdk_base::event_cache`.
|
||||
|
||||
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`.
|
||||
|
||||
- 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
|
||||
|
||||
- [**breaking**] `Client::get_rooms` and `Client::get_rooms_filtered` are renamed
|
||||
`Client::rooms` and `Client::rooms_filtered`.
|
||||
- `Client::get_stripped_rooms` has finally been removed.
|
||||
|
||||
- [**breaking**] `Client::get_stripped_rooms` has finally been removed.
|
||||
|
||||
- [**breaking**] The `StateStore` methods to access data in the media cache
|
||||
where moved to a separate `EventCacheStore` trait.
|
||||
|
||||
- [**breaking**] The `instant` module was removed, use the `ruma::time` module instead.
|
||||
|
||||
# 0.7.0
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -21,9 +21,19 @@ e2e-encryption = ["dep:matrix-sdk-crypto"]
|
||||
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
|
||||
qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
message-ids = ["matrix-sdk-crypto?/message-ids"]
|
||||
experimental-sliding-sync = ["ruma/unstable-msc3575"]
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi"]
|
||||
experimental-sliding-sync = [
|
||||
"ruma/unstable-msc3575",
|
||||
"ruma/unstable-msc4186",
|
||||
]
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = []
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
|
||||
# helpers for testing features build upon this
|
||||
testing = [
|
||||
@@ -40,6 +50,7 @@ assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.4.0", features = ["serde"] }
|
||||
decancer = "3.2.4"
|
||||
eyeball = { workspace = true }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
@@ -50,7 +61,9 @@ 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", "unstable-msc2867"] }
|
||||
regex = "1.11.0"
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
|
||||
unicode-normalization = "0.1.24"
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -66,7 +79,7 @@ futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
web-time = "1.1.0"
|
||||
similar-asserts = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,10 @@
|
||||
use std::fmt;
|
||||
|
||||
pub use matrix_sdk_common::debug::*;
|
||||
use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::{InvitedRoom, KnockedRoom},
|
||||
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.
|
||||
@@ -46,6 +49,20 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a knocked on 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 `knock_state`.
|
||||
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("KnockedRoom")
|
||||
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
|
||||
//! SDK-specific variations of response types from Ruma.
|
||||
|
||||
use std::{collections::BTreeMap, fmt, iter};
|
||||
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
|
||||
|
||||
pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
@@ -31,6 +33,7 @@ use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
/// A change in ambiguity of room members that an `m.room.member` event
|
||||
/// triggers.
|
||||
@@ -67,6 +70,178 @@ pub struct AmbiguityChanges {
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::MXID_PATTERN)
|
||||
.expect("We should be able to create a regex from our static MXID pattern")
|
||||
});
|
||||
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
|
||||
.expect("We should be able to create a regex from our static left-to-right pattern")
|
||||
});
|
||||
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
|
||||
.expect("We should be able to create a regex from our static hidden characters pattern")
|
||||
});
|
||||
|
||||
/// Regex to match `i` characters.
|
||||
///
|
||||
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
|
||||
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
|
||||
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
|
||||
});
|
||||
|
||||
/// Regex to match `0` characters.
|
||||
///
|
||||
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
|
||||
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
|
||||
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
|
||||
});
|
||||
|
||||
/// Regex to match a couple of dot-like characters, also matches an actual dot.
|
||||
///
|
||||
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
|
||||
/// ambiguous.
|
||||
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
|
||||
});
|
||||
|
||||
/// A high-level wrapper for strings representing display names.
|
||||
///
|
||||
/// This wrapper provides attempts to determine whether a display name
|
||||
/// contains characters that could make it ambiguous or easily confused
|
||||
/// with similar names.
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk_base::deserialized_responses::DisplayName;
|
||||
///
|
||||
/// let display_name = DisplayName::new("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
///
|
||||
/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str().
|
||||
/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla"));
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::deserialized_responses::DisplayName;
|
||||
/// let display_name = DisplayName::new("@alice:localhost");
|
||||
///
|
||||
/// // The display name looks like an MXID, which makes it ambiguous.
|
||||
/// assert!(display_name.is_inherently_ambiguous());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub struct DisplayName {
|
||||
raw: String,
|
||||
decancered: Option<String>,
|
||||
}
|
||||
|
||||
impl Hash for DisplayName {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
if let Some(decancered) = &self.decancered {
|
||||
decancered.hash(state);
|
||||
} else {
|
||||
self.raw.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DisplayName {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self.decancered.as_deref(), other.decancered.as_deref()) {
|
||||
(None, None) => self.raw == other.raw,
|
||||
(None, Some(_)) | (Some(_), None) => false,
|
||||
(Some(this), Some(other)) => this == other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayName {
|
||||
/// Regex pattern matching an MXID.
|
||||
const MXID_PATTERN: &str = "@.+[:.].+";
|
||||
|
||||
/// Regex pattern matching some left-to-right formatting marks:
|
||||
/// * LTR and RTL marks U+200E and U+200F
|
||||
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
|
||||
const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
|
||||
|
||||
/// Regex pattern matching bunch of unicode control characters and otherwise
|
||||
/// misleading/invisible characters.
|
||||
///
|
||||
/// This includes:
|
||||
/// * various width spaces U+2000 - U+200D
|
||||
/// * Combining characters U+0300 - U+036F
|
||||
/// * Blank/invisible characters (U2800, U2062-U2063)
|
||||
/// * Arabic Letter RTL mark U+061C
|
||||
/// * Zero width no-break space (BOM) U+FEFF
|
||||
const HIDDEN_CHARACTERS_PATTERN: &str =
|
||||
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
|
||||
|
||||
/// Creates a new [`DisplayName`] from the given raw string.
|
||||
///
|
||||
/// The raw display name is transformed into a Unicode-normalized form, with
|
||||
/// common confusable characters removed to reduce ambiguity.
|
||||
///
|
||||
/// **Note**: If removing confusable characters fails,
|
||||
/// [`DisplayName::is_inherently_ambiguous`] will return `true`, and
|
||||
/// [`DisplayName::as_normalized_str()`] will return `None.
|
||||
pub fn new(raw: &str) -> Self {
|
||||
let normalized = raw.nfd().collect::<String>();
|
||||
let replaced = DOT_REGEX.replace_all(&normalized, ":");
|
||||
let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
|
||||
|
||||
let decancered = decancer::cure!(&replaced).ok().map(|cured| {
|
||||
let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
|
||||
let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
|
||||
// We re-run the dot replacement because decancer normalized a lot of weird
|
||||
// characets into a `.`, it just doesn't do that for /u{1d16d}.
|
||||
let replaced = DOT_REGEX.replace_all(&replaced, ":");
|
||||
let replaced = ZERO_REGEX.replace_all(&replaced, "o");
|
||||
|
||||
replaced.to_string()
|
||||
});
|
||||
|
||||
Self { raw: raw.to_owned(), decancered }
|
||||
}
|
||||
|
||||
/// Is this display name considered to be ambiguous?
|
||||
///
|
||||
/// If the display name has cancer (i.e. fails normalisation or has a
|
||||
/// different normalised form) or looks like an MXID, then it's ambiguous.
|
||||
pub fn is_inherently_ambiguous(&self) -> bool {
|
||||
// If we look like an MXID or have hidden characters then we're ambiguous.
|
||||
self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
|
||||
}
|
||||
|
||||
/// Returns the underlying raw and and unsanitized string of this
|
||||
/// [`DisplayName`].
|
||||
pub fn as_raw_str(&self) -> &str {
|
||||
&self.raw
|
||||
}
|
||||
|
||||
/// Returns the underlying normalized and and sanitized string of this
|
||||
/// [`DisplayName`].
|
||||
///
|
||||
/// Returns `None` if normalization failed during construction of this
|
||||
/// [`DisplayName`].
|
||||
pub fn as_normalized_str(&self) -> Option<&str> {
|
||||
self.decancered.as_deref()
|
||||
}
|
||||
|
||||
fn has_hidden_characters(&self) -> bool {
|
||||
HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
|
||||
}
|
||||
|
||||
fn looks_like_an_mxid(&self) -> bool {
|
||||
self.decancered
|
||||
.as_deref()
|
||||
.map(|d| MXID_REGEX.is_match(d))
|
||||
.unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
|
||||
}
|
||||
}
|
||||
|
||||
/// A deserialized response for the rooms members API call.
|
||||
///
|
||||
/// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers)
|
||||
@@ -294,10 +469,12 @@ impl MemberEvent {
|
||||
///
|
||||
/// It there is no `displayname` in the event's content, the localpart or
|
||||
/// the user ID is returned.
|
||||
pub fn display_name(&self) -> &str {
|
||||
self.original_content()
|
||||
.and_then(|c| c.displayname.as_deref())
|
||||
.unwrap_or_else(|| self.user_id().localpart())
|
||||
pub fn display_name(&self) -> DisplayName {
|
||||
DisplayName::new(
|
||||
self.original_content()
|
||||
.and_then(|c| c.displayname.as_deref())
|
||||
.unwrap_or_else(|| self.user_id().localpart()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,3 +487,240 @@ impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
macro_rules! assert_display_name_eq {
|
||||
($left:expr, $right:expr $(, $desc:expr)?) => {{
|
||||
let left = crate::deserialized_responses::DisplayName::new($left);
|
||||
let right = crate::deserialized_responses::DisplayName::new($right);
|
||||
|
||||
similar_asserts::assert_eq!(
|
||||
left,
|
||||
right
|
||||
$(, $desc)?
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_display_name_ne {
|
||||
($left:expr, $right:expr $(, $desc:expr)?) => {{
|
||||
let left = crate::deserialized_responses::DisplayName::new($left);
|
||||
let right = crate::deserialized_responses::DisplayName::new($right);
|
||||
|
||||
assert_ne!(
|
||||
left,
|
||||
right
|
||||
$(, $desc)?
|
||||
);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_ambiguous {
|
||||
($name:expr) => {
|
||||
let name = crate::deserialized_responses::DisplayName::new($name);
|
||||
|
||||
assert!(
|
||||
name.is_inherently_ambiguous(),
|
||||
"The display {:?} should be considered amgibuous",
|
||||
name
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_not_ambiguous {
|
||||
($name:expr) => {
|
||||
let name = crate::deserialized_responses::DisplayName::new($name);
|
||||
|
||||
assert!(
|
||||
!name.is_inherently_ambiguous(),
|
||||
"The display {:?} should not be considered amgibuous",
|
||||
name
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_inherently_ambiguous() {
|
||||
// These should not be inherently ambiguous, only if another similarly looking
|
||||
// display name appears should they be considered to be ambiguous.
|
||||
assert_not_ambiguous!("Alice");
|
||||
assert_not_ambiguous!("Carol");
|
||||
assert_not_ambiguous!("Car0l");
|
||||
assert_not_ambiguous!("Ivan");
|
||||
assert_not_ambiguous!("𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
assert_not_ambiguous!("Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
|
||||
assert_not_ambiguous!("🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
|
||||
assert_not_ambiguous!("Sahasrahla");
|
||||
// Left to right is fine, if it's the only one in the room.
|
||||
assert_not_ambiguous!("\u{202e}alharsahas");
|
||||
|
||||
// These on the other hand contain invisible chars.
|
||||
assert_ambiguous!("Sa̴hasrahla");
|
||||
assert_ambiguous!("Sahas\u{200D}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_capitalization() {
|
||||
// Display name with different capitalization
|
||||
assert_display_name_eq!("Alice", "alice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_different_names() {
|
||||
// Different display names
|
||||
assert_display_name_ne!("Alice", "Carol");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_capital_l() {
|
||||
// Different display names
|
||||
assert_display_name_eq!("Hello", "HeIlo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_confusable_zero() {
|
||||
// Different display names
|
||||
assert_display_name_eq!("Carol", "Car0l");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_cyrilic() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("alice", "аlice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_scriptures() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("Sahasrahla", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_frakturs() {
|
||||
// Display name with fraktur symbols
|
||||
assert_display_name_eq!("Sahasrahla", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_circled() {
|
||||
// Display name with circled symbols
|
||||
assert_display_name_eq!("Sahasrahla", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_squared() {
|
||||
// Display name with squared symbols
|
||||
assert_display_name_eq!("Sahasrahla", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_big_unicode() {
|
||||
// Display name with big unicode letters
|
||||
assert_display_name_eq!("Sahasrahla", "Sahasrahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_left_to_right() {
|
||||
// Display name with a left-to-right character
|
||||
assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_diacritical() {
|
||||
// Display name with a diacritical mark.
|
||||
assert_display_name_eq!("Sahasrahla", "Sa̴hasrahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_zero_width_joiner() {
|
||||
// Display name with a zero-width joiner
|
||||
assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_zero_width_space() {
|
||||
// Display name with zero-width space.
|
||||
assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_ligatures() {
|
||||
// Display name with a ligature.
|
||||
assert_display_name_eq!("ff", "\u{FB00}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_colon() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid\u{0589}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{05c3}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{0703}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{0a83}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{16ec}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{205a}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{2236}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe13}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe52}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{fe30}domain.tld");
|
||||
assert_ambiguous!("@mxid\u{ff1a}domain.tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_dot() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:domain\u{0701}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{0702}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{2024}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{fe52}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{ff0e}tld");
|
||||
assert_ambiguous!("@mxid:domain\u{1d16d}tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_replacing_a() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
|
||||
assert_ambiguous!("@mxid:dom\u{0430}in.tld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_confusable_mxid_replacing_l() {
|
||||
assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
|
||||
assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
|
||||
|
||||
// Additionally these should be considered to be ambiguous on their own.
|
||||
assert_ambiguous!("@mxid:domain.tId");
|
||||
assert_ambiguous!("@mxid:domain.t\u{217c}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{1d695}d");
|
||||
assert_ambiguous!("@mxid:domain.t\u{2223}d");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,12 @@ pub enum Error {
|
||||
/// function with invalid parameters
|
||||
#[error("receive_all_members function was called with invalid parameters")]
|
||||
InvalidReceiveMembersParameters,
|
||||
|
||||
/// This request failed because the local data wasn't sufficient.
|
||||
#[error("Local cache doesn't contain all necessary data to perform the action.")]
|
||||
InsufficientData,
|
||||
|
||||
/// There was a [`serde_json`] deserialization error.
|
||||
#[error(transparent)]
|
||||
DeserializationError(#[from] serde_json::error::Error),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.
|
||||
|
||||
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
|
||||
pub mod store;
|
||||
|
||||
/// The kind of event the event storage holds.
|
||||
pub type Event = SyncTimelineEvent;
|
||||
|
||||
/// The kind of gap the event storage holds.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Gap {
|
||||
/// The token to use in the query, extracted from a previous "from" /
|
||||
/// "end" field of a `/messages` response.
|
||||
pub prev_token: String,
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
// 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.
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri, uint,
|
||||
};
|
||||
|
||||
use super::DynEventCacheStore;
|
||||
use crate::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings};
|
||||
|
||||
/// `EventCacheStore` integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`event_cache_store_integration_tests!`] macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
|
||||
/// Test replacing a MXID.
|
||||
async fn test_replace_media_key(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let request_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
let request_thumbnail = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
|
||||
Method::Crop,
|
||||
uint!(100),
|
||||
uint!(100),
|
||||
)),
|
||||
};
|
||||
|
||||
let other_uri = mxc_uri!("mxc://localhost/media-other");
|
||||
let request_other_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(other_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content: Vec<u8> = "hello".into();
|
||||
let thumbnail_content: Vec<u8> = "world".into();
|
||||
let other_content: Vec<u8> = "foo".into();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"unexpected media found"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"media not found"
|
||||
);
|
||||
|
||||
// Let's add the media.
|
||||
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
|
||||
|
||||
// Media is present in the cache.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found though added"
|
||||
);
|
||||
|
||||
// Let's remove the media.
|
||||
self.remove_media_content(&request_file).await.expect("removing media failed");
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media still there after removing"
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone())
|
||||
.await
|
||||
.expect("adding media again failed");
|
||||
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found after adding again"
|
||||
);
|
||||
|
||||
// Let's add the thumbnail media.
|
||||
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
|
||||
// Media's thumbnail is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
|
||||
Some(&thumbnail_content),
|
||||
"thumbnail not found"
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(&request_other_file, other_content.clone())
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
|
||||
// Other file is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found"
|
||||
);
|
||||
|
||||
// Let's remove media based on URI.
|
||||
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
|
||||
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"thumbnail wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().is_some(),
|
||||
"other media was removed"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_replace_media_key(&self) {
|
||||
let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
|
||||
let req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content = "hello".as_bytes().to_owned();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
|
||||
|
||||
// Add the media.
|
||||
self.add_media_content(&req, content.clone()).await.expect("adding media failed");
|
||||
|
||||
// Sanity-check: media is found after adding it.
|
||||
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
|
||||
|
||||
// Replacing a media request works.
|
||||
let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
|
||||
let new_req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(new_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
self.replace_media_key(&req, &new_req)
|
||||
.await
|
||||
.expect("replacing the media request key failed");
|
||||
|
||||
// Finding with the previous request doesn't work anymore.
|
||||
assert!(
|
||||
self.get_media_content(&req).await.unwrap().is_none(),
|
||||
"unexpected media found with the old key"
|
||||
);
|
||||
|
||||
// Finding with the new request does work.
|
||||
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your `EventCacheStore` implementation to run the
|
||||
/// entire tests suite locally.
|
||||
///
|
||||
/// You need to provide a `async fn get_event_cache_store() ->
|
||||
/// EventCacheStoreResult<impl EventCacheStore>` providing a fresh event cache
|
||||
/// store on the same level you invoke the macro.
|
||||
///
|
||||
/// ## Usage Example:
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_base::event_cache::store::{
|
||||
/// # EventCacheStore,
|
||||
/// # MemoryStore as MyStore,
|
||||
/// # Result as EventCacheStoreResult,
|
||||
/// # };
|
||||
///
|
||||
/// #[cfg(test)]
|
||||
/// mod tests {
|
||||
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_event_cache_store(
|
||||
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
/// event_cache_store_integration_tests!();
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests {
|
||||
() => {
|
||||
mod event_cache_store_integration_tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache::store::{
|
||||
EventCacheStoreIntegrationTests, IntoEventCacheStore,
|
||||
};
|
||||
|
||||
use super::get_event_cache_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_content() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_media_content().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_replace_media_key() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_replace_media_key().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro generating tests for the event cache store, related to time (mostly
|
||||
/// for the cross-process lock).
|
||||
#[allow(unused_macros)]
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests_time {
|
||||
() => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache::store::IntoEventCacheStore;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_lease_locks() {
|
||||
let store = get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
|
||||
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired0);
|
||||
|
||||
// Should extend the lease automatically (same holder).
|
||||
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired2);
|
||||
|
||||
// Should extend the lease automatically (same holder + time is ok).
|
||||
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired3);
|
||||
|
||||
// Another attempt at taking the lock should fail, because it's taken.
|
||||
let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired4);
|
||||
|
||||
// Even if we insist.
|
||||
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired5);
|
||||
|
||||
// That's a nice test we got here, go take a little nap.
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired55);
|
||||
|
||||
// Ok you can take another nap then.
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
|
||||
// At some point, we do get the lock.
|
||||
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
|
||||
assert!(acquired6);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// The other gets it almost immediately too.
|
||||
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired7);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// But when we take a longer lease...
|
||||
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired8);
|
||||
|
||||
// It blocks the other user.
|
||||
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(!acquired9);
|
||||
|
||||
// We can hold onto our lease.
|
||||
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired10);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// 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::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri};
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
leases: StdRwLock<HashMap<String, (String, Instant)>>,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
|
||||
leases: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
/// Create a new empty MemoryStore
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStore for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
// Now, let's add it.
|
||||
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut medias = self.media.write().unwrap();
|
||||
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let media = self.media.read().unwrap();
|
||||
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut media = self.media.write().unwrap();
|
||||
let Some(index) = media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
(media_uri == &expected_key).then_some(position)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EventCacheStore, MemoryStore, Result};
|
||||
|
||||
async fn get_event_cache_store() -> Result<impl EventCacheStore> {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
event_cache_store_integration_tests_time!();
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// 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.
|
||||
|
||||
//! The event cache stores holds events and downloaded media when the cache was
|
||||
//! activated to save bandwidth at the cost of increased storage space usage.
|
||||
//!
|
||||
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
|
||||
//! into the event cache for the actual storage. By default this brings an
|
||||
//! in-memory store.
|
||||
|
||||
use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
|
||||
use matrix_sdk_common::store_locks::{
|
||||
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
|
||||
};
|
||||
|
||||
/// The high-level public type to represent an `EventCacheStore` lock.
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `EventCacheStore`.
|
||||
cross_process_lock: CrossProcessStoreLock<LockableEventCacheStore>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
/// That's the only place where the store exists.
|
||||
store: Arc<DynEventCacheStore>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for EventCacheStoreLock {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLock").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventCacheStoreLock {
|
||||
/// Create a new lock around the [`EventCacheStore`].
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessStoreLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
where
|
||||
S: IntoEventCacheStore,
|
||||
{
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: CrossProcessStoreLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
),
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire a spin lock (see [`CrossProcessStoreLock::spin_lock`]).
|
||||
pub async fn lock(&self) -> Result<EventCacheStoreLockGuard<'_>, LockStoreError> {
|
||||
let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?;
|
||||
|
||||
Ok(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
|
||||
}
|
||||
}
|
||||
|
||||
/// An RAII implementation of a “scoped lock” of an [`EventCacheStoreLock`].
|
||||
/// When this structure is dropped (falls out of scope), the lock will be
|
||||
/// unlocked.
|
||||
pub struct EventCacheStoreLockGuard<'a> {
|
||||
/// The cross process lock guard.
|
||||
#[allow(unused)]
|
||||
cross_process_lock_guard: CrossProcessStoreLockGuard,
|
||||
|
||||
/// A reference to the store.
|
||||
store: &'a DynEventCacheStore,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
|
||||
type Target = DynEventCacheStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.store
|
||||
}
|
||||
}
|
||||
|
||||
/// Event cache store specific error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EventCacheStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// The store is locked with a passphrase and an incorrect passphrase
|
||||
/// was given.
|
||||
#[error("The event cache store failed to be unlocked")]
|
||||
Locked,
|
||||
|
||||
/// An unencrypted store was tried to be unlocked with a passphrase.
|
||||
#[error("The event cache store is not encrypted but tried to be opened with a passphrase")]
|
||||
Unencrypted,
|
||||
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
|
||||
Encryption(#[from] StoreEncryptionError),
|
||||
|
||||
/// The store failed to encode or decode some data.
|
||||
#[error("Error encoding or decoding data from the event cache store: {0}")]
|
||||
Codec(#[from] Utf8Error),
|
||||
|
||||
/// The database format has changed in a backwards incompatible way.
|
||||
#[error(
|
||||
"The database format of the event cache store changed in an incompatible way, \
|
||||
current version: {0}, latest version: {1}"
|
||||
)]
|
||||
UnsupportedDatabaseVersion(usize, usize),
|
||||
}
|
||||
|
||||
impl EventCacheStoreError {
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `EventCacheStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventCacheStore` specific result type.
|
||||
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
|
||||
|
||||
/// A type that wraps the [`EventCacheStore`] but implements [`BackingStore`] to
|
||||
/// make it usable inside the cross process lock.
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
impl BackingStore for LockableEventCacheStore {
|
||||
type LockError = EventCacheStoreError;
|
||||
|
||||
async fn try_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> std::result::Result<bool, Self::LockError> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// 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, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::AsyncTraitDeps;
|
||||
use ruma::MxcUri;
|
||||
|
||||
use super::EventCacheStoreError;
|
||||
use crate::media::MediaRequestParameters;
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the event cache of the SDK.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// The error type used by this event cache store.
|
||||
type Error: fmt::Debug + Into<EventCacheStoreError>;
|
||||
|
||||
/// Try to take a lock using the given store.
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Replaces the given media's content key with another one.
|
||||
///
|
||||
/// This should be used whenever a temporary (local) MXID has been used, and
|
||||
/// it must now be replaced with its actual remote counterpart (after
|
||||
/// uploading some content, or creating an empty MXC URI).
|
||||
///
|
||||
/// ⚠ No check is performed to ensure that the media formats are consistent,
|
||||
/// i.e. it's possible to update with a thumbnail key a media that was
|
||||
/// keyed as a file before. The caller is responsible of ensuring that
|
||||
/// the replacement makes sense, according to their use case.
|
||||
///
|
||||
/// This should not raise an error when the `from` parameter points to an
|
||||
/// unknown media, and it should silently continue in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `from` - The previous `MediaRequest` of the file.
|
||||
///
|
||||
/// * `to` - The new `MediaRequest` of the file.
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove a media file's content from the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// This should not raise an error when the `uri` parameter points to an
|
||||
/// unknown media, and it should return an Ok result in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
struct EraseEventCacheStoreError<T>(T);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<T: fmt::Debug> fmt::Debug for EraseEventCacheStoreError<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.replace_media_key(from, to).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased [`EventCacheStore`].
|
||||
pub type DynEventCacheStore = dyn EventCacheStore<Error = EventCacheStoreError>;
|
||||
|
||||
/// A type that can be type-erased into `Arc<dyn EventCacheStore>`.
|
||||
///
|
||||
/// This trait is not meant to be implemented directly outside
|
||||
/// `matrix-sdk-base`, but it is automatically implemented for everything that
|
||||
/// implements `EventCacheStore`.
|
||||
pub trait IntoEventCacheStore {
|
||||
#[doc(hidden)]
|
||||
fn into_event_cache_store(self) -> Arc<DynEventCacheStore>;
|
||||
}
|
||||
|
||||
impl<T> IntoEventCacheStore for T
|
||||
where
|
||||
T: EventCacheStore + Sized + 'static,
|
||||
{
|
||||
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
|
||||
Arc::new(EraseEventCacheStoreError(self))
|
||||
}
|
||||
}
|
||||
|
||||
// Turns a given `Arc<T>` into `Arc<DynEventCacheStore>` by attaching the
|
||||
// `EventCacheStore` impl vtable of `EraseEventCacheStoreError<T>`.
|
||||
impl<T> IntoEventCacheStore for Arc<T>
|
||||
where
|
||||
T: EventCacheStore + 'static,
|
||||
{
|
||||
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
|
||||
let ptr: *const T = Arc::into_raw(self);
|
||||
let ptr_erased = ptr as *const EraseEventCacheStoreError<T>;
|
||||
// SAFETY: EraseEventCacheStoreError is repr(transparent) so T and
|
||||
// EraseEventCacheStoreError<T> have the same layout and ABI
|
||||
unsafe { Arc::from_raw(ptr_erased) }
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,22 @@
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::{
|
||||
poll::unstable_start::SyncUnstablePollStartEvent, room::message::SyncRoomMessageEvent,
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::message::SyncRoomMessageEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
};
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
AnySyncStateEvent,
|
||||
},
|
||||
MxcUri, OwnedEventId,
|
||||
MxcUri, OwnedEventId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -29,6 +36,8 @@ use crate::MinimalRoomMemberEvent;
|
||||
pub enum PossibleLatestEvent<'a> {
|
||||
/// This message is suitable - it is an m.room.message
|
||||
YesRoomMessage(&'a SyncRoomMessageEvent),
|
||||
/// This message is suitable - it is a sticker
|
||||
YesSticker(&'a SyncStickerEvent),
|
||||
/// This message is suitable - it is a poll
|
||||
YesPoll(&'a SyncUnstablePollStartEvent),
|
||||
|
||||
@@ -38,6 +47,10 @@ pub enum PossibleLatestEvent<'a> {
|
||||
/// This message is suitable - it's a call notification
|
||||
YesCallNotify(&'a SyncCallNotifyEvent),
|
||||
|
||||
/// This state event is suitable - it's a knock membership change
|
||||
/// that can be handled by the current user.
|
||||
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
|
||||
|
||||
// Later: YesState(),
|
||||
// Later: YesReaction(),
|
||||
/// Not suitable - it's a state event
|
||||
@@ -51,7 +64,10 @@ pub enum PossibleLatestEvent<'a> {
|
||||
/// Decide whether an event could be stored as the latest event in a room.
|
||||
/// Returns a LatestEvent representing our decision.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
|
||||
pub fn is_suitable_for_latest_event<'a>(
|
||||
event: &'a AnySyncTimelineEvent,
|
||||
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
|
||||
) -> PossibleLatestEvent<'a> {
|
||||
match event {
|
||||
// Suitable - we have an m.room.message that was not redacted or edited
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
|
||||
@@ -68,9 +84,8 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
|
||||
|
||||
if is_replacement {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
} else {
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
@@ -88,6 +103,10 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
|
||||
PossibleLatestEvent::YesCallNotify(notify)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
|
||||
PossibleLatestEvent::YesSticker(sticker)
|
||||
}
|
||||
|
||||
// Encrypted events are not suitable
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
@@ -100,8 +119,29 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
|
||||
// suitable
|
||||
AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
|
||||
|
||||
// We don't currently support state events
|
||||
AnySyncTimelineEvent::State(_) => PossibleLatestEvent::NoUnsupportedEventType,
|
||||
// We don't currently support most state events
|
||||
AnySyncTimelineEvent::State(state) => {
|
||||
// But we make an exception for knocked state events *if* the current user
|
||||
// can either accept or decline them
|
||||
if let AnySyncStateEvent::RoomMember(member) = state {
|
||||
if matches!(member.membership(), MembershipState::Knock) {
|
||||
let can_accept_or_decline_knocks = match power_levels_info {
|
||||
Some((own_user_id, room_power_levels)) => {
|
||||
room_power_levels.user_can_invite(own_user_id)
|
||||
|| room_power_levels.user_can_kick(own_user_id)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// The current user can act on the knock changes, so they should be
|
||||
// displayed
|
||||
if can_accept_or_decline_knocks {
|
||||
return PossibleLatestEvent::YesKnockedStateEvent(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +220,7 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
event: value,
|
||||
sender_profile: None,
|
||||
sender_name_is_ambiguous: None,
|
||||
})
|
||||
});
|
||||
}
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
@@ -271,9 +311,14 @@ mod tests {
|
||||
},
|
||||
SessionDescription,
|
||||
},
|
||||
poll::unstable_start::{
|
||||
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, UnstablePollAnswer,
|
||||
UnstablePollStartContentBlock,
|
||||
poll::{
|
||||
unstable_response::{
|
||||
SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
|
||||
},
|
||||
unstable_start::{
|
||||
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
|
||||
UnstablePollAnswer, UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
relation::Replacement,
|
||||
room::{
|
||||
@@ -320,7 +365,7 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
|
||||
assert_eq!(m.content.msgtype.msgtype(), "m.image");
|
||||
@@ -343,7 +388,7 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
|
||||
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
|
||||
@@ -367,7 +412,7 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -389,12 +434,12 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: StickerEventContent::new(
|
||||
@@ -410,7 +455,29 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event =
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
|
||||
SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: UnstablePollResponseEventContent::new(
|
||||
vec![String::from("option1")],
|
||||
owned_event_id!("$1"),
|
||||
),
|
||||
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, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
@@ -438,7 +505,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
|
||||
);
|
||||
}
|
||||
@@ -460,7 +527,10 @@ mod tests {
|
||||
}),
|
||||
));
|
||||
|
||||
assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted);
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -477,7 +547,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
);
|
||||
}
|
||||
@@ -501,7 +571,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
@@ -532,9 +602,12 @@ mod tests {
|
||||
json!({
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"encryption_info": null,
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
"kind": {
|
||||
"PlainText": {
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -548,6 +621,23 @@ mod tests {
|
||||
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
|
||||
|
||||
// The previous format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"encryption_info": null,
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
|
||||
assert!(deserialized.latest_event.sender_profile.is_none());
|
||||
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
|
||||
|
||||
// The even older format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"latest_event": event
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
pub use matrix_sdk_common::*;
|
||||
@@ -27,14 +28,17 @@ mod client;
|
||||
pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
mod error;
|
||||
pub mod event_cache;
|
||||
pub mod latest_event;
|
||||
pub mod media;
|
||||
pub mod notification_settings;
|
||||
mod response_processors;
|
||||
mod rooms;
|
||||
|
||||
pub mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
mod sliding_sync;
|
||||
pub mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
pub mod sync;
|
||||
@@ -52,11 +56,12 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo, RoomInfoUpdate,
|
||||
RoomMember, RoomMemberships, RoomState, RoomStateFilter,
|
||||
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
|
||||
RoomStateFilter,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, StateChanges, StateStore, StateStoreDataKey,
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
StateStoreDataValue, StoreError,
|
||||
};
|
||||
pub use utils::{
|
||||
|
||||
@@ -14,6 +14,7 @@ use ruma::{
|
||||
},
|
||||
MxcUri, UInt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const UNIQUE_SEPARATOR: &str = "_";
|
||||
|
||||
@@ -25,27 +26,27 @@ pub trait UniqueKey {
|
||||
}
|
||||
|
||||
/// The requested format of a media file.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum MediaFormat {
|
||||
/// The file that was uploaded.
|
||||
File,
|
||||
|
||||
/// A thumbnail of the file that was uploaded.
|
||||
Thumbnail(MediaThumbnailSize),
|
||||
Thumbnail(MediaThumbnailSettings),
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaFormat {
|
||||
fn unique_key(&self) -> String {
|
||||
match self {
|
||||
Self::File => "file".into(),
|
||||
Self::Thumbnail(size) => size.unique_key(),
|
||||
Self::Thumbnail(settings) => settings.unique_key(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The requested size of a media thumbnail.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaThumbnailSize {
|
||||
/// The desired settings of a media thumbnail.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MediaThumbnailSettings {
|
||||
/// The desired resizing method.
|
||||
pub method: Method,
|
||||
|
||||
@@ -56,11 +57,44 @@ pub struct MediaThumbnailSize {
|
||||
/// The desired height of the thumbnail. The actual thumbnail may not match
|
||||
/// the size specified.
|
||||
pub height: UInt,
|
||||
|
||||
/// If we want to request an animated thumbnail from the homeserver.
|
||||
///
|
||||
/// If it is `true`, the server should return an animated thumbnail if
|
||||
/// the media supports it.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
pub animated: bool,
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaThumbnailSize {
|
||||
impl MediaThumbnailSettings {
|
||||
/// Constructs a new `MediaThumbnailSettings` with the given method, width
|
||||
/// and height.
|
||||
///
|
||||
/// Requests a non-animated thumbnail by default.
|
||||
pub fn with_method(method: Method, width: UInt, height: UInt) -> Self {
|
||||
Self { method, width, height, animated: false }
|
||||
}
|
||||
|
||||
/// Constructs a new `MediaThumbnailSettings` with the given width and
|
||||
/// height.
|
||||
///
|
||||
/// Requests scaling, and a non-animated thumbnail.
|
||||
pub fn new(width: UInt, height: UInt) -> Self {
|
||||
Self { method: Method::Scale, width, height, animated: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaThumbnailSettings {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height)
|
||||
let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height);
|
||||
|
||||
if self.animated {
|
||||
key.push_str(UNIQUE_SEPARATOR);
|
||||
key.push_str("animated");
|
||||
}
|
||||
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +107,11 @@ impl UniqueKey for MediaSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for media data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaRequest {
|
||||
/// Parameters for a request for retrieve media data.
|
||||
///
|
||||
/// This is used as a key in the media cache too.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MediaRequestParameters {
|
||||
/// The source of the media file.
|
||||
pub source: MediaSource,
|
||||
|
||||
@@ -83,7 +119,7 @@ pub struct MediaRequest {
|
||||
pub format: MediaFormat,
|
||||
}
|
||||
|
||||
impl MediaRequest {
|
||||
impl MediaRequestParameters {
|
||||
/// Get the [`MxcUri`] from `Self`.
|
||||
pub fn uri(&self) -> &MxcUri {
|
||||
match &self.source {
|
||||
@@ -93,7 +129,7 @@ impl MediaRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaRequest {
|
||||
impl UniqueKey for MediaRequestParameters {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key())
|
||||
}
|
||||
@@ -189,14 +225,14 @@ mod tests {
|
||||
fn test_media_request_url() {
|
||||
let mxc_uri = mxc_uri!("mxc://homeserver/media");
|
||||
|
||||
let plain = MediaRequest {
|
||||
let plain = MediaRequestParameters {
|
||||
source: MediaSource::Plain(mxc_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
assert_eq!(plain.uri(), mxc_uri);
|
||||
|
||||
let file = MediaRequest {
|
||||
let file = MediaRequestParameters {
|
||||
source: MediaSource::Encrypted(Box::new(
|
||||
serde_json::from_value(json!({
|
||||
"url": mxc_uri,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Some shared types about notification settings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Enum representing the push notification modes for a room.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum RoomNotificationMode {
|
||||
/// Receive notifications for all messages.
|
||||
AllMessages,
|
||||
/// Receive notifications for mentions and keywords only.
|
||||
MentionsAndKeywordsOnly,
|
||||
/// Do not receive any notifications.
|
||||
Mute,
|
||||
}
|
||||
@@ -203,7 +203,7 @@ impl RoomReadReceipts {
|
||||
/// Returns whether a new event triggered a new unread/notification/mention.
|
||||
#[inline(always)]
|
||||
fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) {
|
||||
if marks_as_unread(&event.event, user_id) {
|
||||
if marks_as_unread(event.raw(), user_id) {
|
||||
self.num_unread += 1;
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ impl ReceiptSelector {
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) {
|
||||
for ev in new_events {
|
||||
// Get the `sender` field, if any, or skip this event.
|
||||
let Ok(Some(sender)) = ev.event.get_field::<OwnedUserId>("sender") else { continue };
|
||||
let Ok(Some(sender)) = ev.raw().get_field::<OwnedUserId>("sender") else { continue };
|
||||
if sender == user_id {
|
||||
// Get the event id, if any, or skip this event.
|
||||
let Some(event_id) = ev.event_id() else { continue };
|
||||
@@ -449,8 +449,6 @@ fn events_intersects<'a>(
|
||||
/// that has been just received for an event that came in a previous sync.
|
||||
///
|
||||
/// See this module's documentation for more information.
|
||||
///
|
||||
/// Returns a boolean indicating if a field changed value in the read receipts.
|
||||
#[instrument(skip_all, fields(room_id = %room_id))]
|
||||
pub(crate) fn compute_unread_counts(
|
||||
user_id: &UserId,
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// 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::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
mem,
|
||||
};
|
||||
|
||||
use ruma::{
|
||||
events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType},
|
||||
serde::Raw,
|
||||
OwnedUserId, RoomId,
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use crate::{store::Store, RoomInfo, StateChanges};
|
||||
|
||||
/// Applies a function to an existing `RoomInfo` if present in changes, or one
|
||||
/// loaded from the database.
|
||||
fn map_info<F: FnOnce(&mut RoomInfo)>(
|
||||
room_id: &RoomId,
|
||||
changes: &mut StateChanges,
|
||||
store: &Store,
|
||||
f: F,
|
||||
) {
|
||||
if let Some(info) = changes.room_infos.get_mut(room_id) {
|
||||
f(info);
|
||||
} else if let Some(room) = store.room(room_id) {
|
||||
let mut info = room.clone_info();
|
||||
f(&mut info);
|
||||
changes.add_room(info);
|
||||
} else {
|
||||
debug!(room = %room_id, "couldn't find room in state changes or store");
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) struct AccountDataProcessor {
|
||||
parsed_events: Vec<AnyGlobalAccountDataEvent>,
|
||||
raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
|
||||
}
|
||||
|
||||
impl AccountDataProcessor {
|
||||
/// Creates a new processor for global account data.
|
||||
pub fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
|
||||
let mut raw_by_type = BTreeMap::new();
|
||||
let mut parsed_events = Vec::new();
|
||||
|
||||
for raw_event in events {
|
||||
let event = match raw_event.deserialize() {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
let event_type: Option<String> = raw_event.get_field("type").ok().flatten();
|
||||
warn!(event_type, "Failed to deserialize a global account data event: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
raw_by_type.insert(event.event_type(), raw_event.clone());
|
||||
parsed_events.push(event);
|
||||
}
|
||||
|
||||
Self { raw_by_type, parsed_events }
|
||||
}
|
||||
|
||||
/// Returns the push rules found by this processor.
|
||||
pub fn push_rules(&self) -> Option<&Raw<AnyGlobalAccountDataEvent>> {
|
||||
self.raw_by_type.get(&GlobalAccountDataEventType::PushRules)
|
||||
}
|
||||
|
||||
/// Processes the direct rooms in a sync response:
|
||||
///
|
||||
/// Given a [`StateChanges`] instance, processes any direct room info
|
||||
/// from the global account data and adds it to the room infos to
|
||||
/// save.
|
||||
#[instrument(skip_all)]
|
||||
pub(crate) fn process_direct_rooms(
|
||||
&self,
|
||||
events: &[AnyGlobalAccountDataEvent],
|
||||
store: &Store,
|
||||
changes: &mut StateChanges,
|
||||
) {
|
||||
for event in events {
|
||||
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
|
||||
|
||||
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedUserId>>::new();
|
||||
for (user_id, rooms) in direct_event.content.iter() {
|
||||
for room_id in rooms {
|
||||
new_dms.entry(room_id).or_default().insert(user_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let rooms = store.rooms();
|
||||
let mut old_dms = rooms
|
||||
.iter()
|
||||
.filter_map(|r| {
|
||||
let direct_targets = r.direct_targets();
|
||||
(!direct_targets.is_empty()).then(|| (r.room_id(), direct_targets))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// Update the direct targets of rooms if they changed.
|
||||
for (room_id, new_direct_targets) in new_dms {
|
||||
if let Some(old_direct_targets) = old_dms.remove(&room_id) {
|
||||
if old_direct_targets == new_direct_targets {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
|
||||
map_info(room_id, changes, store, |info| {
|
||||
info.base_info.dm_targets = new_direct_targets;
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the targets of old direct chats.
|
||||
for room_id in old_dms.keys() {
|
||||
trace!(?room_id, "Unmarking room as direct room");
|
||||
map_info(room_id, changes, store, |info| {
|
||||
info.base_info.dm_targets.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the processed data to the state changes.
|
||||
pub async fn apply(mut self, changes: &mut StateChanges, store: &Store) {
|
||||
// Fill in the content of `changes.account_data`.
|
||||
mem::swap(&mut changes.account_data, &mut self.raw_by_type);
|
||||
|
||||
// Process direct rooms.
|
||||
let has_new_direct_room_data = self
|
||||
.parsed_events
|
||||
.iter()
|
||||
.any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
|
||||
|
||||
if has_new_direct_room_data {
|
||||
self.process_direct_rooms(&self.parsed_events, store, changes);
|
||||
} else if let Ok(Some(direct_account_data)) =
|
||||
store.get_account_data_event(GlobalAccountDataEventType::Direct).await
|
||||
{
|
||||
debug!("Found direct room data in the Store, applying it");
|
||||
if let Ok(direct_account_data) = direct_account_data.deserialize() {
|
||||
self.process_direct_rooms(&[direct_account_data], store, changes);
|
||||
} else {
|
||||
warn!("Failed to deserialize direct room account data");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeSet, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -30,7 +30,8 @@ use ruma::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::{MemberEvent, SyncOrStrippedState},
|
||||
deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState},
|
||||
store::ambiguity_map::is_display_name_ambiguous,
|
||||
MinimalRoomMemberEvent,
|
||||
};
|
||||
|
||||
@@ -67,8 +68,10 @@ impl RoomMember {
|
||||
} = room_info;
|
||||
|
||||
let is_room_creator = room_creator.as_deref() == Some(event.user_id());
|
||||
let display_name_ambiguous =
|
||||
users_display_names.get(event.display_name()).is_some_and(|s| s.len() > 1);
|
||||
let display_name = event.display_name();
|
||||
let display_name_ambiguous = users_display_names
|
||||
.get(&display_name)
|
||||
.is_some_and(|s| is_display_name_ambiguous(&display_name, s));
|
||||
let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
|
||||
|
||||
Self {
|
||||
@@ -194,6 +197,11 @@ impl RoomMember {
|
||||
self.can_do_impl(|pls| pls.user_can_send_state(self.user_id(), state_type))
|
||||
}
|
||||
|
||||
/// Whether this user can pin or unpin events based on the power levels.
|
||||
pub fn can_pin_or_unpin_event(&self) -> bool {
|
||||
self.can_send_state(StateEventType::RoomPinnedEvents)
|
||||
}
|
||||
|
||||
/// Whether this user can notify everybody in the room by writing `@room` in
|
||||
/// a message.
|
||||
///
|
||||
@@ -240,6 +248,6 @@ pub(crate) struct MemberRoomInfo<'a> {
|
||||
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
|
||||
pub(crate) max_power_level: i64,
|
||||
pub(crate) room_creator: Option<OwnedUserId>,
|
||||
pub(crate) users_display_names: BTreeMap<&'a str, BTreeSet<OwnedUserId>>,
|
||||
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
|
||||
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
|
||||
}
|
||||
|
||||
@@ -11,11 +11,16 @@ use std::{
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{Room, RoomHero, RoomInfo, RoomInfoUpdate, RoomState, RoomStateFilter};
|
||||
pub use normal::{
|
||||
Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
|
||||
RoomStateFilter,
|
||||
};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
call::member::CallMemberEventContent,
|
||||
beacon_info::BeaconInfoEventContent,
|
||||
call::member::{CallMemberEventContent, CallMemberStateKey},
|
||||
macros::EventContent,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
@@ -27,6 +32,7 @@ use ruma::{
|
||||
join_rules::RoomJoinRulesEventContent,
|
||||
member::MembershipState,
|
||||
name::RoomNameEventContent,
|
||||
pinned_events::RoomPinnedEventsEventContent,
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
@@ -44,7 +50,7 @@ use crate::MinimalStateEvent;
|
||||
/// The name of the room, either from the metadata or calculated
|
||||
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum DisplayName {
|
||||
pub enum RoomDisplayName {
|
||||
/// The room has been named explicitly as
|
||||
Named(String),
|
||||
/// The room has a canonical alias that should be used
|
||||
@@ -59,14 +65,48 @@ pub enum DisplayName {
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayName {
|
||||
const WHITESPACE_REGEX: &str = r"\s+";
|
||||
const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
|
||||
|
||||
impl RoomDisplayName {
|
||||
/// Transforms the current display name into the name part of a
|
||||
/// `RoomAliasId`.
|
||||
pub fn to_room_alias_name(&self) -> String {
|
||||
let room_name = match self {
|
||||
Self::Named(name) => name,
|
||||
Self::Aliased(name) => name,
|
||||
Self::Calculated(name) => name,
|
||||
Self::EmptyWas(name) => name,
|
||||
Self::Empty => "",
|
||||
};
|
||||
|
||||
let whitespace_regex =
|
||||
Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
|
||||
let symbol_regex =
|
||||
Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
|
||||
|
||||
// Replace whitespaces with `-`
|
||||
let sanitised = whitespace_regex.replace_all(room_name, "-");
|
||||
// Remove non-ASCII characters and ASCII control characters
|
||||
let sanitised =
|
||||
String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
|
||||
// Remove other problematic ASCII symbols
|
||||
let sanitised = symbol_regex.replace_all(&sanitised, "");
|
||||
// Lowercased
|
||||
sanitised.to_lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RoomDisplayName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
|
||||
RoomDisplayName::Named(s)
|
||||
| RoomDisplayName::Calculated(s)
|
||||
| RoomDisplayName::Aliased(s) => {
|
||||
write!(f, "{s}")
|
||||
}
|
||||
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
|
||||
DisplayName::Empty => write!(f, "Empty Room"),
|
||||
RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
|
||||
RoomDisplayName::Empty => write!(f, "Empty Room"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +118,9 @@ impl fmt::Display for DisplayName {
|
||||
pub struct BaseRoomInfo {
|
||||
/// The avatar URL of this room.
|
||||
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
|
||||
/// All shared live location beacons of this room.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
|
||||
pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
|
||||
/// The canonical alias of this room.
|
||||
pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
|
||||
/// The `m.room.create` event content of this room.
|
||||
@@ -104,7 +147,8 @@ pub struct BaseRoomInfo {
|
||||
/// All minimal state events that containing one or more running matrixRTC
|
||||
/// memberships.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
|
||||
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
|
||||
pub(crate) rtc_member_events:
|
||||
BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
|
||||
/// Whether this room has been manually marked as unread.
|
||||
#[serde(default)]
|
||||
pub(crate) is_marked_unread: bool,
|
||||
@@ -114,6 +158,8 @@ pub struct BaseRoomInfo {
|
||||
/// others, and this field collects them.
|
||||
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
|
||||
pub(crate) notable_tags: RoomNotableTags,
|
||||
/// The `m.room.pinned_events` of this room.
|
||||
pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
|
||||
}
|
||||
|
||||
impl BaseRoomInfo {
|
||||
@@ -138,6 +184,9 @@ impl BaseRoomInfo {
|
||||
/// Returns true if the event modified the info, false otherwise.
|
||||
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
|
||||
match ev {
|
||||
AnySyncStateEvent::BeaconInfo(b) => {
|
||||
self.beacons.insert(b.state_key().clone(), b.into());
|
||||
}
|
||||
// No redacted branch - enabling encryption cannot be undone.
|
||||
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
|
||||
self.encryption = Some(encryption.content.clone());
|
||||
@@ -182,15 +231,18 @@ impl BaseRoomInfo {
|
||||
let mut o_ev = o_ev.clone();
|
||||
o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
|
||||
|
||||
// add the new event.
|
||||
self.rtc_member
|
||||
// Add the new event.
|
||||
self.rtc_member_events
|
||||
.insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
|
||||
|
||||
// Remove all events that don't contain any memberships anymore.
|
||||
self.rtc_member.retain(|_, ev| {
|
||||
self.rtc_member_events.retain(|_, ev| {
|
||||
ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
|
||||
});
|
||||
}
|
||||
AnySyncStateEvent::RoomPinnedEvents(p) => {
|
||||
self.pinned_events = p.as_original().map(|p| p.content.clone());
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
@@ -250,6 +302,11 @@ impl BaseRoomInfo {
|
||||
// wont have call information.
|
||||
return false;
|
||||
}
|
||||
AnyStrippedStateEvent::RoomPinnedEvents(p) => {
|
||||
if let Some(pinned) = p.content.pinned.clone() {
|
||||
self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
@@ -279,7 +336,8 @@ impl BaseRoomInfo {
|
||||
} else if self.topic.has_event_id(redacts) {
|
||||
self.topic.as_mut().unwrap().redact(&room_version);
|
||||
} else {
|
||||
self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts));
|
||||
self.rtc_member_events
|
||||
.retain(|_, member_event| member_event.event_id() != Some(redacts));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +390,7 @@ impl Default for BaseRoomInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
avatar: None,
|
||||
beacons: BTreeMap::new(),
|
||||
canonical_alias: None,
|
||||
create: None,
|
||||
dm_targets: Default::default(),
|
||||
@@ -343,9 +402,10 @@ impl Default for BaseRoomInfo {
|
||||
name: None,
|
||||
tombstone: None,
|
||||
topic: None,
|
||||
rtc_member: BTreeMap::new(),
|
||||
rtc_member_events: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
pinned_events: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,6 +576,7 @@ mod tests {
|
||||
use ruma::events::tag::{TagInfo, TagName, Tags};
|
||||
|
||||
use super::{BaseRoomInfo, RoomNotableTags};
|
||||
use crate::RoomDisplayName;
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_favourite() {
|
||||
@@ -546,4 +607,36 @@ mod tests {
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_lowercases() {
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_whitespace() {
|
||||
assert_eq!(
|
||||
"room-alias",
|
||||
RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
|
||||
assert_eq!(
|
||||
"roomalias",
|
||||
RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
// 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.
|
||||
|
||||
//! HTTP types for MSC4186 or MSC3585.
|
||||
//!
|
||||
//! This module provides unified namings for types from MSC3575 and
|
||||
//! MSC4186.
|
||||
|
||||
/// HTTP types from MSC3575, renamed to match the MSC4186 namings.
|
||||
pub mod msc3575 {
|
||||
use ruma::api::client::sync::sync_events::v4;
|
||||
pub use v4::{Request, Response};
|
||||
|
||||
/// HTTP types related to a `Request`.
|
||||
pub mod request {
|
||||
pub use super::v4::{
|
||||
AccountDataConfig as AccountData, ExtensionsConfig as Extensions,
|
||||
ReceiptsConfig as Receipts, RoomDetailsConfig as RoomDetails, RoomSubscription,
|
||||
SyncRequestList as List, SyncRequestListFilters as ListFilters,
|
||||
ToDeviceConfig as ToDevice, TypingConfig as Typing,
|
||||
};
|
||||
}
|
||||
|
||||
/// HTTP types related to a `Response`.
|
||||
pub mod response {
|
||||
pub use super::v4::{
|
||||
AccountData, Extensions, Receipts, SlidingSyncRoom as Room,
|
||||
SlidingSyncRoomHero as RoomHero, SyncList as List, ToDevice, Typing,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP types from MSC4186.
|
||||
pub mod msc4186 {
|
||||
pub use ruma::api::client::sync::sync_events::v5::*;
|
||||
}
|
||||
|
||||
pub use msc4186::*;
|
||||
+1110
-229
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -24,28 +24,24 @@ use ruma::{
|
||||
},
|
||||
OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tracing::trace;
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
use super::{DynStateStore, Result, StateChanges};
|
||||
use crate::{
|
||||
deserialized_responses::{AmbiguityChange, RawMemberEvent},
|
||||
deserialized_responses::{AmbiguityChange, DisplayName, RawMemberEvent},
|
||||
store::StateStoreExt,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AmbiguityCache {
|
||||
pub store: Arc<DynStateStore>,
|
||||
pub cache: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AmbiguityMap {
|
||||
display_name: String,
|
||||
/// A map of users that use a certain display name.
|
||||
#[derive(Debug, Clone)]
|
||||
struct DisplayNameUsers {
|
||||
display_name: DisplayName,
|
||||
users: BTreeSet<OwnedUserId>,
|
||||
}
|
||||
|
||||
impl AmbiguityMap {
|
||||
impl DisplayNameUsers {
|
||||
/// Remove the given [`UserId`] from the map, marking that the [`UserId`]
|
||||
/// doesn't use the display name anymore.
|
||||
fn remove(&mut self, user_id: &UserId) -> Option<OwnedUserId> {
|
||||
self.users.remove(user_id);
|
||||
|
||||
@@ -56,6 +52,8 @@ impl AmbiguityMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the given [`UserId`] from the map, marking that the [`UserId`]
|
||||
/// is using the display name.
|
||||
fn add(&mut self, user_id: OwnedUserId) -> Option<OwnedUserId> {
|
||||
let ambiguous_user =
|
||||
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
|
||||
@@ -65,46 +63,73 @@ impl AmbiguityMap {
|
||||
ambiguous_user
|
||||
}
|
||||
|
||||
/// How many users are using this display name.
|
||||
fn user_count(&self) -> usize {
|
||||
self.users.len()
|
||||
}
|
||||
|
||||
/// Is the display name considered to be ambiguous.
|
||||
fn is_ambiguous(&self) -> bool {
|
||||
self.user_count() > 1
|
||||
is_display_name_ambiguous(&self.display_name, &self.users)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_member_active(membership: &MembershipState) -> bool {
|
||||
use MembershipState::*;
|
||||
matches!(membership, Join | Invite | Knock)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AmbiguityCache {
|
||||
pub store: Arc<DynStateStore>,
|
||||
pub cache: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
#[instrument(ret)]
|
||||
pub(crate) fn is_display_name_ambiguous(
|
||||
display_name: &DisplayName,
|
||||
users_with_display_name: &BTreeSet<OwnedUserId>,
|
||||
) -> bool {
|
||||
trace!("Checking if a display name is ambiguous");
|
||||
display_name.is_inherently_ambiguous() || users_with_display_name.len() > 1
|
||||
}
|
||||
|
||||
impl AmbiguityCache {
|
||||
/// Create a new [`AmbiguityCache`] backed by the given state store.
|
||||
pub fn new(store: Arc<DynStateStore>) -> Self {
|
||||
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
|
||||
}
|
||||
|
||||
/// Handle a newly received [`SyncRoomMemberEvent`] for the given room.
|
||||
pub async fn handle_event(
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &SyncRoomMemberEvent,
|
||||
) -> Result<()> {
|
||||
// Synapse seems to have a bug where it puts the same event into the
|
||||
// state and the timeline sometimes.
|
||||
// Synapse seems to have a bug where it puts the same event into the state and
|
||||
// the timeline sometimes.
|
||||
//
|
||||
// Since our state, e.g. the old display name, already ended up inside
|
||||
// the state changes and we're pulling stuff out of the cache if it's
|
||||
// there calculating this twice for the same event will result in an
|
||||
// incorrect AmbiguityChange overwriting the correct one. In other
|
||||
// words, this method is not idempotent so we make it by ignoring
|
||||
// duplicate events.
|
||||
// Since our state, e.g. the old display name, already ended up inside the state
|
||||
// changes and we're pulling stuff out of the cache if it's there calculating
|
||||
// this twice for the same event will result in an incorrect AmbiguityChange
|
||||
// overwriting the correct one. In other words, this method is not idempotent so
|
||||
// we make it by ignoring duplicate events.
|
||||
if self.changes.get(room_id).is_some_and(|c| c.contains_key(member_event.event_id())) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?;
|
||||
let (mut old_map, mut new_map) =
|
||||
self.calculate_changes(changes, room_id, member_event).await?;
|
||||
|
||||
let display_names_same = match (&old_map, &new_map) {
|
||||
(Some(a), Some(b)) => a.display_name == b.display_name,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// If the user's display name didn't change, then there's nothing more to
|
||||
// calculate here.
|
||||
if display_names_same {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -126,16 +151,21 @@ impl AmbiguityCache {
|
||||
|
||||
trace!(user_id = ?member_event.state_key(), "Handling display name ambiguity: {change:#?}");
|
||||
|
||||
self.add_change(room_id, member_event.event_id().to_owned(), change);
|
||||
self.changes
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(member_event.event_id().to_owned(), change);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the [`AmbiguityCache`] state for the given room with a pair of
|
||||
/// [`DisplayNameUsers`] that got created by a new [`SyncRoomMemberEvent`].
|
||||
fn update(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
old_map: Option<AmbiguityMap>,
|
||||
new_map: Option<AmbiguityMap>,
|
||||
old_map: Option<DisplayNameUsers>,
|
||||
new_map: Option<DisplayNameUsers>,
|
||||
) {
|
||||
let entry = self.cache.entry(room_id.to_owned()).or_default();
|
||||
|
||||
@@ -148,74 +178,102 @@ impl AmbiguityCache {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_change(&mut self, room_id: &RoomId, event_id: OwnedEventId, change: AmbiguityChange) {
|
||||
self.changes.entry(room_id.to_owned()).or_default().insert(event_id, change);
|
||||
/// Get the previously used display name, if any, of the member described in
|
||||
/// the given new [`SyncRoomMemberEvent`].
|
||||
async fn get_old_display_name(
|
||||
&self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
new_event: &SyncRoomMemberEvent,
|
||||
) -> Result<Option<String>> {
|
||||
let user_id = new_event.state_key();
|
||||
|
||||
let old_event = if let Some(m) = changes
|
||||
.state
|
||||
.get(room_id)
|
||||
.and_then(|events| events.get(&StateEventType::RoomMember)?.get(user_id.as_str()))
|
||||
{
|
||||
Some(RawMemberEvent::Sync(m.clone().cast()))
|
||||
} else {
|
||||
self.store.get_member_event(room_id, user_id).await?
|
||||
};
|
||||
|
||||
let Some(Ok(old_event)) = old_event.map(|r| r.deserialize()) else { return Ok(None) };
|
||||
|
||||
if is_member_active(old_event.membership()) {
|
||||
let display_name = if let Some(d) = changes
|
||||
.profiles
|
||||
.get(room_id)
|
||||
.and_then(|p| p.get(user_id)?.as_original()?.content.displayname.as_deref())
|
||||
{
|
||||
Some(d.to_owned())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, user_id)
|
||||
.await?
|
||||
.and_then(|p| p.into_original()?.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
old_event.original_content().and_then(|c| c.displayname.clone())
|
||||
};
|
||||
|
||||
Ok(Some(display_name.unwrap_or_else(|| user_id.localpart().to_owned())))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get(
|
||||
/// Get the [`DisplayNameUsers`] for the given display name in the given
|
||||
/// room.
|
||||
///
|
||||
/// This method will get the [`DisplayNameUsers`] from the cache, if the
|
||||
/// cache doesn't contain such an entry, it falls back to the state
|
||||
/// store.
|
||||
async fn get_users_with_display_name(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<DisplayNameUsers> {
|
||||
Ok(if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(display_name) {
|
||||
DisplayNameUsers { display_name: display_name.clone(), users: u.clone() }
|
||||
} else {
|
||||
let users_with_display_name =
|
||||
self.store.get_users_with_display_name(room_id, display_name).await?;
|
||||
|
||||
DisplayNameUsers { display_name: display_name.clone(), users: users_with_display_name }
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate the change in the users that use a display name a
|
||||
/// [`SyncRoomMemberEvent`] will cause for a given room.
|
||||
///
|
||||
/// Returns the [`DisplayNameUsers`] before the member event is applied and
|
||||
/// the [`DisplayNameUsers`] after the member event is applied to the
|
||||
/// room state.
|
||||
async fn calculate_changes(
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &SyncRoomMemberEvent,
|
||||
) -> 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)?.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?
|
||||
};
|
||||
|
||||
// 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())?.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()?.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
event.original_content().and_then(|c| c.displayname.clone())
|
||||
};
|
||||
|
||||
Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
) -> Result<(Option<DisplayNameUsers>, Option<DisplayNameUsers>)> {
|
||||
let old_display_name = self.get_old_display_name(changes, room_id, member_event).await?;
|
||||
|
||||
let old_map = if let Some(old_name) = old_display_name.as_deref() {
|
||||
let old_display_name_map =
|
||||
if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(old_name) {
|
||||
u.clone()
|
||||
} else {
|
||||
self.store.get_users_with_display_name(room_id, old_name).await?
|
||||
};
|
||||
|
||||
Some(AmbiguityMap { display_name: old_name.to_owned(), users: old_display_name_map })
|
||||
let old_display_name = DisplayName::new(old_name);
|
||||
Some(self.get_users_with_display_name(room_id, &old_display_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let new_map = if matches!(member_event.membership(), Join | Invite) {
|
||||
let new_map = if is_member_active(member_event.membership()) {
|
||||
let new = member_event
|
||||
.as_original()
|
||||
.and_then(|ev| ev.content.displayname.as_deref())
|
||||
.unwrap_or_else(|| member_event.state_key().localpart());
|
||||
|
||||
// We don't allow other users to set the display name, so if we
|
||||
// have a more trusted version of the display
|
||||
// name use that.
|
||||
// We don't allow other users to set the display name, so if we have a more
|
||||
// trusted version of the display name use that.
|
||||
let new_display_name = if member_event.sender().as_str() == member_event.state_key() {
|
||||
new
|
||||
} else if let Some(old) = old_display_name.as_deref() {
|
||||
@@ -224,22 +282,221 @@ impl AmbiguityCache {
|
||||
new
|
||||
};
|
||||
|
||||
let new_display_name_map = if let Some(u) =
|
||||
self.cache.entry(room_id.to_owned()).or_default().get(new_display_name)
|
||||
{
|
||||
u.clone()
|
||||
} else {
|
||||
self.store.get_users_with_display_name(room_id, new_display_name).await?
|
||||
};
|
||||
let new_display_name = DisplayName::new(new_display_name);
|
||||
|
||||
Some(AmbiguityMap {
|
||||
display_name: new_display_name.to_owned(),
|
||||
users: new_display_name_map,
|
||||
})
|
||||
Some(self.get_users_with_display_name(room_id, &new_display_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((old_map, new_map))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn check(&self, room_id: &RoomId, display_name: &DisplayName) -> bool {
|
||||
self.cache
|
||||
.get(room_id)
|
||||
.and_then(|display_names| {
|
||||
display_names
|
||||
.get(display_name)
|
||||
.map(|user_ids| is_display_name_ambiguous(display_name, user_ids))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"The display name {:?} should be part of the cache {:?}",
|
||||
display_name, self.cache
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{room_id, server_name, user_id, EventId};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::store::{IntoStateStore, MemoryStore};
|
||||
|
||||
fn generate_event(user_id: &UserId, display_name: &str) -> SyncRoomMemberEvent {
|
||||
let server_name = server_name!("localhost");
|
||||
serde_json::from_value(json!({
|
||||
"content": {
|
||||
"displayname": display_name,
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": EventId::new(server_name),
|
||||
"origin_server_ts": 152037280,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
|
||||
}))
|
||||
.expect("We should be able to deserialize the static member event")
|
||||
}
|
||||
|
||||
macro_rules! assert_ambiguity {
|
||||
(
|
||||
[ $( ($user:literal, $display_name:literal) ),* ],
|
||||
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ] $(,)?
|
||||
) => {
|
||||
assert_ambiguity!(
|
||||
[ $( ($user, $display_name) ),* ],
|
||||
[ $( ($check_display_name, $ambiguous) ),* ],
|
||||
"The test failed the ambiguity assertions"
|
||||
)
|
||||
};
|
||||
|
||||
(
|
||||
[ $( ($user:literal, $display_name:literal) ),* ],
|
||||
[ $( ($check_display_name:literal, $ambiguous:expr) ),* ],
|
||||
$description:literal $(,)?
|
||||
) => {
|
||||
let store = MemoryStore::new();
|
||||
let mut ambiguity_cache = AmbiguityCache::new(store.into_state_store());
|
||||
|
||||
let changes = Default::default();
|
||||
let room_id = room_id!("!foo:bar");
|
||||
|
||||
macro_rules! add_display_name {
|
||||
($u:literal, $n:literal) => {
|
||||
let event = generate_event(user_id!($u), $n);
|
||||
|
||||
ambiguity_cache
|
||||
.handle_event(&changes, room_id, &event)
|
||||
.await
|
||||
.expect("We should be able to handle a member event to calculate the ambiguity.");
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_display_name_ambiguity {
|
||||
($n:literal, $a:expr) => {
|
||||
let display_name = DisplayName::new($n);
|
||||
|
||||
if ambiguity_cache.check(room_id, &display_name) != $a {
|
||||
let foo = if $a { "be" } else { "not be" };
|
||||
panic!("{}: the display name {} should {} ambiguous", $description, $n, foo);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$(
|
||||
add_display_name!($user, $display_name);
|
||||
)*
|
||||
|
||||
$(
|
||||
assert_display_name_ambiguity!($check_display_name, $ambiguous);
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_disambiguation() {
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice")],
|
||||
[("alice", false)],
|
||||
"Alice is alone in the room"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice")],
|
||||
[("Alice", false)],
|
||||
"Alice is alone in the room and has a capitalized display name"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "alice")],
|
||||
[("alice", true)],
|
||||
"Alice and bob share a display name"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[
|
||||
("@alice:localhost", "alice"),
|
||||
("@bob:localhost", "alice"),
|
||||
("@carol:localhost", "carol")
|
||||
],
|
||||
[("alice", true), ("carol", false)],
|
||||
"Alice and Bob share a display name, while Carol is unique"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "ALICE")],
|
||||
[("alice", true)],
|
||||
"Alice and Bob share a display name that is differently capitalized"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "аlice")],
|
||||
[("alice", true)],
|
||||
"Bob tries to impersonate Alice using a cyrilic а"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "@bob:localhost"), ("@bob:localhost", "аlice")],
|
||||
[("@bob:localhost", true)],
|
||||
"Alice tries to impersonate bob using an mxid"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝒮𝒶𝒽𝒶𝓈𝓇𝒶𝒽𝓁𝒶")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using scripture symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "𝔖𝔞𝔥𝔞𝔰𝔯𝔞𝔥𝔩𝔞")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using fraktur symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Ⓢⓐⓗⓐⓢⓡⓐⓗⓛⓐ")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using circled symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "🅂🄰🄷🄰🅂🅁🄰🄷🄻🄰")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using squared symbols"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahasrahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using big unicode letters"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "\u{202e}alharsahas")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using left to right shenanigans"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sa̴hasrahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using a diacritical mark"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200B}rahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using a zero-width space"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "Sahasrahla"), ("@bob:localhost", "Sahas\u{200D}rahla")],
|
||||
[("Sahasrahla", true)],
|
||||
"Bob tries to impersonate Alice using a zero-width space"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "ff"), ("@bob:localhost", "\u{FB00}")],
|
||||
[("ff", true)],
|
||||
"Bob tries to impersonate Alice using a ligature"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Trait and macro of integration tests for StateStore implementations.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
@@ -8,7 +8,7 @@ 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,
|
||||
api::MatrixVersion,
|
||||
event_id,
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
@@ -21,24 +21,25 @@ use ruma::{
|
||||
message::RoomMessageEventContent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
MediaSource,
|
||||
},
|
||||
AnyEphemeralRoomEventContent, AnyGlobalAccountDataEvent, AnyMessageLikeEventContent,
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent,
|
||||
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
|
||||
SyncStateEvent,
|
||||
},
|
||||
mxc_uri, owned_mxc_uri, room_id,
|
||||
owned_event_id, owned_mxc_uri, room_id,
|
||||
serde::Raw,
|
||||
uint, user_id, EventId, OwnedEventId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
};
|
||||
use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
use super::DynStateStore;
|
||||
use super::{
|
||||
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
|
||||
ServerCapabilities,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
media::{MediaFormat, MediaRequest, MediaThumbnailSize},
|
||||
store::{Result, SerializableEventContent, StateStoreExt},
|
||||
store::{ChildTransactionId, QueueWedgeError, Result, SerializableEventContent, StateStoreExt},
|
||||
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
|
||||
};
|
||||
|
||||
@@ -51,8 +52,6 @@ use crate::{
|
||||
pub trait StateStoreIntegrationTests {
|
||||
/// Populate the given `StateStore`.
|
||||
async fn populate(&self) -> Result<()>;
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
/// Test room topic redaction.
|
||||
async fn test_topic_redaction(&self) -> Result<()>;
|
||||
/// Test populating the store.
|
||||
@@ -75,8 +74,6 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_receipts_saving(&self);
|
||||
/// Test custom storage.
|
||||
async fn test_custom_storage(&self) -> Result<()>;
|
||||
/// Test invited room saving.
|
||||
async fn test_persist_invited_room(&self) -> Result<()>;
|
||||
/// Test stripped and non-stripped room member saving.
|
||||
async fn test_stripped_non_stripped(&self) -> Result<()>;
|
||||
/// Test room removal.
|
||||
@@ -89,6 +86,12 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_display_names_saving(&self);
|
||||
/// Test operations with the send queue.
|
||||
async fn test_send_queue(&self);
|
||||
/// Test priority of operations with the send queue.
|
||||
async fn test_send_queue_priority(&self);
|
||||
/// Test operations related to send queue dependents.
|
||||
async fn test_send_queue_dependents(&self);
|
||||
/// Test saving/restoring server capabilities.
|
||||
async fn test_server_capabilities_saving(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -115,7 +118,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
serde_json::from_value::<Raw<AnyGlobalAccountDataEvent>>(pushrules_json.clone())
|
||||
.unwrap();
|
||||
let pushrules_event = pushrules_raw.deserialize().unwrap();
|
||||
changes.add_account_data(pushrules_event, pushrules_raw);
|
||||
changes.account_data.insert(pushrules_event.event_type(), pushrules_raw);
|
||||
|
||||
let mut room = RoomInfo::new(room_id, RoomState::Joined);
|
||||
room.mark_as_left();
|
||||
@@ -139,13 +142,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room.handle_state_event(&topic_event);
|
||||
changes.add_state_event(room_id, topic_event, topic_raw);
|
||||
|
||||
let mut room_ambiguity_map = BTreeMap::new();
|
||||
let mut room_ambiguity_map = HashMap::new();
|
||||
let mut room_profiles = BTreeMap::new();
|
||||
|
||||
let member_json: &JsonValue = &test_json::MEMBER;
|
||||
let member_event: SyncRoomMemberEvent =
|
||||
serde_json::from_value(member_json.clone()).unwrap();
|
||||
let displayname = member_event.as_original().unwrap().content.displayname.clone().unwrap();
|
||||
let displayname = DisplayName::new(
|
||||
member_event.as_original().unwrap().content.displayname.as_ref().unwrap(),
|
||||
);
|
||||
room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()]));
|
||||
room_profiles.insert(user_id.to_owned(), (&member_event).into());
|
||||
|
||||
@@ -209,110 +214,6 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let request_file =
|
||||
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
|
||||
let request_thumbnail = MediaRequest {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
method: Method::Crop,
|
||||
width: uint!(100),
|
||||
height: uint!(100),
|
||||
}),
|
||||
};
|
||||
|
||||
let other_uri = mxc_uri!("mxc://localhost/media-other");
|
||||
let request_other_file = MediaRequest {
|
||||
source: MediaSource::Plain(other_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content: Vec<u8> = "hello".into();
|
||||
let thumbnail_content: Vec<u8> = "world".into();
|
||||
let other_content: Vec<u8> = "foo".into();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"unexpected media found"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"media not found"
|
||||
);
|
||||
|
||||
// Let's add the media.
|
||||
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
|
||||
|
||||
// Media is present in the cache.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found though added"
|
||||
);
|
||||
|
||||
// Let's remove the media.
|
||||
self.remove_media_content(&request_file).await.expect("removing media failed");
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media still there after removing"
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone())
|
||||
.await
|
||||
.expect("adding media again failed");
|
||||
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found after adding again"
|
||||
);
|
||||
|
||||
// Let's add the thumbnail media.
|
||||
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
|
||||
// Media's thumbnail is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
|
||||
Some(&thumbnail_content),
|
||||
"thumbnail not found"
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(&request_other_file, other_content.clone())
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
|
||||
// Other file is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found"
|
||||
);
|
||||
|
||||
// Let's remove media based on URI.
|
||||
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
|
||||
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"thumbnail wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().is_some(),
|
||||
"other media was removed"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_topic_redaction(&self) -> Result<()> {
|
||||
let room_id = room_id();
|
||||
self.populate().await?;
|
||||
@@ -358,14 +259,13 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
async fn test_populate_store(&self) -> Result<()> {
|
||||
let room_id = room_id();
|
||||
let user_id = user_id();
|
||||
let display_name = DisplayName::new("example");
|
||||
|
||||
self.populate().await?;
|
||||
|
||||
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
|
||||
assert!(self.get_presence_event(user_id).await?.is_some());
|
||||
assert_eq!(self.get_room_infos().await?.len(), 2, "Expected to find 2 room infos");
|
||||
#[allow(deprecated)]
|
||||
let stripped_rooms = self.get_stripped_room_infos().await?;
|
||||
assert_eq!(stripped_rooms.len(), 1, "Expected to find 1 stripped room info");
|
||||
assert!(self
|
||||
.get_account_data_event(GlobalAccountDataEventType::PushRules)
|
||||
.await?
|
||||
@@ -395,7 +295,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
"Expected to find 1 joined user ids"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_users_with_display_name(room_id, "example").await?.len(),
|
||||
self.get_users_with_display_name(room_id, &display_name).await?.len(),
|
||||
2,
|
||||
"Expected to find 2 display names for room"
|
||||
);
|
||||
@@ -570,6 +470,36 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_server_capabilities_saving(&self) {
|
||||
let versions = &[MatrixVersion::V1_1, MatrixVersion::V1_2, MatrixVersion::V1_11];
|
||||
let server_caps = ServerCapabilities::new(
|
||||
versions,
|
||||
[("org.matrix.experimental".to_owned(), true)].into(),
|
||||
);
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::ServerCapabilities,
|
||||
StateStoreDataValue::ServerCapabilities(server_caps.clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_let!(
|
||||
Ok(Some(StateStoreDataValue::ServerCapabilities(stored_caps))) =
|
||||
self.get_kv_data(StateStoreDataKey::ServerCapabilities).await
|
||||
);
|
||||
assert_eq!(stored_caps, server_caps);
|
||||
|
||||
let (stored_versions, stored_features) = stored_caps.maybe_decode().unwrap();
|
||||
|
||||
assert_eq!(stored_versions, versions);
|
||||
assert_eq!(stored_features.len(), 1);
|
||||
assert_eq!(stored_features.get("org.matrix.experimental"), Some(&true));
|
||||
|
||||
self.remove_kv_data(StateStoreDataKey::ServerCapabilities).await.unwrap();
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::ServerCapabilities).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";
|
||||
@@ -992,25 +922,12 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_persist_invited_room(&self) -> Result<()> {
|
||||
self.populate().await?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
let stripped_rooms = self.get_stripped_room_infos().await?;
|
||||
assert_eq!(stripped_rooms.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_stripped_non_stripped(&self) -> Result<()> {
|
||||
let room_id = room_id!("!test_stripped_non_stripped:localhost");
|
||||
let user_id = user_id();
|
||||
|
||||
assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none());
|
||||
assert_eq!(self.get_room_infos().await.unwrap().len(), 0);
|
||||
#[allow(deprecated)]
|
||||
let stripped_rooms = self.get_stripped_room_infos().await?;
|
||||
assert_eq!(stripped_rooms.len(), 0);
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes
|
||||
@@ -1027,9 +944,6 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
|
||||
assert!(matches!(member_event, MemberEvent::Sync(_)));
|
||||
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
|
||||
#[allow(deprecated)]
|
||||
let stripped_rooms = self.get_stripped_room_infos().await?;
|
||||
assert_eq!(stripped_rooms.len(), 0);
|
||||
|
||||
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
|
||||
assert_eq!(members, vec![user_id.to_owned()]);
|
||||
@@ -1043,9 +957,6 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
|
||||
assert!(matches!(member_event, MemberEvent::Stripped(_)));
|
||||
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
|
||||
#[allow(deprecated)]
|
||||
let stripped_rooms = self.get_stripped_room_infos().await?;
|
||||
assert_eq!(stripped_rooms.len(), 1);
|
||||
|
||||
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
|
||||
assert_eq!(members, vec![user_id.to_owned()]);
|
||||
@@ -1056,6 +967,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
async fn test_room_removal(&self) -> Result<()> {
|
||||
let room_id = room_id();
|
||||
let user_id = user_id();
|
||||
let display_name = DisplayName::new("example");
|
||||
let stripped_room_id = stripped_room_id();
|
||||
|
||||
self.populate().await?;
|
||||
@@ -1063,9 +975,6 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
self.remove_room(room_id).await?;
|
||||
|
||||
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
|
||||
#[allow(deprecated)]
|
||||
let stripped_rooms = self.get_stripped_room_infos().await?;
|
||||
assert_eq!(stripped_rooms.len(), 1);
|
||||
|
||||
assert!(self.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none());
|
||||
assert!(
|
||||
@@ -1087,7 +996,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
"still joined users found"
|
||||
);
|
||||
assert!(
|
||||
self.get_users_with_display_name(room_id, "example").await?.is_empty(),
|
||||
self.get_users_with_display_name(room_id, &display_name).await?.is_empty(),
|
||||
"still display names found"
|
||||
);
|
||||
assert!(self
|
||||
@@ -1118,9 +1027,6 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
self.remove_room(stripped_room_id).await?;
|
||||
|
||||
assert!(self.get_room_infos().await?.is_empty(), "still room info found");
|
||||
#[allow(deprecated)]
|
||||
let stripped_rooms = self.get_stripped_room_infos().await?;
|
||||
assert!(stripped_rooms.is_empty(), "still stripped room info found");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1245,15 +1151,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
async fn test_display_names_saving(&self) {
|
||||
let room_id = room_id!("!test_display_names_saving:localhost");
|
||||
let user_id = user_id();
|
||||
let user_display_name = "User";
|
||||
let user_display_name = DisplayName::new("User");
|
||||
let second_user_id = user_id!("@second:localhost");
|
||||
let third_user_id = user_id!("@third:localhost");
|
||||
let other_display_name = "Raoul";
|
||||
let unknown_display_name = "Unknown";
|
||||
let other_display_name = DisplayName::new("Raoul");
|
||||
let unknown_display_name = DisplayName::new("Unknown");
|
||||
|
||||
// No event in store.
|
||||
let mut display_names = vec![user_display_name.to_owned()];
|
||||
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
|
||||
assert!(users.is_empty());
|
||||
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
|
||||
assert!(names.is_empty());
|
||||
@@ -1267,7 +1173,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
.insert(user_display_name.to_owned(), [user_id.to_owned()].into());
|
||||
self.save_changes(&changes).await.unwrap();
|
||||
|
||||
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
|
||||
assert_eq!(users.len(), 1);
|
||||
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
|
||||
assert_eq!(names.len(), 1);
|
||||
@@ -1282,9 +1188,9 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
self.save_changes(&changes).await.unwrap();
|
||||
|
||||
display_names.push(other_display_name.to_owned());
|
||||
let users = self.get_users_with_display_name(room_id, user_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &user_display_name).await.unwrap();
|
||||
assert_eq!(users.len(), 1);
|
||||
let users = self.get_users_with_display_name(room_id, other_display_name).await.unwrap();
|
||||
let users = self.get_users_with_display_name(room_id, &other_display_name).await.unwrap();
|
||||
assert_eq!(users.len(), 2);
|
||||
let names = self.get_users_with_display_names(room_id, &display_names).await.unwrap();
|
||||
assert_eq!(names.len(), 2);
|
||||
@@ -1306,7 +1212,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let room_id = room_id!("!test_send_queue:localhost");
|
||||
|
||||
// No queued event in store at first.
|
||||
let events = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let events = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
assert!(events.is_empty());
|
||||
|
||||
// Saving one thing should work.
|
||||
@@ -1314,20 +1220,20 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id, txn0.clone(), event0).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
|
||||
// Reading it will work.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 1);
|
||||
{
|
||||
assert_eq!(pending[0].transaction_id, txn0);
|
||||
|
||||
let deserialized = pending[0].event.deserialize().unwrap();
|
||||
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "msg0");
|
||||
|
||||
assert!(!pending[0].is_wedged);
|
||||
assert!(!pending[0].is_wedged());
|
||||
}
|
||||
|
||||
// Saving another three things should work.
|
||||
@@ -1338,11 +1244,11 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.save_send_queue_event(room_id, txn, event).await.unwrap();
|
||||
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
|
||||
}
|
||||
|
||||
// Reading all the events should work.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
// All the events should be retrieved, in the same order.
|
||||
assert_eq!(pending.len(), 4);
|
||||
@@ -1350,27 +1256,36 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert_eq!(pending[0].transaction_id, txn0);
|
||||
|
||||
for i in 0..4 {
|
||||
let deserialized = pending[i].event.deserialize().unwrap();
|
||||
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), format!("msg{i}"));
|
||||
assert!(!pending[i].is_wedged);
|
||||
assert!(!pending[i].is_wedged());
|
||||
}
|
||||
|
||||
// Marking an event as wedged works.
|
||||
let txn2 = &pending[2].transaction_id;
|
||||
self.update_send_queue_event_status(room_id, txn2, true).await.unwrap();
|
||||
self.update_send_queue_request_status(
|
||||
room_id,
|
||||
txn2,
|
||||
Some(QueueWedgeError::GenericApiError { msg: "Oops".to_owned() }),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// And it is reflected.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
// All the events should be retrieved, in the same order.
|
||||
assert_eq!(pending.len(), 4);
|
||||
assert_eq!(pending[0].transaction_id, txn0);
|
||||
assert_eq!(pending[2].transaction_id, *txn2);
|
||||
assert!(pending[2].is_wedged);
|
||||
assert!(pending[2].is_wedged());
|
||||
let error = pending[2].clone().error.unwrap();
|
||||
let generic_error = assert_matches!(error, QueueWedgeError::GenericApiError { msg } => msg);
|
||||
assert_eq!(generic_error, "Oops");
|
||||
for i in 0..4 {
|
||||
if i != 2 {
|
||||
assert!(!pending[i].is_wedged);
|
||||
assert!(!pending[i].is_wedged());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1379,37 +1294,37 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
&RoomMessageEventContent::text_plain("wow that's a cool test").into(),
|
||||
)
|
||||
.unwrap();
|
||||
self.update_send_queue_event(room_id, txn2, event0).await.unwrap();
|
||||
self.update_send_queue_request(room_id, txn2, event0.into()).await.unwrap();
|
||||
|
||||
// And it is reflected.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 4);
|
||||
{
|
||||
assert_eq!(pending[2].transaction_id, *txn2);
|
||||
|
||||
let deserialized = pending[2].event.deserialize().unwrap();
|
||||
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "wow that's a cool test");
|
||||
|
||||
assert!(!pending[2].is_wedged);
|
||||
assert!(!pending[2].is_wedged());
|
||||
|
||||
for i in 0..4 {
|
||||
if i != 2 {
|
||||
let deserialized = pending[i].event.deserialize().unwrap();
|
||||
let deserialized = pending[i].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), format!("msg{i}"));
|
||||
|
||||
assert!(!pending[i].is_wedged);
|
||||
assert!(!pending[i].is_wedged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removing an event works.
|
||||
self.remove_send_queue_event(room_id, &txn0).await.unwrap();
|
||||
self.remove_send_queue_request(room_id, &txn0).await.unwrap();
|
||||
|
||||
// And it is reflected.
|
||||
let pending = self.load_send_queue_events(room_id).await.unwrap();
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 3);
|
||||
assert_eq!(pending[1].transaction_id, *txn2);
|
||||
@@ -1427,7 +1342,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id2, txn.clone(), event).await.unwrap();
|
||||
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
|
||||
}
|
||||
|
||||
// Add and remove one event for room3.
|
||||
@@ -1437,18 +1352,182 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_event(room_id3, txn.clone(), event).await.unwrap();
|
||||
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
|
||||
|
||||
self.remove_send_queue_event(room_id3, &txn).await.unwrap();
|
||||
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
|
||||
}
|
||||
|
||||
// Query all the rooms which have unsent events. Per the previous steps,
|
||||
// it should be room1 and room2, not room3.
|
||||
let outstanding_rooms = self.load_rooms_with_unsent_events().await.unwrap();
|
||||
let outstanding_rooms = self.load_rooms_with_unsent_requests().await.unwrap();
|
||||
assert_eq!(outstanding_rooms.len(), 2);
|
||||
assert!(outstanding_rooms.iter().any(|room| room == room_id));
|
||||
assert!(outstanding_rooms.iter().any(|room| room == room_id2));
|
||||
}
|
||||
|
||||
async fn test_send_queue_priority(&self) {
|
||||
let room_id = room_id!("!test_send_queue:localhost");
|
||||
|
||||
// No queued event in store at first.
|
||||
let events = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
assert!(events.is_empty());
|
||||
|
||||
// Saving one request should work.
|
||||
let low0_txn = TransactionId::new();
|
||||
let ev0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap();
|
||||
|
||||
// Saving one request with higher priority should work.
|
||||
let high_txn = TransactionId::new();
|
||||
let ev1 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap();
|
||||
|
||||
// Saving another request with the low priority should work.
|
||||
let low1_txn = TransactionId::new();
|
||||
let ev2 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap();
|
||||
|
||||
// The requests should be ordered from higher priority to lower, and when equal,
|
||||
// should use the insertion order instead.
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(pending.len(), 3);
|
||||
{
|
||||
assert_eq!(pending[0].transaction_id, high_txn);
|
||||
|
||||
let deserialized = pending[0].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "high");
|
||||
}
|
||||
|
||||
{
|
||||
assert_eq!(pending[1].transaction_id, low0_txn);
|
||||
|
||||
let deserialized = pending[1].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "low0");
|
||||
}
|
||||
|
||||
{
|
||||
assert_eq!(pending[2].transaction_id, low1_txn);
|
||||
|
||||
let deserialized = pending[2].as_event().unwrap().deserialize().unwrap();
|
||||
assert_let!(AnyMessageLikeEventContent::RoomMessage(content) = deserialized);
|
||||
assert_eq!(content.body(), "low1");
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_send_queue_dependents(&self) {
|
||||
let room_id = room_id!("!test_send_queue_dependents:localhost");
|
||||
|
||||
// Save one send queue event to start with.
|
||||
let txn0 = TransactionId::new();
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
|
||||
// No dependents, to start with.
|
||||
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
|
||||
|
||||
// Save a redaction for that event.
|
||||
let child_txn = ChildTransactionId::new();
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
child_txn.clone(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// It worked.
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn0);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert!(dependents[0].parent_key.is_none());
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Update the event id.
|
||||
let event_id = owned_event_id!("$1");
|
||||
let num_updated = self
|
||||
.update_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
SentRequestKey::Event(event_id.clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(num_updated, 1);
|
||||
|
||||
// It worked.
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn0);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert_matches!(dependents[0].parent_key.as_ref(), Some(SentRequestKey::Event(eid)) => {
|
||||
assert_eq!(*eid, event_id);
|
||||
});
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Now remove it.
|
||||
let removed = self
|
||||
.remove_dependent_queued_request(room_id, &dependents[0].own_transaction_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(removed);
|
||||
|
||||
// It worked.
|
||||
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
|
||||
|
||||
// Now, inserting a dependent event and removing the original send queue event
|
||||
// will NOT remove the dependent event.
|
||||
let txn1 = TransactionId::new();
|
||||
let event1 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
|
||||
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
ChildTransactionId::new(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 1);
|
||||
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn1,
|
||||
ChildTransactionId::new(),
|
||||
DependentQueuedRequestKind::EditEvent {
|
||||
new_content: SerializableEventContent::new(
|
||||
&RoomMessageEventContent::text_plain("edit").into(),
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(self.load_dependent_queued_requests(room_id).await.unwrap().len(), 2);
|
||||
|
||||
// Remove event0 / txn0.
|
||||
let removed = self.remove_send_queue_request(room_id, &txn0).await.unwrap();
|
||||
assert!(removed);
|
||||
|
||||
// This has removed none of the dependent events.
|
||||
let dependents = self.load_dependent_queued_requests(room_id).await.unwrap();
|
||||
assert_eq!(dependents.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your StateStore implementation to run the entire
|
||||
@@ -1479,137 +1558,134 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! statestore_integration_tests {
|
||||
(with_media_tests) => {
|
||||
mod statestore_integration_tests {
|
||||
$crate::statestore_integration_tests!(@inner);
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_content() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_media_content().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
() => {
|
||||
mod statestore_integration_tests {
|
||||
$crate::statestore_integration_tests!(@inner);
|
||||
}
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::store::{
|
||||
IntoStateStore, Result as StoreResult, StateStoreIntegrationTests,
|
||||
};
|
||||
|
||||
(@inner) => {
|
||||
use matrix_sdk_test::async_test;
|
||||
use super::get_store;
|
||||
|
||||
use $crate::store::{IntoStateStore, Result as StoreResult, StateStoreIntegrationTests};
|
||||
#[async_test]
|
||||
async fn test_topic_redaction() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_topic_redaction().await
|
||||
}
|
||||
|
||||
use super::get_store;
|
||||
#[async_test]
|
||||
async fn test_populate_store() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_populate_store().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_topic_redaction() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_topic_redaction().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_member_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_member_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_populate_store() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_populate_store().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_filter_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_filter_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_member_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_member_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_filter_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_filter_saving().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_server_capabilities_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_server_capabilities_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_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_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();
|
||||
store.test_stripped_member_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_stripped_member_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_stripped_member_saving().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_power_level_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_power_level_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_power_level_saving() {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_power_level_saving().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_receipts_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_receipts_saving().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receipts_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_receipts_saving().await;
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_custom_storage() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_custom_storage().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_custom_storage() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_custom_storage().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_stripped_non_stripped() -> StoreResult<()> {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_stripped_non_stripped().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_persist_invited_room() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_persist_invited_room().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_room_removal() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_room_removal().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_stripped_non_stripped() -> StoreResult<()> {
|
||||
let store = get_store().await.unwrap().into_state_store();
|
||||
store.test_stripped_non_stripped().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_room_removal() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_room_removal().await
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_presence_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_presence_saving().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_display_names_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_display_names_saving().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_presence_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_presence_saving().await;
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_send_queue() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_send_queue().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_names_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_display_names_saving().await;
|
||||
}
|
||||
#[async_test]
|
||||
async fn test_send_queue_priority() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_send_queue_priority().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_queue() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_send_queue().await;
|
||||
#[async_test]
|
||||
async fn test_send_queue_dependents() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_send_queue_dependents().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
|
||||
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},
|
||||
events::{
|
||||
@@ -31,36 +29,40 @@ use ruma::{
|
||||
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
CanonicalJsonObject, EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomId,
|
||||
OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
time::Instant,
|
||||
CanonicalJsonObject, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId,
|
||||
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use super::{
|
||||
traits::{ComposerDraft, QueuedEvent, SerializableEventContent},
|
||||
Result, RoomInfo, StateChanges, StateStore, StoreError,
|
||||
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
|
||||
traits::{ComposerDraft, ServerCapabilities},
|
||||
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
|
||||
StateChanges, StateStore, StoreError,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedState,
|
||||
media::{MediaRequest, UniqueKey as _},
|
||||
MinimalRoomMemberEvent, RoomMemberships, RoomState, StateStoreDataKey, StateStoreDataValue,
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
|
||||
store::QueueWedgeError,
|
||||
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
|
||||
};
|
||||
|
||||
/// In-Memory, non-persistent implementation of the `StateStore`
|
||||
/// In-memory, non-persistent implementation of the `StateStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MemoryStore {
|
||||
recently_visited_rooms: StdRwLock<HashMap<OwnedUserId, Vec<OwnedRoomId>>>,
|
||||
composer_drafts: StdRwLock<HashMap<OwnedRoomId, ComposerDraft>>,
|
||||
user_avatar_url: StdRwLock<HashMap<OwnedUserId, OwnedMxcUri>>,
|
||||
sync_token: StdRwLock<Option<String>>,
|
||||
server_capabilities: StdRwLock<Option<ServerCapabilities>>,
|
||||
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>>>>,
|
||||
display_names: StdRwLock<HashMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>>,
|
||||
members: StdRwLock<HashMap<OwnedRoomId, HashMap<OwnedUserId, MembershipState>>>,
|
||||
room_info: StdRwLock<HashMap<OwnedRoomId, RoomInfo>>,
|
||||
room_state: StdRwLock<
|
||||
@@ -86,40 +88,9 @@ pub struct MemoryStore {
|
||||
HashMap<(String, Option<String>), HashMap<OwnedEventId, HashMap<OwnedUserId, Receipt>>>,
|
||||
>,
|
||||
>,
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
|
||||
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedEvent>>>,
|
||||
}
|
||||
|
||||
// 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(),
|
||||
send_queue_events: Default::default(),
|
||||
}
|
||||
}
|
||||
send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<QueuedRequest>>>,
|
||||
dependent_send_queue_events: StdRwLock<BTreeMap<OwnedRoomId, Vec<DependentQueuedRequest>>>,
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
@@ -175,6 +146,12 @@ impl StateStore for MemoryStore {
|
||||
StateStoreDataKey::SyncToken => {
|
||||
self.sync_token.read().unwrap().clone().map(StateStoreDataValue::SyncToken)
|
||||
}
|
||||
StateStoreDataKey::ServerCapabilities => self
|
||||
.server_capabilities
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.map(StateStoreDataValue::ServerCapabilities),
|
||||
StateStoreDataKey::Filter(filter_name) => self
|
||||
.filters
|
||||
.read()
|
||||
@@ -255,6 +232,13 @@ impl StateStore for MemoryStore {
|
||||
value.into_composer_draft().expect("Session data not a composer draft"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::ServerCapabilities => {
|
||||
*self.server_capabilities.write().unwrap() = Some(
|
||||
value
|
||||
.into_server_capabilities()
|
||||
.expect("Session data not containing server capabilities"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -263,6 +247,9 @@ impl StateStore for MemoryStore {
|
||||
async fn remove_kv_data(&self, key: StateStoreDataKey<'_>) -> Result<()> {
|
||||
match key {
|
||||
StateStoreDataKey::SyncToken => *self.sync_token.write().unwrap() = None,
|
||||
StateStoreDataKey::ServerCapabilities => {
|
||||
*self.server_capabilities.write().unwrap() = None
|
||||
}
|
||||
StateStoreDataKey::Filter(filter_name) => {
|
||||
self.filters.write().unwrap().remove(filter_name);
|
||||
}
|
||||
@@ -352,9 +339,13 @@ impl StateStore for MemoryStore {
|
||||
trace!("room state");
|
||||
{
|
||||
let mut room_state = self.room_state.write().unwrap();
|
||||
trace!("room state: got room_state lock");
|
||||
let mut stripped_room_state = self.stripped_room_state.write().unwrap();
|
||||
trace!("room state: got stripped_room_state lock");
|
||||
let mut members = self.members.write().unwrap();
|
||||
trace!("room state: got members lock");
|
||||
let mut stripped_members = self.stripped_members.write().unwrap();
|
||||
trace!("room state: got stripped_members lock");
|
||||
|
||||
for (room, event_types) in &changes.state {
|
||||
for (event_type, events) in event_types {
|
||||
@@ -575,21 +566,18 @@ impl StateStore for MemoryStore {
|
||||
Some(state_events.values().cloned().map(to_enum).collect())
|
||||
}
|
||||
|
||||
Ok(get_events(
|
||||
&self.stripped_room_state.read().unwrap(),
|
||||
room_id,
|
||||
&event_type,
|
||||
RawAnySyncOrStrippedState::Stripped,
|
||||
)
|
||||
.or_else(|| {
|
||||
get_events(
|
||||
&self.room_state.read().unwrap(),
|
||||
room_id,
|
||||
&event_type,
|
||||
RawAnySyncOrStrippedState::Sync,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default())
|
||||
let state_map = self.stripped_room_state.read().unwrap();
|
||||
Ok(get_events(&state_map, room_id, &event_type, RawAnySyncOrStrippedState::Stripped)
|
||||
.or_else(|| {
|
||||
drop(state_map); // release the lock on stripped_room_state
|
||||
get_events(
|
||||
&self.room_state.read().unwrap(),
|
||||
room_id,
|
||||
&event_type,
|
||||
RawAnySyncOrStrippedState::Sync,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn get_state_events_for_keys(
|
||||
@@ -669,6 +657,7 @@ impl StateStore for MemoryStore {
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, memberships))]
|
||||
async fn get_user_ids(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
@@ -697,41 +686,23 @@ impl StateStore for MemoryStore {
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
let v = get_user_ids_inner(&self.stripped_members.read().unwrap(), room_id, memberships);
|
||||
let state_map = self.stripped_members.read().unwrap();
|
||||
let v = get_user_ids_inner(&state_map, room_id, memberships);
|
||||
if !v.is_empty() {
|
||||
return Ok(v);
|
||||
}
|
||||
drop(state_map); // release the stripped_members lock
|
||||
Ok(get_user_ids_inner(&self.members.read().unwrap(), room_id, memberships))
|
||||
}
|
||||
|
||||
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>> {
|
||||
StateStore::get_user_ids(self, room_id, RoomMemberships::INVITE).await
|
||||
}
|
||||
|
||||
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>> {
|
||||
StateStore::get_user_ids(self, room_id, RoomMemberships::JOIN).await
|
||||
}
|
||||
|
||||
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>> {
|
||||
Ok(self.room_info.read().unwrap().values().cloned().collect())
|
||||
}
|
||||
|
||||
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>> {
|
||||
Ok(self
|
||||
.room_info
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|r| matches!(r.state(), RoomState::Invited))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
display_name: &DisplayName,
|
||||
) -> Result<BTreeSet<OwnedUserId>> {
|
||||
Ok(self
|
||||
.display_names
|
||||
@@ -745,21 +716,18 @@ impl StateStore for MemoryStore {
|
||||
async fn get_users_with_display_names<'a>(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_names: &'a [String],
|
||||
) -> Result<BTreeMap<&'a str, BTreeSet<OwnedUserId>>> {
|
||||
display_names: &'a [DisplayName],
|
||||
) -> Result<HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>> {
|
||||
if display_names.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let read_guard = &self.display_names.read().unwrap();
|
||||
let Some(room_names) = read_guard.get(room_id) else {
|
||||
return Ok(BTreeMap::new());
|
||||
return Ok(HashMap::new());
|
||||
};
|
||||
|
||||
Ok(display_names
|
||||
.iter()
|
||||
.filter_map(|n| room_names.get(n).map(|d| (n.as_str(), d.clone())))
|
||||
.collect())
|
||||
Ok(display_names.iter().filter_map(|n| room_names.get(n).map(|d| (n, d.clone()))).collect())
|
||||
}
|
||||
|
||||
async fn get_account_data_event(
|
||||
@@ -817,58 +785,6 @@ impl StateStore for MemoryStore {
|
||||
Ok(self.custom.write().unwrap().remove(key))
|
||||
}
|
||||
|
||||
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
// Now, let's add it.
|
||||
self.media.write().unwrap().push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
|
||||
let media = self.media.read().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
Ok(media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
let Some(index) = media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
(media_uri == &expected_key).then_some(position)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<()> {
|
||||
self.profiles.write().unwrap().remove(room_id);
|
||||
self.display_names.write().unwrap().remove(room_id);
|
||||
@@ -884,26 +800,27 @@ impl StateStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_send_queue_event(
|
||||
async fn save_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
event: SerializableEventContent,
|
||||
kind: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.send_queue_events
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(QueuedEvent { event, transaction_id, is_wedged: false });
|
||||
.push(QueuedRequest { kind, transaction_id, error: None, priority });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_send_queue_event(
|
||||
async fn update_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
content: SerializableEventContent,
|
||||
kind: QueuedRequestKind,
|
||||
) -> Result<bool, Self::Error> {
|
||||
if let Some(entry) = self
|
||||
.send_queue_events
|
||||
@@ -914,15 +831,15 @@ impl StateStore for MemoryStore {
|
||||
.iter_mut()
|
||||
.find(|item| item.transaction_id == transaction_id)
|
||||
{
|
||||
entry.event = content;
|
||||
entry.is_wedged = false;
|
||||
entry.kind = kind;
|
||||
entry.error = None;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_send_queue_event(
|
||||
async fn remove_send_queue_request(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
@@ -945,18 +862,22 @@ impl StateStore for MemoryStore {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn load_send_queue_events(
|
||||
async fn load_send_queue_requests(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<QueuedEvent>, Self::Error> {
|
||||
Ok(self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone())
|
||||
) -> Result<Vec<QueuedRequest>, Self::Error> {
|
||||
let mut ret =
|
||||
self.send_queue_events.write().unwrap().entry(room_id.to_owned()).or_default().clone();
|
||||
// Inverted order of priority, use stable sort to keep insertion order.
|
||||
ret.sort_by(|lhs, rhs| rhs.priority.cmp(&lhs.priority));
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn update_send_queue_event_status(
|
||||
async fn update_send_queue_request_status(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: &TransactionId,
|
||||
wedged: bool,
|
||||
error: Option<QueueWedgeError>,
|
||||
) -> Result<(), Self::Error> {
|
||||
if let Some(entry) = self
|
||||
.send_queue_events
|
||||
@@ -967,14 +888,74 @@ impl StateStore for MemoryStore {
|
||||
.iter_mut()
|
||||
.find(|item| item.transaction_id == transaction_id)
|
||||
{
|
||||
entry.is_wedged = wedged;
|
||||
entry.error = error;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_rooms_with_unsent_events(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
|
||||
async fn load_rooms_with_unsent_requests(&self) -> Result<Vec<OwnedRoomId>, Self::Error> {
|
||||
Ok(self.send_queue_events.read().unwrap().keys().cloned().collect())
|
||||
}
|
||||
|
||||
async fn save_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
parent_transaction_id: &TransactionId,
|
||||
own_transaction_id: ChildTransactionId,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.dependent_send_queue_events.write().unwrap().entry(room.to_owned()).or_default().push(
|
||||
DependentQueuedRequest {
|
||||
kind: content,
|
||||
parent_transaction_id: parent_transaction_id.to_owned(),
|
||||
own_transaction_id,
|
||||
parent_key: None,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
parent_txn_id: &TransactionId,
|
||||
sent_parent_key: SentRequestKey,
|
||||
) -> Result<usize, Self::Error> {
|
||||
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
|
||||
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
|
||||
let mut num_updated = 0;
|
||||
for d in dependents.iter_mut().filter(|item| item.parent_transaction_id == parent_txn_id) {
|
||||
d.parent_key = Some(sent_parent_key.clone());
|
||||
num_updated += 1;
|
||||
}
|
||||
Ok(num_updated)
|
||||
}
|
||||
|
||||
async fn remove_dependent_queued_request(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
txn_id: &ChildTransactionId,
|
||||
) -> Result<bool, Self::Error> {
|
||||
let mut dependent_send_queue_events = self.dependent_send_queue_events.write().unwrap();
|
||||
let dependents = dependent_send_queue_events.entry(room.to_owned()).or_default();
|
||||
if let Some(pos) = dependents.iter().position(|item| item.own_transaction_id == *txn_id) {
|
||||
dependents.remove(pos);
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// List all the dependent send queue events.
|
||||
///
|
||||
/// This returns absolutely all the dependent send queue events, whether
|
||||
/// they have an event id or not.
|
||||
async fn load_dependent_queued_requests(
|
||||
&self,
|
||||
room: &RoomId,
|
||||
) -> Result<Vec<DependentQueuedRequest>, Self::Error> {
|
||||
Ok(self.dependent_send_queue_events.read().unwrap().get(room).cloned().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -985,5 +966,5 @@ mod tests {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
statestore_integration_tests!(with_media_tests);
|
||||
statestore_integration_tests!();
|
||||
}
|
||||
|
||||
@@ -111,8 +111,10 @@ impl RoomInfoV1 {
|
||||
} = self;
|
||||
|
||||
RoomInfo {
|
||||
version: 0,
|
||||
room_id,
|
||||
room_state: room_type,
|
||||
prev_room_state: None,
|
||||
notification_counts,
|
||||
summary,
|
||||
members_synced,
|
||||
@@ -125,6 +127,9 @@ impl RoomInfoV1 {
|
||||
base_info: base_info.migrate(create),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
recency_stamp: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,6 +202,7 @@ impl BaseRoomInfoV1 {
|
||||
|
||||
Box::new(BaseRoomInfo {
|
||||
avatar,
|
||||
beacons: BTreeMap::new(),
|
||||
canonical_alias,
|
||||
create,
|
||||
dm_targets,
|
||||
@@ -208,9 +214,10 @@ impl BaseRoomInfoV1 {
|
||||
name,
|
||||
tombstone,
|
||||
topic,
|
||||
rtc_member: BTreeMap::new(),
|
||||
rtc_member_events: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
pinned_events: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//! store.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
result::Result as StdResult,
|
||||
@@ -29,9 +29,7 @@ use std::{
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use eyeball_im::{Vector, VectorDiff};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use futures_util::Stream;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
@@ -57,24 +55,32 @@ use ruma::{
|
||||
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
rooms::{normal::RoomInfoUpdate, RoomInfo, RoomState},
|
||||
deserialized_responses::DisplayName,
|
||||
event_cache::store as event_cache_store,
|
||||
rooms::{normal::RoomInfoNotableUpdate, RoomInfo, RoomState},
|
||||
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
pub(crate) mod ambiguity_map;
|
||||
mod memory_store;
|
||||
pub mod migration_helpers;
|
||||
mod send_queue;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::StateStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
send_queue::{
|
||||
ChildTransactionId, DependentQueuedRequest, DependentQueuedRequestKind,
|
||||
FinishUploadThumbnailInfo, QueueWedgeError, QueuedRequest, QueuedRequestKind,
|
||||
SentMediaInfo, SentRequestKey, SerializableEventContent,
|
||||
},
|
||||
traits::{
|
||||
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, QueuedEvent,
|
||||
SerializableEventContent, StateStore, StateStoreDataKey, StateStoreDataValue,
|
||||
StateStoreExt,
|
||||
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerCapabilities,
|
||||
StateStore, StateStoreDataKey, StateStoreDataValue, StateStoreExt,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -169,6 +175,36 @@ impl Store {
|
||||
&self.sync_lock
|
||||
}
|
||||
|
||||
/// Load the room infos from the inner `StateStore`.
|
||||
///
|
||||
/// Applies migrations to the room infos if needed.
|
||||
async fn load_room_infos(&self) -> Result<Vec<RoomInfo>> {
|
||||
let mut room_infos = self.inner.get_room_infos().await?;
|
||||
let mut migrated_room_infos = Vec::with_capacity(room_infos.len());
|
||||
|
||||
for room_info in room_infos.iter_mut() {
|
||||
if room_info.apply_migrations(self.inner.clone()).await {
|
||||
migrated_room_infos.push(room_info.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !migrated_room_infos.is_empty() {
|
||||
let changes = StateChanges {
|
||||
room_infos: migrated_room_infos
|
||||
.into_iter()
|
||||
.map(|room_info| (room_info.room_id.clone(), room_info))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Err(error) = self.inner.save_changes(&changes).await {
|
||||
warn!("Failed to save migrated room infos: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(room_infos)
|
||||
}
|
||||
|
||||
/// Set the meta of the session.
|
||||
///
|
||||
/// Restores the state of this `Store` from the given `SessionMeta` and the
|
||||
@@ -178,10 +214,10 @@ impl Store {
|
||||
pub async fn set_session_meta(
|
||||
&self,
|
||||
session_meta: SessionMeta,
|
||||
roominfo_update_sender: &broadcast::Sender<RoomInfoUpdate>,
|
||||
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Result<()> {
|
||||
{
|
||||
let room_infos = self.inner.get_room_infos().await?;
|
||||
let room_infos = self.load_room_infos().await?;
|
||||
|
||||
let mut rooms = self.rooms.write().unwrap();
|
||||
|
||||
@@ -190,7 +226,7 @@ impl Store {
|
||||
&session_meta.user_id,
|
||||
self.inner.clone(),
|
||||
room_info,
|
||||
roominfo_update_sender.clone(),
|
||||
room_info_notable_update_sender.clone(),
|
||||
);
|
||||
let new_room_id = new_room.room_id().to_owned();
|
||||
|
||||
@@ -230,7 +266,6 @@ impl Store {
|
||||
|
||||
/// Get a stream of all the rooms changes, in addition to the existing
|
||||
/// rooms.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
|
||||
self.rooms.read().unwrap().stream()
|
||||
}
|
||||
@@ -240,13 +275,19 @@ impl Store {
|
||||
self.rooms.read().unwrap().get(room_id).cloned()
|
||||
}
|
||||
|
||||
/// Check if a room exists.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn room_exists(&self, room_id: &RoomId) -> bool {
|
||||
self.rooms.read().unwrap().get(room_id).is_some()
|
||||
}
|
||||
|
||||
/// 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,
|
||||
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
|
||||
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Room {
|
||||
let user_id =
|
||||
&self.session_meta.get().expect("Creating room while not being logged in").user_id;
|
||||
@@ -255,10 +296,27 @@ impl Store {
|
||||
.write()
|
||||
.unwrap()
|
||||
.get_or_create(room_id, || {
|
||||
Room::new(user_id, self.inner.clone(), room_id, room_type, roominfo_update_sender)
|
||||
Room::new(
|
||||
user_id,
|
||||
self.inner.clone(),
|
||||
room_id,
|
||||
room_type,
|
||||
room_info_notable_update_sender,
|
||||
)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Forget the room with the given room ID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room that should be forgotten.
|
||||
pub(crate) async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
|
||||
self.inner.remove_room(room_id).await?;
|
||||
self.rooms.write().unwrap().remove(room_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -307,8 +365,10 @@ pub struct StateChanges {
|
||||
/// A mapping of `RoomId` to a map of event type string to `AnyBasicEvent`.
|
||||
pub room_account_data:
|
||||
BTreeMap<OwnedRoomId, BTreeMap<RoomAccountDataEventType, Raw<AnyRoomAccountDataEvent>>>,
|
||||
/// A map of `RoomId` to `RoomInfo`.
|
||||
|
||||
/// A map of `OwnedRoomId` to `RoomInfo`.
|
||||
pub room_infos: BTreeMap<OwnedRoomId, RoomInfo>,
|
||||
|
||||
/// A map of `RoomId` to `ReceiptEventContent`.
|
||||
pub receipts: BTreeMap<OwnedRoomId, ReceiptEventContent>,
|
||||
|
||||
@@ -325,7 +385,7 @@ pub struct StateChanges {
|
||||
|
||||
/// A map from room id to a map of a display name and a set of user ids that
|
||||
/// share that display name in the given room.
|
||||
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
|
||||
pub ambiguity_maps: BTreeMap<OwnedRoomId, HashMap<DisplayName, BTreeSet<OwnedUserId>>>,
|
||||
}
|
||||
|
||||
impl StateChanges {
|
||||
@@ -344,15 +404,6 @@ impl StateChanges {
|
||||
self.room_infos.insert(room.room_id.clone(), room);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given `AnyBasicEvent`.
|
||||
pub fn add_account_data(
|
||||
&mut self,
|
||||
event: AnyGlobalAccountDataEvent,
|
||||
raw_event: Raw<AnyGlobalAccountDataEvent>,
|
||||
) {
|
||||
self.account_data.insert(event.event_type(), raw_event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `AnyBasicEvent`.
|
||||
pub fn add_room_account_data(
|
||||
@@ -419,21 +470,27 @@ impl StateChanges {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the state store and, when `encryption` is enabled, for the
|
||||
/// crypto store.
|
||||
/// Configuration for the various stores.
|
||||
///
|
||||
/// By default, this always includes a state store and an event cache store.
|
||||
/// When the `e2e-encryption` feature is enabled, this also includes a crypto
|
||||
/// store.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::store::StoreConfig;
|
||||
///
|
||||
/// let store_config = StoreConfig::new();
|
||||
/// let store_config =
|
||||
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct StoreConfig {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) crypto_store: Arc<DynCryptoStore>,
|
||||
pub(crate) state_store: Arc<DynStateStore>,
|
||||
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
|
||||
cross_process_store_locks_holder_name: String,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -445,12 +502,20 @@ impl fmt::Debug for StoreConfig {
|
||||
|
||||
impl StoreConfig {
|
||||
/// Create a new default `StoreConfig`.
|
||||
///
|
||||
/// To learn more about `cross_process_store_locks_holder_name`, please read
|
||||
/// [`CrossProcessStoreLock::new`](matrix_sdk_common::store_locks::CrossProcessStoreLock::new).
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
pub fn new(cross_process_store_locks_holder_name: String) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
crypto_store: matrix_sdk_crypto::store::MemoryStore::new().into_crypto_store(),
|
||||
state_store: Arc::new(MemoryStore::new()),
|
||||
event_cache_store: event_cache_store::EventCacheStoreLock::new(
|
||||
event_cache_store::MemoryStore::new(),
|
||||
cross_process_store_locks_holder_name.clone(),
|
||||
),
|
||||
cross_process_store_locks_holder_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,10 +533,16 @@ impl StoreConfig {
|
||||
self.state_store = store.into_state_store();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StoreConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
/// Set a custom implementation of an `EventCacheStore`.
|
||||
pub fn event_cache_store<S>(mut self, event_cache_store: S) -> Self
|
||||
where
|
||||
S: event_cache_store::IntoEventCacheStore,
|
||||
{
|
||||
self.event_cache_store = event_cache_store::EventCacheStoreLock::new(
|
||||
event_cache_store,
|
||||
self.cross_process_store_locks_holder_name.clone(),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user