Compare commits
1022 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c46e42201 | |||
| 0d4bc65e28 | |||
| 5e1bae02fe | |||
| 77a67de7df | |||
| f27eb4d1c8 | |||
| 05814c5559 | |||
| d5d9898fb4 | |||
| 3f71d9a379 | |||
| b077f45e78 | |||
| 648d527f2f | |||
| 8513547e92 | |||
| d18669e8d9 | |||
| a0426251a3 | |||
| 2739c5bf27 | |||
| 381f4d419f | |||
| 9ab5547065 | |||
| df3cb002a5 | |||
| 6ebd4295b9 | |||
| c5104d68fd | |||
| 0064839283 | |||
| 2727d72916 | |||
| 33a2cc3031 | |||
| 38097f90b2 | |||
| 47d08683a2 | |||
| 57919f5480 | |||
| b8949cfe26 | |||
| 8d27b0c811 | |||
| 3707d2fb81 | |||
| eaaa5e17a0 | |||
| e3958b754c | |||
| 78d9e1292f | |||
| d594b4dad7 | |||
| 3f40ad83a5 | |||
| 5049d1a3b6 | |||
| 29862fc9bd | |||
| 585224b2fa | |||
| 0dc5e69ace | |||
| b323802ab0 | |||
| 252786d2ef | |||
| 97cbe57d3f | |||
| 0d8ad159c3 | |||
| 9d732395ce | |||
| 6a772d1c56 | |||
| b71499ffe6 | |||
| 4dadf8581a | |||
| 4d4cd61363 | |||
| 6f42b0a67b | |||
| e3b348761e | |||
| d755a8a3aa | |||
| 66e3ddec47 | |||
| 720d443452 | |||
| 33d691a58e | |||
| c2f39c1086 | |||
| df9c355aed | |||
| 0334ff3f64 | |||
| 8262726369 | |||
| a4a6bf540d | |||
| 9b38f38aea | |||
| 2e05cc74bf | |||
| eb9b86971a | |||
| 31e7ec182c | |||
| 5e8f3b2bc8 | |||
| c0b91c4b0e | |||
| 46d90afa9c | |||
| 29e19b729b | |||
| 20c1eff391 | |||
| 3e40db3d7f | |||
| 8ca5983093 | |||
| 144f568a5c | |||
| 34c7dd48ae | |||
| 6c7d8c16bb | |||
| 2c930df8aa | |||
| 834bed2b1a | |||
| 8d530ef220 | |||
| 542d68dcda | |||
| 50696a0d74 | |||
| 182fc6fd8f | |||
| fe85cddf88 | |||
| 9ff3761cac | |||
| a311dcbd3e | |||
| 447bd67fe1 | |||
| d8ba2b521c | |||
| 8de15429fb | |||
| 3f398d8934 | |||
| f8ec957193 | |||
| 7cc121ab38 | |||
| 8a4918309a | |||
| 30d7fac927 | |||
| be71c6df56 | |||
| 06ad67f99c | |||
| 28fb6f7c27 | |||
| 842d32d41b | |||
| b52cf8327a | |||
| d14526f161 | |||
| 1b8a6b705c | |||
| 7c2b15fe86 | |||
| 3085f05d51 | |||
| 4344e06707 | |||
| 173ec75bb3 | |||
| 1d3f8bf898 | |||
| 5b3b87d3e2 | |||
| 6dc5b33d87 | |||
| 408b843156 | |||
| 0820170261 | |||
| 254ac6f2ce | |||
| 468a7ac883 | |||
| 3e610c80e1 | |||
| f43edbd31f | |||
| 7c57f2cee4 | |||
| 8d612eca46 | |||
| 98f4d55aa0 | |||
| 709b09c4ec | |||
| 818876a22e | |||
| 2657eb7866 | |||
| aaecbf07f2 | |||
| f336638a17 | |||
| 839fbe477c | |||
| 35ad5441d3 | |||
| 756dec264d | |||
| 87983ab610 | |||
| 66ffc3448e | |||
| c6e308717d | |||
| 4c4dd03411 | |||
| 2d0f873342 | |||
| 041627ec4a | |||
| da4b8004f2 | |||
| 3428494468 | |||
| 0c2046f93b | |||
| eb31f035e6 | |||
| df51404a14 | |||
| 3e78e441d4 | |||
| 02c2e55855 | |||
| a528624274 | |||
| 1d83d42e9f | |||
| 4684cfb780 | |||
| 991c9ad610 | |||
| e826c54a42 | |||
| 9ae658c1b9 | |||
| 4341aaf65c | |||
| ad847a82c8 | |||
| dad3e6839f | |||
| d078ef6155 | |||
| 210c5749f1 | |||
| 0c74abbc50 | |||
| dbadfe19b0 | |||
| f3e43dbfa4 | |||
| b846a6dd81 | |||
| c82e469fc3 | |||
| f2c9a8f723 | |||
| 47fc073b70 | |||
| 160600e8c0 | |||
| 993c103270 | |||
| e077980ba2 | |||
| 63d14b798b | |||
| 077d63a9fc | |||
| 453c4e12db | |||
| f7db52e069 | |||
| 2bd8c56e64 | |||
| f231c74314 | |||
| 2cb6ee8e6d | |||
| c24770a774 | |||
| 7fa06cb028 | |||
| 50383098ff | |||
| 6f780a499c | |||
| 425e48a46d | |||
| 3dd81fbe2c | |||
| 6a0333e812 | |||
| b3a789af90 | |||
| 560e582e41 | |||
| de7397a20e | |||
| 8bd94318c0 | |||
| fe3cc09ae0 | |||
| 3a3cc54067 | |||
| 47f8b32ea1 | |||
| 49748dbd4b | |||
| 25ea5fdd73 | |||
| 5fadde5a6d | |||
| b6be4d5170 | |||
| c9bac4ff2b | |||
| fedf7d214f | |||
| c969f903b7 | |||
| bd5d7aafee | |||
| e015a531da | |||
| b9014a5e2a | |||
| e9487b0851 | |||
| c60bfb877a | |||
| ee32b1f600 | |||
| 9641aa9082 | |||
| a8ca77f4fc | |||
| e647ff935e | |||
| 7f04a9a18b | |||
| 67d2cb790d | |||
| 279c78b3e2 | |||
| 9514388108 | |||
| e6dc10933c | |||
| c456356424 | |||
| 5af326b36e | |||
| 5548f38393 | |||
| d9c1188f87 | |||
| 588702756b | |||
| d6a74d389d | |||
| d807d71e22 | |||
| 587545ae82 | |||
| 49985e5476 | |||
| 4fbe79a27d | |||
| f61ad19ae6 | |||
| c9a49006f6 | |||
| ca9eb70db5 | |||
| f173aea6e4 | |||
| e37ad11b47 | |||
| d6c2a63f5c | |||
| 4ebf5056be | |||
| a79d409f9d | |||
| 5941495e68 | |||
| b3491582d0 | |||
| def4bbbed2 | |||
| 1dd2b2c9e8 | |||
| e4b269e0de | |||
| cb72d4375f | |||
| 7ec384c61a | |||
| 7466f77eae | |||
| 526b5c4630 | |||
| 4043f9bf5d | |||
| ff5dcbf631 | |||
| 6c053a86bf | |||
| 0cae54cc3f | |||
| 692aceba50 | |||
| c4a86a3d0a | |||
| 5f5aa81174 | |||
| 6e0f258a39 | |||
| c4bfbd0f44 | |||
| 8e0ee47637 | |||
| 0915eeed51 | |||
| fb54e869e9 | |||
| 9e97ed3134 | |||
| b926c4287a | |||
| ddf4d575b7 | |||
| 2a954e3ce3 | |||
| b837865226 | |||
| eac5a5eb35 | |||
| d6c64027f6 | |||
| df4b69666c | |||
| 5675ac7f46 | |||
| 6b2233f8c4 | |||
| 61dd560499 | |||
| 62567ca6eb | |||
| 46dc2a9c5e | |||
| 891583b70e | |||
| e19bdbfd59 | |||
| 14d0cc1935 | |||
| b8d0384da7 | |||
| 4e0a6d15ca | |||
| 251433382f | |||
| 34e993435d | |||
| dc2775e194 | |||
| 45c3752cae | |||
| ed178602d7 | |||
| 35a03278c3 | |||
| 9e69b631ee | |||
| 5ff556f6c3 | |||
| d64960679f | |||
| 7ff1170681 | |||
| 55e25a3717 | |||
| 3f977b79fa | |||
| aca8c8b8ee | |||
| 47c24b9a17 | |||
| 47445b10f1 | |||
| c5a9a1e215 | |||
| 2ef14ded41 | |||
| 8205da898e | |||
| 618e47250d | |||
| 5110aa64aa | |||
| bcad0a3059 | |||
| b7b88f58d2 | |||
| 412fcab4dc | |||
| 8e75a940f7 | |||
| 70fb7899e6 | |||
| 1480fada6e | |||
| c50358366f | |||
| adb4428a69 | |||
| 667a8e684c | |||
| f4b50db972 | |||
| 1abb2efc51 | |||
| c4132252d3 | |||
| 51c76a15ad | |||
| f1842ba5d0 | |||
| d8dd72fd9c | |||
| 054f5e28f6 | |||
| 38e35b99d0 | |||
| 39afb531ef | |||
| c1ff5ff49f | |||
| 2358e4c32f | |||
| 409fccb709 | |||
| b25fd830ec | |||
| 02ab57870a | |||
| eca3749b28 | |||
| 3f17325bac | |||
| 23c09b2c9d | |||
| c1f8232450 | |||
| e28073361d | |||
| 1c2fb1ab72 | |||
| be89e3aacb | |||
| 36427b0e12 | |||
| f8a9d12c88 | |||
| 5f5e979e16 | |||
| 519f281844 | |||
| 3b31bbec0c | |||
| f2942db316 | |||
| e4712be946 | |||
| bc8c4f5e58 | |||
| fe9354a886 | |||
| d00ff8fa1f | |||
| 60f521cc23 | |||
| bcb9a86a00 | |||
| 3f0712010f | |||
| d89194f071 | |||
| a20ad728b5 | |||
| 0d546dce5f | |||
| 38cc9fb7c8 | |||
| 616c193a30 | |||
| 4a88e7cfee | |||
| 5d0fed5e53 | |||
| 9975365a1e | |||
| f18e0b18a1 | |||
| b18100228e | |||
| de568837fb | |||
| bc582ae101 | |||
| bb573117e1 | |||
| ff7077b742 | |||
| bb70229dd8 | |||
| 03947618ff | |||
| b18e7d71ed | |||
| 612ba6fa29 | |||
| db39c6bea6 | |||
| 5f3b56a987 | |||
| 373709fb38 | |||
| 5d8ad3a4a9 | |||
| 0ca35d6c4a | |||
| daeffc07b3 | |||
| bd15f4ecbe | |||
| f17f4e2bf6 | |||
| 177ec1216f | |||
| 512a2d2662 | |||
| 95582a6c3c | |||
| 866b5fea40 | |||
| 34ea42aec0 | |||
| cae7e43b91 | |||
| 34d15a4d37 | |||
| f6cb8186c6 | |||
| 47044b1a23 | |||
| 05d46e6027 | |||
| 338769508e | |||
| 93ebae6601 | |||
| 780c264e59 | |||
| 9a899c1cb1 | |||
| 2703f7f7d4 | |||
| 8d2e672996 | |||
| 5a25e65da3 | |||
| c197808b42 | |||
| ed34719295 | |||
| a052a79aaf | |||
| b6542477bb | |||
| a573b650c9 | |||
| 789bd317b3 | |||
| 2b39476d9b | |||
| 6dcefe49c2 | |||
| 150d9e4b05 | |||
| 54bd1d7931 | |||
| 7ae31d0cb1 | |||
| f7f58dfd71 | |||
| 780a4630e4 | |||
| 3356e0cc82 | |||
| fda374ee81 | |||
| 0264e49968 | |||
| d42c449612 | |||
| 925d10f2ff | |||
| 4402f59e74 | |||
| 20184552a8 | |||
| 832fedb05e | |||
| eeb14f6cbe | |||
| a562f73b1e | |||
| 7295f29055 | |||
| 723d7973d5 | |||
| d5e7a9c949 | |||
| 8f064581d6 | |||
| 634edf2b65 | |||
| 935e4df927 | |||
| 1d72d2774f | |||
| 1e72131e7f | |||
| e8b3949db3 | |||
| c501a39ad4 | |||
| a04f9187f8 | |||
| 32e2070f56 | |||
| 4ee96aaffc | |||
| 0783cf89ba | |||
| cf02e694f2 | |||
| cf178d603c | |||
| ee94c86164 | |||
| 3526761580 | |||
| 9a08975c8e | |||
| 6b56c9efd8 | |||
| 0f2ada0958 | |||
| 0d17ea353f | |||
| 13e26b13e7 | |||
| 72f1bd6180 | |||
| e32ea1627e | |||
| ed1f2e29ed | |||
| 92cb18207e | |||
| 80f6b8d2cd | |||
| 05969fefde | |||
| 81c962238a | |||
| 56218ee5d7 | |||
| aa9138b281 | |||
| 6f231523b3 | |||
| 943b3fbd91 | |||
| 40ff880597 | |||
| 0647be1bc3 | |||
| b069b20e18 | |||
| 91b73a2b16 | |||
| 14d0f6877a | |||
| a2210bce48 | |||
| 68cb85a2b2 | |||
| 72fcc50f80 | |||
| 5721c3622d | |||
| 50eb46dc82 | |||
| 8aae16ffd7 | |||
| e402ed4ce8 | |||
| a1a04ee513 | |||
| affdc25256 | |||
| 8db78efbbc | |||
| d8184e72eb | |||
| 3bd57d4307 | |||
| 42193f1b06 | |||
| a277e6d37f | |||
| bf6fa4cd55 | |||
| 6501a44e6a | |||
| ee30008f38 | |||
| 22cb8a1878 | |||
| 111f916a78 | |||
| a6e1f05957 | |||
| 0b64c68191 | |||
| 713039279c | |||
| d317e5d73c | |||
| ee93c278df | |||
| 1009ea86ae | |||
| 7d8e7af308 | |||
| 136522c694 | |||
| 6801811226 | |||
| a4434d79c9 | |||
| e0b1b5dc05 | |||
| 1a63d8f0b7 | |||
| 5bf3b11edf | |||
| 8f1722f2a8 | |||
| 5d95387935 | |||
| bd93a9a40e | |||
| 5cde4a6630 | |||
| de5511f009 | |||
| 9bdd9fa831 | |||
| 48bb3dbbe7 | |||
| b8bf847fc1 | |||
| 17812b6949 | |||
| bab979aaf4 | |||
| 42778dc79d | |||
| a948be9c85 | |||
| 9c381c1022 | |||
| 9002f82659 | |||
| 5f7fb4699a | |||
| 5907104e0e | |||
| d7dff5b026 | |||
| cabde8ed11 | |||
| b02fd92ad0 | |||
| 9be8578aff | |||
| 4f28dd85bf | |||
| 74119e8861 | |||
| e76b8f7e15 | |||
| 31bd5c6790 | |||
| 50f036d283 | |||
| 8c73f0c655 | |||
| 8de76deb1b | |||
| b65728d46f | |||
| 0b4b4ea791 | |||
| 552ab81739 | |||
| d49d12249a | |||
| ed1d406b72 | |||
| 80a48f53ad | |||
| 51cfaaacee | |||
| 2f9866cf04 | |||
| 7de74e2c04 | |||
| 019de4ffa0 | |||
| 9f1e3c179b | |||
| 17e17f0b9c | |||
| 5da36d13c8 | |||
| cce322f9c8 | |||
| ed3b03f454 | |||
| 27e1cded2e | |||
| ad3d1fb6b3 | |||
| d2fecb6701 | |||
| 685386df13 | |||
| f94b202341 | |||
| d1a6956e77 | |||
| 2d2215edbe | |||
| bcd0d20e2f | |||
| ba5881355d | |||
| 1072d0a019 | |||
| 783c86aa78 | |||
| 5564fe8852 | |||
| e1f0037fd5 | |||
| daa984f7de | |||
| aa0eb760de | |||
| 9ed65bc321 | |||
| ce95b6089f | |||
| c6ba71ae33 | |||
| e57d38cf57 | |||
| 9bea0cff24 | |||
| 197da2c585 | |||
| d2ecd745f6 | |||
| e99939db85 | |||
| 600a708e7b | |||
| a94a5f1716 | |||
| 46064680ce | |||
| 6fe5acfc97 | |||
| 3369903766 | |||
| a0c86d9645 | |||
| 7a454888a3 | |||
| 37f52e1c6c | |||
| 185423539e | |||
| 9e20659d5d | |||
| 7783188769 | |||
| 514af54c4c | |||
| ad615b7612 | |||
| a1b7906a7d | |||
| 79c8d2c345 | |||
| dcf6af405d | |||
| bb598b61a5 | |||
| 1c554c4912 | |||
| 21f8b7ed31 | |||
| 23ee8e25dd | |||
| 1098095846 | |||
| 3e7d7e8a31 | |||
| 2c45316bcb | |||
| 8dc7c1f876 | |||
| db84936dcd | |||
| 75d7d07013 | |||
| d4d5f45edc | |||
| d0257d1cb2 | |||
| ecf44348cf | |||
| cc8bc05537 | |||
| 728d646ce2 | |||
| ca397dca0f | |||
| 1fbe6815c3 | |||
| c61f70727f | |||
| 2abbf58825 | |||
| b979b2ea1e | |||
| 24b968ad39 | |||
| faa8aa2b9c | |||
| db9ee9d87b | |||
| 1dbb494b94 | |||
| fe52b4cb78 | |||
| 5519442ad8 | |||
| 88363d8033 | |||
| fb5d8f29ac | |||
| 912b121d27 | |||
| 2e975d9b19 | |||
| edc93e62b4 | |||
| 9d6ffa951f | |||
| 079ec023b7 | |||
| e55a1c7e00 | |||
| ddd737e4d8 | |||
| 38a15afc9c | |||
| fa93daabd2 | |||
| 6b0987385e | |||
| 48fbda844f | |||
| bc70f3c051 | |||
| d2f255d613 | |||
| bf86b168d7 | |||
| e5ca44bb04 | |||
| 1f563c964c | |||
| 9a9730d59e | |||
| af3ce4b32b | |||
| 03f0c3a001 | |||
| 639833acf1 | |||
| 60893d2797 | |||
| 9e45111d8b | |||
| 0080f17c1f | |||
| fa47af3dd6 | |||
| c4ff07124b | |||
| 900cf5d071 | |||
| 8a6ced0e8f | |||
| f20401c657 | |||
| b987fc1de2 | |||
| efeac2ef39 | |||
| 6b80055bd2 | |||
| 0af53e99ee | |||
| bc0c2a6be2 | |||
| 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 |
@@ -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,43 +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",
|
||||
"-Wclippy::redundant_clone",
|
||||
]
|
||||
|
||||
[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
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
# 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",
|
||||
"Unicode-3.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",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
"https://github.com/jplatte/async-compat",
|
||||
# We can release vodozemac whenever we need but let's not block development
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
]
|
||||
@@ -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 }}
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
java-version: '17'
|
||||
|
||||
- name: Install android sdk
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.4
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.7
|
||||
|
||||
- name: Install android ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
test-apple:
|
||||
name: matrix-rust-components-swift
|
||||
needs: xtask
|
||||
runs-on: macos-12
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
@@ -175,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=reldbg
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
@@ -186,12 +186,12 @@ jobs:
|
||||
|
||||
test-crypto-apple-framework-generation:
|
||||
name: Generate Crypto FFI Apple XCFramework
|
||||
runs-on: macos-12
|
||||
runs-on: macos-15
|
||||
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
|
||||
|
||||
+44
-40
@@ -18,6 +18,9 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Insta.rs is run directly via cargo test. We don't want insta.rs to create new snapshots files.
|
||||
# Just want it to run the tests (option `no` instead of `auto`).
|
||||
INSTA_UPDATE: no
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
@@ -40,7 +43,6 @@ jobs:
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
- image-proc
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -49,6 +51,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:
|
||||
@@ -114,6 +121,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
|
||||
|
||||
@@ -165,6 +177,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:
|
||||
@@ -206,12 +224,16 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m], no-default'
|
||||
cmd: matrix-sdk-no-default
|
||||
|
||||
- name: '[m]-ui'
|
||||
cmd: matrix-sdk-ui
|
||||
check_only: true
|
||||
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
|
||||
- name: '[m], no-default, wasm-flags'
|
||||
cmd: matrix-sdk-no-default
|
||||
|
||||
- name: '[m], indexeddb stores'
|
||||
cmd: matrix-sdk-indexeddb-stores
|
||||
|
||||
@@ -230,6 +252,7 @@ jobs:
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.0
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.10.3
|
||||
|
||||
@@ -259,27 +282,10 @@ jobs:
|
||||
target/debug/xtask ci wasm ${{ matrix.cmd }}
|
||||
|
||||
- name: Wasm-Pack test
|
||||
if: '!matrix.check_only'
|
||||
run: |
|
||||
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
|
||||
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
run: |
|
||||
cargo fmt -- --check
|
||||
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
@@ -289,10 +295,10 @@ jobs:
|
||||
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.29.5
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
lint:
|
||||
name: Lint
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -308,8 +314,8 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
components: clippy
|
||||
toolchain: nightly-2024-11-26
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -323,6 +329,10 @@ jobs:
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Check Formatting
|
||||
run: |
|
||||
target/debug/xtask ci style
|
||||
|
||||
- name: Clippy
|
||||
run: |
|
||||
target/debug/xtask ci clippy
|
||||
@@ -335,7 +345,7 @@ jobs:
|
||||
# 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
|
||||
@@ -353,21 +363,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
|
||||
@@ -378,6 +377,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
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ jobs:
|
||||
# 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
|
||||
@@ -47,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
|
||||
@@ -74,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
|
||||
|
||||
@@ -101,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
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-06-25
|
||||
toolchain: nightly-2024-11-26
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -51,11 +51,9 @@ 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
|
||||
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
# 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 successful execution
|
||||
@@ -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-15
|
||||
os-name: 🍏
|
||||
cachekey-id: macos
|
||||
|
||||
|
||||
+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).
|
||||
+132
-27
@@ -29,43 +29,148 @@ 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*.
|
||||
### Snapshot Testing
|
||||
|
||||
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.
|
||||
You can add/review snapshot tests using [insta.rs](https://insta.rs)
|
||||
|
||||
A *good commit message* should be composed of:
|
||||
Every new struct/enum that derives `Serialize` `Deserialise` should have a snapshot test for it.
|
||||
Any code change that breaks serialisation will then break a test, the author will then have to decide
|
||||
how to handle migration and test it if needed.
|
||||
|
||||
- a prefix to indicate which area/feature is related by the commit
|
||||
- a short description that would give sufficient context for a reviewer to guess what the commit is
|
||||
about.
|
||||
|
||||
Examples of commit messages that aren't so useful:
|
||||
And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool:
|
||||
|
||||
- “add new method“
|
||||
- “enhance performance“
|
||||
- “fix receipts“
|
||||
Unix:
|
||||
```
|
||||
curl -LsSf https://insta.rs/install.sh | sh
|
||||
```
|
||||
|
||||
Examples of good commit messages:
|
||||
Windows:
|
||||
```
|
||||
powershell -c "irm https://insta.rs/install.ps1 | iex"
|
||||
```
|
||||
|
||||
- “ffi: Add new method `frobnicate_the_foos`”
|
||||
- “indexeddb: Break up the request inside `get_inbound_group_sessions`”
|
||||
- “read_receipts: Store receipts locally, fixing #12345”
|
||||
Usual flow is to first run the test, then review them.
|
||||
```
|
||||
cargo insta test
|
||||
cargo insta review
|
||||
```
|
||||
|
||||
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.
|
||||
## Pull requests
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
|
||||
branch name.)
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
each commit should have a *good commit message*.
|
||||
|
||||
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.
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR,
|
||||
following the same guidelines of a good commit message, including the
|
||||
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
|
||||
be a good PR title.
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`,
|
||||
that is, just the branch name.)
|
||||
|
||||
# Writing changelog entries
|
||||
|
||||
Our goal is to maintain clear, concise, and informative changelogs that
|
||||
accurately document changes in the project. Changelog entries should be written
|
||||
manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file.
|
||||
|
||||
Be sure to include a link to the pull request for additional context. A
|
||||
well-written changelog entry should be understandable even to those who may not
|
||||
be deeply familiar with the project. Provide enough context to ensure clarity
|
||||
and ease of understanding.
|
||||
|
||||
A couple of examples of bad changelog entry would look like:
|
||||
|
||||
```markdown
|
||||
- Fixed a panic.
|
||||
```
|
||||
|
||||
```markdown
|
||||
- Added the Bar function to Foo.
|
||||
```
|
||||
|
||||
A good example of a changelog entry could look like the following:
|
||||
|
||||
```markdown
|
||||
- Use the inviter's server name and the server name from the room alias as
|
||||
fallback values for the via parameter when requesting the room summary from
|
||||
the homeserver. This ensures requests succeed even when the room being
|
||||
previewed is hosted on a federated server.
|
||||
([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357))
|
||||
```
|
||||
|
||||
For security-related changelog entries, please include the following additional
|
||||
details alongside the pull request number:
|
||||
|
||||
* Impact: Clearly describe the issue's potential impact on users or systems.
|
||||
* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier.
|
||||
* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context.
|
||||
|
||||
```markdown
|
||||
- Use a constant-time Base64 encoder for secret key material to mitigate
|
||||
side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
|
||||
```
|
||||
|
||||
## Commit message format
|
||||
|
||||
Commit messages should be formatted as Conventional Commits. In addition, some
|
||||
git trailers are supported and have special meaning (see below).
|
||||
|
||||
### Conventional commits
|
||||
|
||||
Conventional Commits are structured as follows:
|
||||
|
||||
```
|
||||
<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).
|
||||
|
||||
### Security fixes
|
||||
|
||||
Commits addressing security vulnerabilities must include specific trailers for
|
||||
vulnerability metadata, which should also be reflected in the corresponding
|
||||
changelog entry.
|
||||
|
||||
The metadata must be included in the following 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.
|
||||
|
||||
Please include all of the fields that are available.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Review process
|
||||
|
||||
@@ -126,7 +231,7 @@ requested.
|
||||
commits, the [autosquash] option can help with this.
|
||||
|
||||
```bash
|
||||
git rebase main --autosquash
|
||||
git rebase main --interactive --autosquash
|
||||
```
|
||||
|
||||
[fixup]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt
|
||||
|
||||
Generated
+1011
-997
File diff suppressed because it is too large
Load Diff
+97
-46
@@ -18,36 +18,49 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.83"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.68"
|
||||
assert-json-diff = "2"
|
||||
anyhow = "1.0.95"
|
||||
aquamarine = "0.6.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.1"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.60"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.22.0"
|
||||
byteorder = "1.4.3"
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.6.0"
|
||||
futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
growable-bloom-filter = "2.1.0"
|
||||
http = "1.1.0"
|
||||
imbl = "3.0.0"
|
||||
itertools = "0.12.0"
|
||||
once_cell = "1.16.0"
|
||||
pin-project-lite = "0.2.9"
|
||||
eyeball-im = { version = "0.6.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.8.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
getrandom = { version = "0.2.15", default-features = false }
|
||||
gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.2.0"
|
||||
imbl = "4.0.1"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.69"
|
||||
mime = "0.3.17"
|
||||
once_cell = "1.20.2"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d", features = [
|
||||
reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.1", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -60,37 +73,45 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d" }
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
ruma-common = { version = "0.15.1" }
|
||||
serde = "1.0.217"
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.5.0"
|
||||
similar-asserts = "1.6.1"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tempfile = "3.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
tracing-subscriber = "0.3.18"
|
||||
unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.0"
|
||||
vodozemac = { git = "https://github.com/matrix-org/vodozemac", rev = "57cbf7e939d7b54d20207e8361b7135bd65c9cc2", features = ["insecure-pk-encryption"] }
|
||||
wiremock = "0.6.0"
|
||||
zeroize = "1.6.0"
|
||||
url = "2.5.4"
|
||||
uuid = "1.12.1"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
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-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 = { path = "crates/matrix-sdk", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
@@ -107,6 +128,9 @@ debug = 0
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
sha2 = { opt-level = 2 }
|
||||
# faster runs for insta.rs snapshot testing
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
# Custom profile with full debugging info, use `--profile dbg` to select
|
||||
[profile.dbg]
|
||||
@@ -122,10 +146,37 @@ 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]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
trivial_casts = "warn"
|
||||
trivial_numeric_casts = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
box_default = "allow"
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
inefficient_to_string = "warn"
|
||||
macro_use_imports = "warn"
|
||||
mut_mut = "warn"
|
||||
needless_borrow = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
str_to_string = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
redundant_clone = "warn"
|
||||
|
||||
@@ -24,17 +24,11 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
|
||||
used to add Matrix E2EE support to your client or client library.
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
|
||||
|
||||
## Status
|
||||
|
||||
The library is in an alpha state, things that are implemented generally work but
|
||||
the API will change in breaking ways.
|
||||
The library is considered production ready and backs multiple client implementations such as Element X [[1]](https://github.com/element-hq/element-x-ios) [[2]](https://github.com/element-hq/element-x-android) and [Fractal](https://gitlab.gnome.org/World/fractal). Client developers should feel confident to build upon it.
|
||||
|
||||
If you are interested in using the matrix-sdk now is the time to try it out and
|
||||
provide feedback.
|
||||
Development of the SDK has been primarily sponsored by Element though accepts contributions from all.
|
||||
|
||||
## Bindings
|
||||
|
||||
|
||||
-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`, set the versions in
|
||||
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
|
||||
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
|
||||
@@ -1,121 +0,0 @@
|
||||
# Upgrades 0.5 ➜ 0.6
|
||||
|
||||
This is a rough migration guide to help you upgrade your code using matrix-sdk 0.5 to the newly released matrix-sdk 0.6 . While it won't cover all edge cases and problems, we are trying to get the most common issues covered. If you experience any other difficulties in upgrade or need support with using the matrix-sdk in general, please approach us in our [matrix-sdk channel on matrix.org][matrix-channel].
|
||||
|
||||
## Minimum Supported Rust Version Update: `1.60`
|
||||
|
||||
We have updated the minimal rust version you need in order to build `matrix-sdk`, as we require some new dependency resolving features from it:
|
||||
|
||||
> These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.60
|
||||
|
||||
## Dependencies
|
||||
|
||||
Many dependencies have been upgraded. Most notably, we are using `ruma` at version `0.7.0` now. It has seen some renamings and restructurings since our last release, so you might find that some Types have new names now.
|
||||
|
||||
## Repo Structure Updates
|
||||
|
||||
If you are looking at the repository itself, you will find we've rearranged the code quite a bit: we have split out any bindings-specific and testing related crates (and other things) into respective folders, and we've moved all `examples` into its own top-level-folder with each example as their own crate (rendering them easier to find and copy as starting points), all in all slimming down the `crates` folder to the core aspects.
|
||||
|
||||
|
||||
## Architecture Changes / API overall
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
We are moving to the [builder pattern][] (familiar from e.g. `std::io:process:Command`) as the main configurable path for many aspects of the API, including to construct Matrix-Requests and workflows. This has been and is an on-going effort, and this release sees a lot of APIs transitioning to this pattern, you should already be familiar with from the `matrix_sdk::Client::builder()` in `0.5`. This pattern been extended onto:
|
||||
- the [login configuration][login builder] and [login with sso][ssologin builder],
|
||||
- [`SledStore` configuratiion][sled-store builder]
|
||||
- [`Indexeddb` configuration][indexeddb builder]
|
||||
|
||||
Most have fallback (though maybe with deprecation warning) support for an existing code path, but these are likely to be removed in upcoming releases.
|
||||
|
||||
### Splitting of concerns: Media
|
||||
|
||||
In an effort to declutter the `Client` API dedicated types have been created dealing with specific concerns in one place. In `0.5` we introduced `client.account()`, and `client.encryption()`, we are doing the same with `client.media()` to manage media and attachments in one place with the [`media::Media` type][media typ] now.
|
||||
|
||||
The signatures of media uploads, have also changed slightly: rather than expecting a reader `R: Read + Seek`, it now is a simple `&[u8]`. Which also means no more unnecessary `seek(0)` to reset the cursor, as we are just taking an immutable reference now.
|
||||
|
||||
### Event Handling & sync updaes
|
||||
|
||||
If you are using the `client.register_event_handler` function to receive updates on incoming sync events, you'll find yourself with a deprecation warning now. That is because we've refactored and redesigned the event handler logic to allowing `removing` of event handlers on the fly, too. For that the new `add_event_handler()` (and `add_room_event_handler`) will hand you an `EventHandlerHandle` (pardon the pun), which you can pass to `remove_event_handler`, or by using the convenient `client.event_handler_drop_guard` to create a `DropGuard` that will remove the handler when the guard is dropped. While the code still works, we recommend you switch to the new one, as we will be removing the `register_event_handler` and `register_event_handler_context` in a coming release.
|
||||
|
||||
Secondly, you will find a new [`sync_with_result_callback` sync function][sync with result]. Other than the previous sync functions, this will pass the entire `Result` to your callback, allowing you to handle errors or even raise some yourself to stop the loop. Further more, it will propagate any unhandled errors (it still handles retries as before) to the outer caller, allowing the higher level to decide how to handle that (e.g. in case of a network failure). This result-returning-behavior also punshes through the existing `sync` and `sync_with_callback`-API, allowing you to handle them on a higher level now (rather than the futures just resolving). If you find that warning, just adding a `?` to the `.await` of the call is probably the quickest way to move forward.
|
||||
|
||||
### Refresh Tokens
|
||||
|
||||
This release now [supports `refresh_token`s][refresh tokens PR] as part of the [`Session`][session]. It is implemented with a default-flag in serde so deserializing a previously serialized Session (e.g. in a store) will work as before. As part of `refresh_token` support, you can now configure the client via `ClientBuilder.request_refresh_token()` to refresh the access token automagically on certain failures or do it manually by calling `client.refresh_access_token()` yourself. Auto-refresh is _off_ by default.
|
||||
|
||||
You can stay informed about updates on the access token by listening to `client.session_tokens_signal()`.
|
||||
|
||||
### Further changes
|
||||
|
||||
- [`MessageOptions`][message options] has been updated to Matrix 1.3 by making the `from` parameter optional (and function signatures have been updated, too). You can now request the server sends you messages from the first one you are allowed to have received.
|
||||
- `client.user_id()` is not a `future` anymore. Remove any `.await` you had behind it.
|
||||
- `verified()`, `blacklisted()` and `deleted()` on `matrix_sdk::encryption::identities::Device` have been renamed with a `is_` prefix.
|
||||
- `verified()` on `matrix_sdk::encryption::identities::UserIdentity`, too has been prefixed with `is_` and thus is now called `is_verified()`.
|
||||
- The top-level crypto and state-store types of Indexeddb and Sled have been renamed to unique types>
|
||||
- `state_store` and `crypto_store` do not need to be boxed anymore when passed to the [`StoreConfig`][store config]
|
||||
- Indexeddb's `SerializationError` is now `IndexedDBStoreError`
|
||||
- Javascript specific features are now behind the `js` feature-gate
|
||||
- The new experimental next generation of sync ("sliding sync"), with a totally revamped api, can be found behind the optional `sliding-sync`-feature-gate
|
||||
|
||||
|
||||
## Quick Troubleshooting
|
||||
|
||||
You find yourself focused with any of these, here are the steps to follow to upgrade your code accordingly:
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::register_event_handler`: Use [`Client::add_event_handler`](#method.add_event_handler) instead
|
||||
|
||||
As it says on the tin: we have deprecated this function in favor of the newer removable handler approach (see above). You can still continue to use this `fn` for now, but it will be removed in a future release.
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::login`: Replaced by [`Client::login_username`](#method.login_username)
|
||||
|
||||
We have replaced the login facilities with a `LoginBuilder` and recommend you use that from now on. This isn't an error yet, but the function will be removed in a future release.
|
||||
|
||||
### expected slice `[u8]`, found struct ...
|
||||
|
||||
We've updated the `send_attachment` and `Media` signatures to use `&[u8]` rather than `reader: Read + Seek` as it is more convenient and common place for most architectures anyways. If you are using `File::open(path)?` to get that handler, you can just replace that with `std::fs::read(path)?`
|
||||
|
||||
### no method named `verified` found for struct `matrix_sdk::encryption::identities::Device` in the current scope
|
||||
|
||||
Boolean flags like `verified`, `deleted`, `blacklisted`, etc have been renamed with a `is_` prefix. So, just follow the cargo suggestion:
|
||||
```
|
||||
|
|
||||
69 | device.verified()
|
||||
| ^^^^^^^^ help: there is an associated function with a similar name: `is_verified`
|
||||
```
|
||||
|
||||
### unresolved import `matrix_sdk::ruma::events::AnySyncRoomEvent`
|
||||
|
||||
Ruma has been updated to `0.7.0`, you will find some ruma Events names have changed, most notably, the `AnySyncRoomEvent` is now named `AnySyncTimelineEvent` (and not `AnySyncStateEvent`, which cargo wrongly suggests). Just rename the import and usage of it.
|
||||
|
||||
### `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
||||
You are seeing something along the lines of:
|
||||
```
|
||||
19 | if room_member.state_key != client.user_id().await.unwrap() {
|
||||
| ^^^^^^ `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
|
||||
= help: the trait `Future` is not implemented for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
= note: std::option::Option<&matrix_sdk::ruma::UserId> must be a future or must implement `IntoFuture` to be awaited
|
||||
= note: required because of the requirements on the impl of `IntoFuture` for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
help: remove the `.await`
|
||||
|
|
||||
19 - if room_member.state_key != client.user_id().await.unwrap() {
|
||||
19 + if room_member.state_key != client.user_id().unwrap() {
|
||||
```
|
||||
|
||||
You are using `client.user_id().await` but `user_id()` is no longer `async`. Just follow the cargo suggestion and remove the `.await`, it is not necessary any longer.
|
||||
|
||||
|
||||
[matrix-channel]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
|
||||
[builder pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
|
||||
[login builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.LoginBuilder.html
|
||||
[ssologin builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.SsoLoginBuilder.html
|
||||
[sled-store builder]: https://docs.rs/matrix-sdk-sled/latest/matrix_sdk_sled/struct.SledStateStoreBuilder.html
|
||||
[indexeddb builder]: https://docs.rs/matrix-sdk-indexeddb/latest/matrix_sdk_indexeddb/struct.IndexeddbStateStoreBuilder.html
|
||||
[media type]: https://docs.rs/matrix-sdk/latest/matrix_sdk//media/struct.Media.html
|
||||
[sync with result]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Client.html#method.sync_with_result_callback
|
||||
[session]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Session.html
|
||||
[refresh tokens PR]: https://github.com/matrix-org/matrix-rust-sdk/pull/892
|
||||
[store config]: https://docs.rs/matrix-sdk-base/latest/matrix_sdk_base/store/struct.StoreConfig.html
|
||||
[message options]: https://docs.rs/matrix-sdk/latest/matrix_sdk/room/struct.MessagesOptions.html
|
||||
@@ -23,7 +23,7 @@ tokio = { workspace = true, default-features = false, features = ["rt-multi-thre
|
||||
wiremock = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
@@ -36,3 +36,6 @@ harness = false
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
test_utils::{events::EventFactory, logged_in_client_with_server},
|
||||
utils::IntoRawStateEventContent,
|
||||
};
|
||||
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder};
|
||||
use matrix_sdk_test::{
|
||||
event_factory::EventFactory, 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_user_id,
|
||||
events::room::member::{MembershipState, RoomMemberEvent},
|
||||
mxc_uri, owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
};
|
||||
@@ -34,28 +32,17 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let ev_builder = EventBuilder::new();
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
let member_content_json = json!({
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support",
|
||||
});
|
||||
let member_content: Raw<RoomMemberEventContent> =
|
||||
member_content_json.into_raw_state_event_content().cast();
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
|
||||
let state_key = user_id.to_string();
|
||||
let event: Raw<RoomMemberEvent> = ev_builder
|
||||
.make_state_event(
|
||||
&user_id,
|
||||
&room_id,
|
||||
&state_key,
|
||||
member_content.deserialize().unwrap(),
|
||||
None,
|
||||
)
|
||||
.cast();
|
||||
let event = f
|
||||
.member(&user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.avatar_url(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
|
||||
.display_name("Alice Margatroid")
|
||||
.reason("Looking for support")
|
||||
.into_raw();
|
||||
member_events.push(event);
|
||||
}
|
||||
|
||||
@@ -74,7 +61,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(
|
||||
@@ -171,8 +161,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let room = client.get_room(&room_id).expect("Room not found");
|
||||
assert!(!room.pinned_event_ids().is_empty());
|
||||
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
|
||||
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");
|
||||
@@ -191,8 +182,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
assert!(!room.pinned_event_ids().is_empty());
|
||||
assert_eq!(room.pinned_event_ids().len(), PINNED_EVENTS_COUNT);
|
||||
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;
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::{MatrixSession, MatrixSessionTokens},
|
||||
config::StoreConfig,
|
||||
matrix_auth::{MatrixSession, MatrixSessionTokens},
|
||||
Client, RoomInfo, RoomState, StateChanges,
|
||||
};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
@@ -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");
|
||||
|
||||
@@ -13,6 +13,7 @@ let package = Package(
|
||||
],
|
||||
products: [
|
||||
.library(name: "MatrixRustSDK",
|
||||
type: .dynamic,
|
||||
targets: ["MatrixRustSDK"]),
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -26,6 +26,7 @@ futures-util = { workspace = true }
|
||||
hmac = "0.12.1"
|
||||
http = { 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 }
|
||||
@@ -66,3 +67,6 @@ assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -71,10 +71,6 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
|
||||
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
|
||||
```
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::{env, error::Error};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
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
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-ffi/build.rs] too!
|
||||
@@ -12,26 +17,45 @@ 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");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
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/lib/clang/{clang_version}/lib/linux/"
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_crypto::dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
use matrix_sdk_crypto::{
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Handle;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum DehydrationError {
|
||||
#[error(transparent)]
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::DehydratedDeviceError),
|
||||
#[error(transparent)]
|
||||
LegacyPickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
#[error(transparent)]
|
||||
MissingSigningKey(#[from] matrix_sdk_crypto::SignatureError),
|
||||
#[error(transparent)]
|
||||
@@ -22,6 +28,8 @@ pub enum DehydrationError {
|
||||
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
|
||||
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
|
||||
PickleKeyLength(usize),
|
||||
#[error(transparent)]
|
||||
Rand(#[from] rand::Error),
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
|
||||
@@ -29,10 +37,16 @@ impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for Dehydrati
|
||||
match value {
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Json(e) => Self::Json(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Pickle(e) => Self::Pickle(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::LegacyPickle(e) => {
|
||||
Self::LegacyPickle(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::MissingSigningKey(e) => {
|
||||
Self::MissingSigningKey(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => {
|
||||
Self::PickleKeyLength(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +67,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())?;
|
||||
@@ -66,14 +80,14 @@ impl DehydratedDevices {
|
||||
|
||||
pub fn rehydrate(
|
||||
&self,
|
||||
pickle_key: Vec<u8>,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
device_id: String,
|
||||
device_data: String,
|
||||
) -> Result<Arc<RehydratedDevice>, DehydrationError> {
|
||||
let device_data: Raw<_> = serde_json::from_str(&device_data)?;
|
||||
let device_id: OwnedDeviceId = device_id.into();
|
||||
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let ret = RehydratedDevice {
|
||||
runtime: self.runtime.to_owned(),
|
||||
@@ -85,10 +99,41 @@ impl DehydratedDevices {
|
||||
}
|
||||
.into();
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Get the cached dehydrated device pickle key if any.
|
||||
///
|
||||
/// None if the key was not previously cached (via
|
||||
/// [`Self::save_dehydrated_device_pickle_key`]).
|
||||
///
|
||||
/// Should be used to periodically rotate the dehydrated device to avoid
|
||||
/// OTK exhaustion and accumulation of to_device messages.
|
||||
pub fn get_dehydrated_device_key(
|
||||
&self,
|
||||
) -> Result<Option<crate::DehydratedDeviceKey>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.get_dehydrated_device_pickle_key())?
|
||||
.map(crate::DehydratedDeviceKey::from))
|
||||
}
|
||||
|
||||
/// Store the dehydrated device pickle key in the crypto store.
|
||||
///
|
||||
/// This is useful if the client wants to periodically rotate dehydrated
|
||||
/// devices to avoid OTK exhaustion and accumulated to_device problems.
|
||||
pub fn save_dehydrated_device_key(
|
||||
&self,
|
||||
pickle_key: &crate::DehydratedDeviceKey,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?)
|
||||
}
|
||||
|
||||
/// Deletes the previously stored dehydrated device pickle key.
|
||||
pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -107,7 +152,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,20 +178,18 @@ impl Drop for DehydratedDevice {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
device_display_name: String,
|
||||
pickle_key: Vec<u8>,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let request =
|
||||
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(request.into())
|
||||
}
|
||||
}
|
||||
@@ -177,15 +220,36 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
|
||||
let pickle_key_length = pickle_key.len();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
|
||||
|
||||
if pickle_key_length == 32 {
|
||||
let mut key = Box::new([0u8; 32]);
|
||||
key.copy_from_slice(pickle_key);
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key() {
|
||||
let result = DehydratedDeviceKey::new();
|
||||
assert!(result.is_ok());
|
||||
let dehydrated_device_key = result.unwrap();
|
||||
let base_64 = dehydrated_device_key.to_base64();
|
||||
let inner_bytes = dehydrated_device_key.inner;
|
||||
|
||||
Ok(key)
|
||||
} else {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length))
|
||||
let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap();
|
||||
|
||||
assert_eq!(base_64, copy.to_base64());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key_failure() {
|
||||
let bytes = [0u8; 24];
|
||||
|
||||
let pickle_key = DehydratedDeviceKey::from_slice(&bytes);
|
||||
|
||||
assert!(pickle_key.is_err());
|
||||
|
||||
match pickle_key {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length)) => {
|
||||
assert_eq!(bytes.len(), pickle_key_length);
|
||||
}
|
||||
_ => panic!("Should have failed!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
|
||||
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
|
||||
SignatureError as InnerSignatureError,
|
||||
};
|
||||
use matrix_sdk_sqlite::OpenStoreError;
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
@@ -57,6 +58,8 @@ pub enum CryptoStoreError {
|
||||
InvalidUserId(String, IdParseError),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
#[error(transparent)]
|
||||
DehydrationError(#[from] InnerDehydrationError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -112,7 +115,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_withheld_error_mapping() {
|
||||
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
|
||||
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
|
||||
|
||||
|
||||
@@ -36,7 +36,10 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
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},
|
||||
store::{
|
||||
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
@@ -62,6 +65,8 @@ pub use verification::{
|
||||
};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use crate::dehydrated_devices::DehydrationError;
|
||||
|
||||
/// Struct collecting data that is important to migrate to the rust-sdk
|
||||
#[derive(Deserialize, Serialize, uniffi::Record)]
|
||||
pub struct MigrationData {
|
||||
@@ -196,7 +201,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,
|
||||
@@ -359,7 +364,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,
|
||||
@@ -532,7 +537,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,
|
||||
@@ -558,7 +563,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
|
||||
///
|
||||
@@ -675,15 +680,20 @@ pub struct EncryptionSettings {
|
||||
|
||||
impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
fn from(v: EncryptionSettings) -> Self {
|
||||
let sharing_strategy = if v.only_allow_trusted_devices {
|
||||
CollectStrategy::OnlyTrustedDevices
|
||||
} else if v.error_on_verified_user_problem {
|
||||
CollectStrategy::ErrorOnVerifiedUserProblem
|
||||
} else {
|
||||
CollectStrategy::AllDevices
|
||||
};
|
||||
|
||||
RustEncryptionSettings {
|
||||
algorithm: v.algorithm.into(),
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
sharing_strategy: CollectStrategy::DeviceBasedStrategy {
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
error_on_verified_user_problem: v.error_on_verified_user_problem,
|
||||
},
|
||||
sharing_strategy,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -794,7 +804,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> {
|
||||
@@ -822,6 +832,39 @@ impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dehydrated device key
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct DehydratedDeviceKey {
|
||||
pub(crate) inner: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DehydratedDeviceKey {
|
||||
/// Generates a new random pickle key.
|
||||
pub fn new() -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::new()?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Creates a new dehydration pickle key from the given slice.
|
||||
///
|
||||
/// Fail if the slice length is not 32.
|
||||
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
|
||||
inner.to_base64()
|
||||
}
|
||||
}
|
||||
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
|
||||
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
|
||||
DehydratedDeviceKey { inner: pickle_key.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
@@ -891,7 +934,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(),
|
||||
@@ -915,12 +958,12 @@ 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()
|
||||
}
|
||||
@@ -935,7 +978,7 @@ pub struct PkEncryption {
|
||||
inner: matrix_sdk_crypto::vodozemac::pk_encryption::PkEncryption,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl PkEncryption {
|
||||
/// Create a new [`PkEncryption`] object from a `Curve25519PublicKey`
|
||||
/// encoded as Base64.
|
||||
|
||||
@@ -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,8 +17,8 @@ use matrix_sdk_crypto::{
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, TrustRequirement,
|
||||
UserIdentities,
|
||||
types::requests::ToDeviceRequest,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
@@ -38,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};
|
||||
@@ -178,7 +179,7 @@ impl From<RustSignatureCheckResult> for SignatureVerification {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl OlmMachine {
|
||||
/// Create a new `OlmMachine`
|
||||
///
|
||||
@@ -314,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 {
|
||||
@@ -528,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"),
|
||||
@@ -540,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 {
|
||||
@@ -861,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
|
||||
@@ -882,8 +885,6 @@ impl OlmMachine {
|
||||
let event: Raw<_> = serde_json::from_str(&event)?;
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(
|
||||
&event,
|
||||
&room_id,
|
||||
@@ -891,7 +892,7 @@ impl OlmMachine {
|
||||
))?;
|
||||
|
||||
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,
|
||||
@@ -909,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())?;
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@ use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
types::requests::{
|
||||
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
},
|
||||
CrossSigningBootstrapRequests,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
@@ -136,7 +139,7 @@ pub enum Request {
|
||||
|
||||
impl From<OutgoingRequest> for Request {
|
||||
fn from(r: OutgoingRequest) -> Self {
|
||||
use matrix_sdk_crypto::OutgoingRequests::*;
|
||||
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*;
|
||||
|
||||
match r.request() {
|
||||
KeysUpload(u) => {
|
||||
@@ -338,16 +341,16 @@ impl From<RoomMessageResponse> for OwnedResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> {
|
||||
fn from(r: &'a OwnedResponse) -> Self {
|
||||
match r {
|
||||
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
|
||||
OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
@@ -791,8 +791,7 @@ impl VerificationRequest {
|
||||
// task.
|
||||
let should_break = matches!(
|
||||
state,
|
||||
RustVerificationRequestState::Done { .. }
|
||||
| RustVerificationRequestState::Cancelled { .. }
|
||||
RustVerificationRequestState::Done | RustVerificationRequestState::Cancelled { .. }
|
||||
);
|
||||
|
||||
let state = Self::convert_verification_request(&request, state);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[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"
|
||||
publish = false
|
||||
|
||||
[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
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
@@ -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,6 +2,9 @@
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
|
||||
original message and the associated error code and kind.
|
||||
|
||||
- `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
|
||||
@@ -31,4 +34,7 @@ Breaking changes:
|
||||
|
||||
Additions:
|
||||
|
||||
- Add `Encryption::get_user_identity` which returns `UserIdentity`
|
||||
- Add `ClientBuilder::room_key_recipient_strategy`
|
||||
- Add `Room::send_raw`
|
||||
- Expose `withdraw_verification` to `UserIdentity`
|
||||
|
||||
@@ -28,11 +28,11 @@ eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-util = { workspace = true }
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
matrix-sdk-ffi-macros = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -56,7 +56,6 @@ features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"rustls-tls", # note: differ from block below
|
||||
@@ -71,7 +70,6 @@ features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"native-tls", # note: differ from block above
|
||||
@@ -82,3 +80,6 @@ features = [
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::{env, error::Error};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
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
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-crypto-ffi/build.rs] too!
|
||||
@@ -12,26 +17,45 @@ 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");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
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/lib/clang/{clang_version}/lib/linux/"
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
|
||||
|
||||
@@ -8,15 +8,3 @@ dictionary Mentions {
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
interface MediaSource {
|
||||
[Name=from_json, Throws=ClientError]
|
||||
constructor(string json);
|
||||
string to_json();
|
||||
string url();
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
oidc::{
|
||||
authentication::oidc::{
|
||||
registrations::OidcRegistrationsError,
|
||||
types::{
|
||||
iana::oauth::OAuthClientAuthenticationMethod,
|
||||
@@ -19,17 +19,18 @@ use matrix_sdk::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::client::{Client, SlidingSyncVersion};
|
||||
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverLoginDetails {
|
||||
pub(crate) url: 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 {
|
||||
@@ -46,6 +47,12 @@ 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
|
||||
@@ -62,7 +69,7 @@ pub struct SsoHandler {
|
||||
pub(crate) url: String,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[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
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
mem::ManuallyDrop,
|
||||
path::Path,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use matrix_sdk::{
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequest, MediaThumbnailSettings,
|
||||
},
|
||||
oidc::{
|
||||
authentication::oidc::{
|
||||
registrations::{ClientId, OidcRegistrations},
|
||||
requests::account_management::AccountManagementActionFull,
|
||||
types::{
|
||||
@@ -19,28 +15,31 @@ use matrix_sdk::{
|
||||
registration::{
|
||||
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
|
||||
},
|
||||
requests::Prompt as SdkOidcPrompt,
|
||||
},
|
||||
OidcAuthorizationData, OidcSession,
|
||||
},
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaThumbnailSettings,
|
||||
},
|
||||
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,
|
||||
user_directory::search_users,
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent, MediaSource,
|
||||
},
|
||||
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
|
||||
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
@@ -54,13 +53,19 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
|
||||
room::{
|
||||
join_rules::{
|
||||
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, 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;
|
||||
@@ -74,9 +79,10 @@ use crate::{
|
||||
notification_settings::NotificationSettings,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::AuthData,
|
||||
ruma::{AuthData, MediaSource},
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utils::AsyncRuntimeDropped,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
@@ -114,7 +120,7 @@ impl TryFrom<PusherKind> for RumaPusherKind {
|
||||
let mut ruma_data = RumaHttpPusherData::new(data.url);
|
||||
if let Some(payload) = data.default_payload {
|
||||
let json: Value = serde_json::from_str(&payload)?;
|
||||
ruma_data.default_payload = json;
|
||||
ruma_data.data.insert("default_payload".to_owned(), json);
|
||||
}
|
||||
ruma_data.format = data.format.map(Into::into);
|
||||
Ok(Self::Http(ruma_data))
|
||||
@@ -140,25 +146,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.
|
||||
@@ -182,58 +188,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 {
|
||||
@@ -260,11 +260,35 @@ 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_version = self.sliding_sync_version();
|
||||
|
||||
@@ -272,6 +296,7 @@ impl Client {
|
||||
url: self.homeserver(),
|
||||
sliding_sync_version,
|
||||
supports_oidc_login,
|
||||
supported_oidc_prompts,
|
||||
supports_password_login,
|
||||
})
|
||||
}
|
||||
@@ -295,6 +320,31 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -335,13 +385,14 @@ impl Client {
|
||||
Ok(Arc::new(SsoHandler { client: Arc::clone(self), url }))
|
||||
}
|
||||
|
||||
/// Requests the URL needed for login in a web view using OIDC. Once the web
|
||||
/// 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);
|
||||
@@ -362,14 +413,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;
|
||||
}
|
||||
|
||||
@@ -389,7 +441,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>,
|
||||
@@ -401,8 +453,8 @@ impl Client {
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequest { source, format: MediaFormat::File },
|
||||
body,
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
@@ -449,7 +501,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 {
|
||||
@@ -501,7 +553,7 @@ 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 {
|
||||
@@ -552,6 +604,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) => {
|
||||
@@ -617,7 +673,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()))
|
||||
}
|
||||
|
||||
@@ -650,7 +706,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();
|
||||
@@ -670,12 +726,13 @@ impl Client {
|
||||
&self,
|
||||
media_source: Arc<MediaSource>,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let source = (*media_source).clone().media_source;
|
||||
|
||||
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?)
|
||||
}
|
||||
|
||||
@@ -685,16 +742,16 @@ impl Client {
|
||||
width: u64,
|
||||
height: u64,
|
||||
) -> Result<Vec<u8>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let source = (*media_source).clone().media_source;
|
||||
|
||||
debug!(?source, width, height, "requesting media thumbnail");
|
||||
Ok(self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_content(
|
||||
&MediaRequest {
|
||||
&MediaRequestParameters {
|
||||
source,
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
|
||||
Method::Scale,
|
||||
UInt::new(width).unwrap(),
|
||||
UInt::new(height).unwrap(),
|
||||
)),
|
||||
@@ -947,6 +1004,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
|
||||
@@ -969,10 +1040,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.
|
||||
@@ -984,7 +1066,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
|
||||
@@ -997,16 +1079,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")?;
|
||||
|
||||
@@ -1014,9 +1096,9 @@ 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
|
||||
@@ -1058,9 +1140,21 @@ impl Client {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: Sync + Send {
|
||||
fn call(&self, ignored_user_ids: Vec<String>);
|
||||
}
|
||||
@@ -1281,16 +1375,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()
|
||||
@@ -1318,6 +1419,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 {
|
||||
@@ -1326,12 +1433,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1342,6 +1451,9 @@ pub enum RoomVisibility {
|
||||
|
||||
/// Indicates that the room will not be shown in the published room list.
|
||||
Private,
|
||||
|
||||
/// A custom value that's not present in the spec.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<RoomVisibility> for Visibility {
|
||||
@@ -1349,6 +1461,17 @@ impl From<RoomVisibility> for Visibility {
|
||||
match value {
|
||||
RoomVisibility::Public => Self::Public,
|
||||
RoomVisibility::Private => Self::Private,
|
||||
RoomVisibility::Custom { value } => value.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Visibility> for RoomVisibility {
|
||||
fn from(value: Visibility) -> Self {
|
||||
match value {
|
||||
Visibility::Public => Self::Public,
|
||||
Visibility::Private => Self::Private,
|
||||
_ => Self::Custom { value: value.as_str().to_owned() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1412,10 +1535,13 @@ impl Session {
|
||||
match auth_api {
|
||||
// Build the session from the regular Matrix Auth Session.
|
||||
AuthApi::Matrix(a) => {
|
||||
let matrix_sdk::matrix_auth::MatrixSession {
|
||||
let matrix_sdk::authentication::matrix::MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::matrix_auth::MatrixSessionTokens { access_token, refresh_token },
|
||||
matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
} = a.session().context("Missing session")?;
|
||||
|
||||
Ok(Session {
|
||||
@@ -1430,10 +1556,10 @@ impl Session {
|
||||
}
|
||||
// Build the session from the OIDC UserSession.
|
||||
AuthApi::Oidc(api) => {
|
||||
let matrix_sdk::oidc::UserSession {
|
||||
let matrix_sdk::authentication::oidc::UserSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::oidc::OidcSessionTokens {
|
||||
matrix_sdk::authentication::oidc::OidcSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
@@ -1494,12 +1620,12 @@ impl TryFrom<Session> for AuthSession {
|
||||
.transpose()
|
||||
.context("OIDC latest_id_token is invalid.")?;
|
||||
|
||||
let user_session = matrix_sdk::oidc::UserSession {
|
||||
let user_session = matrix_sdk::authentication::oidc::UserSession {
|
||||
meta: matrix_sdk::SessionMeta {
|
||||
user_id: user_id.try_into()?,
|
||||
device_id: device_id.into(),
|
||||
},
|
||||
tokens: matrix_sdk::oidc::OidcSessionTokens {
|
||||
tokens: matrix_sdk::authentication::oidc::OidcSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
@@ -1513,15 +1639,15 @@ impl TryFrom<Session> for AuthSession {
|
||||
user: user_session,
|
||||
};
|
||||
|
||||
Ok(AuthSession::Oidc(session))
|
||||
Ok(AuthSession::Oidc(session.into()))
|
||||
} else {
|
||||
// Create a regular Matrix Session.
|
||||
let session = matrix_sdk::matrix_auth::MatrixSession {
|
||||
let session = matrix_sdk::authentication::matrix::MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta {
|
||||
user_id: user_id.try_into()?,
|
||||
device_id: device_id.into(),
|
||||
},
|
||||
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
|
||||
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
@@ -1617,7 +1743,7 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn gen_transaction_id() -> String {
|
||||
TransactionId::new().to_string()
|
||||
}
|
||||
@@ -1635,7 +1761,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> {
|
||||
@@ -1699,3 +1825,206 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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 },
|
||||
|
||||
/// A custom allow rule implementation, containing its JSON representation
|
||||
/// as a `String`.
|
||||
Custom { json: String },
|
||||
}
|
||||
|
||||
impl TryFrom<JoinRule> for RumaJoinRule {
|
||||
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 = ruma_allow_rules_from_ffi(rules)?;
|
||||
Ok(Self::Restricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::KnockRestricted { rules } => {
|
||||
let rules = ruma_allow_rules_from_ffi(rules)?;
|
||||
Ok(Self::KnockRestricted(ruma::events::room::join_rules::Restricted::new(rules)))
|
||||
}
|
||||
JoinRule::Custom { repr } => Ok(serde_json::from_str(&repr)?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ruma_allow_rules_from_ffi(value: Vec<AllowRule>) -> Result<Vec<RumaAllowRule>, ClientError> {
|
||||
let mut ret = Vec::with_capacity(value.len());
|
||||
for rule in value {
|
||||
let rule: Result<RumaAllowRule, ClientError> = rule.try_into();
|
||||
match rule {
|
||||
Ok(rule) => ret.push(rule),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
impl TryFrom<AllowRule> for RumaAllowRule {
|
||||
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,
|
||||
)))
|
||||
}
|
||||
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaJoinRule> for JoinRule {
|
||||
type Error = String;
|
||||
fn try_from(value: RumaJoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaJoinRule::Knock => Ok(JoinRule::Knock),
|
||||
RumaJoinRule::Public => Ok(JoinRule::Public),
|
||||
RumaJoinRule::Private => Ok(JoinRule::Private),
|
||||
RumaJoinRule::KnockRestricted(restricted) => {
|
||||
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
|
||||
Vec<_>,
|
||||
Self::Error,
|
||||
>>(
|
||||
)?;
|
||||
Ok(JoinRule::KnockRestricted { rules })
|
||||
}
|
||||
RumaJoinRule::Restricted(restricted) => {
|
||||
let rules = restricted.allow.into_iter().map(TryInto::try_into).collect::<Result<
|
||||
Vec<_>,
|
||||
Self::Error,
|
||||
>>(
|
||||
)?;
|
||||
Ok(JoinRule::Restricted { rules })
|
||||
}
|
||||
RumaJoinRule::Invite => Ok(JoinRule::Invite),
|
||||
RumaJoinRule::_Custom(_) => Ok(JoinRule::Custom { repr: value.as_str().to_owned() }),
|
||||
_ => Err(format!("Unknown JoinRule: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaAllowRule> for AllowRule {
|
||||
type Error = String;
|
||||
fn try_from(value: RumaAllowRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaAllowRule::RoomMembership(membership) => {
|
||||
Ok(AllowRule::RoomMembership { room_id: membership.room_id.to_string() })
|
||||
}
|
||||
RumaAllowRule::_Custom(repr) => {
|
||||
let json = serde_json::to_string(&repr)
|
||||
.map_err(|e| format!("Couldn't serialize custom AllowRule: {e:?}"))?;
|
||||
Ok(Self::Custom { json })
|
||||
}
|
||||
_ => Err(format!("Invalid AllowRule: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::{
|
||||
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
CollectStrategy,
|
||||
CollectStrategy, TrustRequirement,
|
||||
},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
reqwest::Certificate,
|
||||
ruma::{ServerName, UserId},
|
||||
sliding_sync::{
|
||||
@@ -47,7 +48,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.
|
||||
///
|
||||
@@ -159,7 +160,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);
|
||||
}
|
||||
@@ -202,6 +203,8 @@ pub enum ClientBuildError {
|
||||
SlidingSyncVersion(VersionBuilderError),
|
||||
#[error(transparent)]
|
||||
Sdk(MatrixClientBuildError),
|
||||
#[error(transparent)]
|
||||
EventCache(#[from] EventCacheError),
|
||||
#[error("Failed to build the client: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
@@ -260,16 +263,22 @@ pub struct ClientBuilder {
|
||||
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>,
|
||||
|
||||
/// Whether to enable use of the event cache store, for reloading events
|
||||
/// when building timelines et al.
|
||||
use_event_cache_persistent_storage: bool,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
@@ -283,7 +292,8 @@ impl ClientBuilder {
|
||||
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,
|
||||
@@ -294,18 +304,41 @@ impl ClientBuilder {
|
||||
auto_enable_backups: false,
|
||||
},
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
request_config: Default::default(),
|
||||
use_event_cache_persistent_storage: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable_cross_process_refresh_lock(
|
||||
/// Whether to use the event cache persistent storage or not.
|
||||
///
|
||||
/// This is a temporary feature flag, for testing the event cache's
|
||||
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
|
||||
///
|
||||
/// This is disabled by default. When disabled, a one-time cleanup is
|
||||
/// performed when creating the client, and it will clear all the events
|
||||
/// previously stored in the event cache.
|
||||
///
|
||||
/// When enabled, it will attempt to store events in the event cache as
|
||||
/// they're received, and reuse them when reconstructing timelines.
|
||||
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.use_event_cache_persistent_storage = value;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
self: Arc<Self>,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -449,6 +482,16 @@ impl ClientBuilder {
|
||||
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);
|
||||
@@ -460,9 +503,14 @@ impl ClientBuilder {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
|
||||
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);
|
||||
let data_path = Path::new(&session_paths.data_path);
|
||||
let cache_path = Path::new(&session_paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
@@ -470,12 +518,12 @@ impl ClientBuilder {
|
||||
"Creating directories for data and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(&data_path)?;
|
||||
fs::create_dir_all(&cache_path)?;
|
||||
fs::create_dir_all(data_path)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
|
||||
inner_builder = inner_builder.sqlite_store_with_cache_path(
|
||||
&data_path,
|
||||
&cache_path,
|
||||
data_path,
|
||||
cache_path,
|
||||
builder.passphrase.as_deref(),
|
||||
);
|
||||
} else {
|
||||
@@ -548,7 +596,8 @@ impl ClientBuilder {
|
||||
|
||||
inner_builder = inner_builder
|
||||
.with_encryption_settings(builder.encryption_settings)
|
||||
.with_room_key_recipient_strategy(builder.room_key_recipient_strategy);
|
||||
.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 => {
|
||||
@@ -600,13 +649,22 @@ impl ClientBuilder {
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
if builder.use_event_cache_persistent_storage {
|
||||
// Enable the persistent storage \o/
|
||||
sdk_client.event_cache().enable_storage()?;
|
||||
} else {
|
||||
// Get rid of all the previous events, if any.
|
||||
let store = sdk_client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::LockingStorage)?;
|
||||
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(
|
||||
sdk_client,
|
||||
builder.cross_process_refresh_lock_id,
|
||||
builder.session_delegate,
|
||||
)
|
||||
.await?,
|
||||
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct ElementWellKnown {
|
||||
}
|
||||
|
||||
/// 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,6 +6,7 @@ use matrix_sdk::{
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
@@ -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.
|
||||
@@ -253,7 +254,7 @@ impl Encryption {
|
||||
/// Therefore it is necessary to poll the server for an answer every time
|
||||
/// you want to differentiate between those two states.
|
||||
pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.backups().exists_on_server().await?)
|
||||
Ok(self.inner.backups().fetch_exists_on_server().await?)
|
||||
}
|
||||
|
||||
pub fn recovery_state(&self) -> RecoveryState {
|
||||
@@ -280,7 +281,7 @@ impl Encryption {
|
||||
}
|
||||
|
||||
pub async fn is_last_device(&self) -> Result<bool> {
|
||||
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
|
||||
Ok(self.inner.recovery().is_last_device().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
@@ -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)
|
||||
}
|
||||
@@ -390,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());
|
||||
@@ -402,6 +412,98 @@ 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?)
|
||||
}
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
///
|
||||
/// If an identity was previously verified and is not anymore it will be
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.withdraw_verification().await?)
|
||||
}
|
||||
|
||||
/// Get the public part of the Master key of this user identity.
|
||||
///
|
||||
/// The public part of the Master key is usually used to uniquely identify
|
||||
/// 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)]
|
||||
@@ -409,7 +511,7 @@ pub struct IdentityResetHandle {
|
||||
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl IdentityResetHandle {
|
||||
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
|
||||
/// process is using.
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
use std::fmt::Display;
|
||||
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
|
||||
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
Generic { msg: String },
|
||||
#[error("api error {code}: {msg}")]
|
||||
MatrixApi { kind: ErrorKind, code: String, msg: String },
|
||||
}
|
||||
|
||||
impl ClientError {
|
||||
@@ -40,7 +46,22 @@ impl From<UnexpectedUniFFICallbackError> for ClientError {
|
||||
|
||||
impl From<matrix_sdk::Error> for ClientError {
|
||||
fn from(e: matrix_sdk::Error) -> Self {
|
||||
Self::new(e)
|
||||
match e {
|
||||
matrix_sdk::Error::Http(http_error) => {
|
||||
if let Some(api_error) = http_error.as_client_api_error() {
|
||||
if let ErrorBody::Standard { kind, message } = &api_error.body {
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return Self::Generic { msg: message.to_string() };
|
||||
};
|
||||
return Self::MatrixApi { kind, code, msg: message.to_owned() };
|
||||
}
|
||||
}
|
||||
Self::Generic { msg: http_error.to_string() }
|
||||
}
|
||||
_ => Self::Generic { msg: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +149,12 @@ 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)
|
||||
@@ -146,6 +173,108 @@ impl From<RoomSendQueueError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotYetImplemented> for ClientError {
|
||||
fn from(_: NotYetImplemented) -> Self {
|
||||
Self::new("This functionality is not implemented yet.")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FocusEventError> for ClientError {
|
||||
fn from(e: FocusEventError) -> 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 {
|
||||
@@ -217,3 +346,444 @@ 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;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)]
|
||||
// Please keep the variants sorted alphabetically.
|
||||
pub enum ErrorKind {
|
||||
/// `M_BAD_ALIAS`
|
||||
///
|
||||
/// One or more [room aliases] within the `m.room.canonical_alias` event do
|
||||
/// not point to the room ID for which the state event is to be sent to.
|
||||
///
|
||||
/// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
BadAlias,
|
||||
|
||||
/// `M_BAD_JSON`
|
||||
///
|
||||
/// The request contained valid JSON, but it was malformed in some way, e.g.
|
||||
/// missing required keys, invalid values for keys.
|
||||
BadJson,
|
||||
|
||||
/// `M_BAD_STATE`
|
||||
///
|
||||
/// The state change requested cannot be performed, such as attempting to
|
||||
/// unban a user who is not banned.
|
||||
BadState,
|
||||
|
||||
/// `M_BAD_STATUS`
|
||||
///
|
||||
/// The application service returned a bad status.
|
||||
BadStatus {
|
||||
/// The HTTP status code of the response.
|
||||
status: Option<u16>,
|
||||
|
||||
/// The body of the response.
|
||||
body: Option<String>,
|
||||
},
|
||||
|
||||
/// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
|
||||
///
|
||||
/// The user is unable to reject an invite to join the [server notices]
|
||||
/// room.
|
||||
///
|
||||
/// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
|
||||
CannotLeaveServerNoticeRoom,
|
||||
|
||||
/// `M_CANNOT_OVERWRITE_MEDIA`
|
||||
///
|
||||
/// The [`create_content_async`] endpoint was called with a media ID that
|
||||
/// already has content.
|
||||
///
|
||||
/// [`create_content_async`]: crate::media::create_content_async
|
||||
CannotOverwriteMedia,
|
||||
|
||||
/// `M_CAPTCHA_INVALID`
|
||||
///
|
||||
/// The Captcha provided did not match what was expected.
|
||||
CaptchaInvalid,
|
||||
|
||||
/// `M_CAPTCHA_NEEDED`
|
||||
///
|
||||
/// A Captcha is required to complete the request.
|
||||
CaptchaNeeded,
|
||||
|
||||
/// `M_CONNECTION_FAILED`
|
||||
///
|
||||
/// The connection to the application service failed.
|
||||
ConnectionFailed,
|
||||
|
||||
/// `M_CONNECTION_TIMEOUT`
|
||||
///
|
||||
/// The connection to the application service timed out.
|
||||
ConnectionTimeout,
|
||||
|
||||
/// `M_DUPLICATE_ANNOTATION`
|
||||
///
|
||||
/// The request is an attempt to send a [duplicate annotation].
|
||||
///
|
||||
/// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
|
||||
DuplicateAnnotation,
|
||||
|
||||
/// `M_EXCLUSIVE`
|
||||
///
|
||||
/// The resource being requested is reserved by an application service, or
|
||||
/// the application service making the request has not created the
|
||||
/// resource.
|
||||
Exclusive,
|
||||
|
||||
/// `M_FORBIDDEN`
|
||||
///
|
||||
/// Forbidden access, e.g. joining a room without permission, failed login.
|
||||
Forbidden,
|
||||
|
||||
/// `M_GUEST_ACCESS_FORBIDDEN`
|
||||
///
|
||||
/// The room or resource does not permit [guests] to access it.
|
||||
///
|
||||
/// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
|
||||
GuestAccessForbidden,
|
||||
|
||||
/// `M_INCOMPATIBLE_ROOM_VERSION`
|
||||
///
|
||||
/// The client attempted to join a room that has a version the server does
|
||||
/// not support.
|
||||
IncompatibleRoomVersion {
|
||||
/// The room's version.
|
||||
room_version: String,
|
||||
},
|
||||
|
||||
/// `M_INVALID_PARAM`
|
||||
///
|
||||
/// A parameter that was specified has the wrong value. For example, the
|
||||
/// server expected an integer and instead received a string.
|
||||
InvalidParam,
|
||||
|
||||
/// `M_INVALID_ROOM_STATE`
|
||||
///
|
||||
/// The initial state implied by the parameters to the [`create_room`]
|
||||
/// request is invalid, e.g. the user's `power_level` is set below that
|
||||
/// necessary to set the room name.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
InvalidRoomState,
|
||||
|
||||
/// `M_INVALID_USERNAME`
|
||||
///
|
||||
/// The desired user name is not valid.
|
||||
InvalidUsername,
|
||||
|
||||
/// `M_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request has been refused due to [rate limiting]: too many requests
|
||||
/// have been sent in a short period of time.
|
||||
///
|
||||
/// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
|
||||
LimitExceeded {
|
||||
/// How long a client should wait before they can try again.
|
||||
retry_after_ms: Option<u64>,
|
||||
},
|
||||
|
||||
/// `M_MISSING_PARAM`
|
||||
///
|
||||
/// A required parameter was missing from the request.
|
||||
MissingParam,
|
||||
|
||||
/// `M_MISSING_TOKEN`
|
||||
///
|
||||
/// No [access token] was specified for the request, but one is required.
|
||||
///
|
||||
/// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
MissingToken,
|
||||
|
||||
/// `M_NOT_FOUND`
|
||||
///
|
||||
/// No resource was found for this request.
|
||||
NotFound,
|
||||
|
||||
/// `M_NOT_JSON`
|
||||
///
|
||||
/// The request did not contain valid JSON.
|
||||
NotJson,
|
||||
|
||||
/// `M_NOT_YET_UPLOADED`
|
||||
///
|
||||
/// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used
|
||||
/// and the content is not yet available.
|
||||
///
|
||||
/// [`create_mxc_uri`]: crate::media::create_mxc_uri
|
||||
NotYetUploaded,
|
||||
|
||||
/// `M_RESOURCE_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request cannot be completed because the homeserver has reached a
|
||||
/// resource limit imposed on it. For example, a homeserver held in a
|
||||
/// shared hosting environment may reach a resource limit if it starts
|
||||
/// using too much memory or disk space.
|
||||
ResourceLimitExceeded {
|
||||
/// A URI giving a contact method for the server administrator.
|
||||
admin_contact: String,
|
||||
},
|
||||
|
||||
/// `M_ROOM_IN_USE`
|
||||
///
|
||||
/// The [room alias] specified in the [`create_room`] request is already
|
||||
/// taken.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
/// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
RoomInUse,
|
||||
|
||||
/// `M_SERVER_NOT_TRUSTED`
|
||||
///
|
||||
/// The client's request used a third-party server, e.g. identity server,
|
||||
/// that this server does not trust.
|
||||
ServerNotTrusted,
|
||||
|
||||
/// `M_THREEPID_AUTH_FAILED`
|
||||
///
|
||||
/// Authentication could not be performed on the [third-party identifier].
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidAuthFailed,
|
||||
|
||||
/// `M_THREEPID_DENIED`
|
||||
///
|
||||
/// The server does not permit this [third-party identifier]. This may
|
||||
/// happen if the server only permits, for example, email addresses from
|
||||
/// a particular domain.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidDenied,
|
||||
|
||||
/// `M_THREEPID_IN_USE`
|
||||
///
|
||||
/// The [third-party identifier] is already in use by another user.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidInUse,
|
||||
|
||||
/// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
|
||||
///
|
||||
/// The homeserver does not support adding a [third-party identifier] of the
|
||||
/// given medium.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidMediumNotSupported,
|
||||
|
||||
/// `M_THREEPID_NOT_FOUND`
|
||||
///
|
||||
/// No account matching the given [third-party identifier] could be found.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidNotFound,
|
||||
|
||||
/// `M_TOO_LARGE`
|
||||
///
|
||||
/// The request or entity was too large.
|
||||
TooLarge,
|
||||
|
||||
/// `M_UNABLE_TO_AUTHORISE_JOIN`
|
||||
///
|
||||
/// The room is [restricted] and none of the conditions can be validated by
|
||||
/// the homeserver. This can happen if the homeserver does not know
|
||||
/// about any of the rooms listed as conditions, for example.
|
||||
///
|
||||
/// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToAuthorizeJoin,
|
||||
|
||||
/// `M_UNABLE_TO_GRANT_JOIN`
|
||||
///
|
||||
/// A different server should be attempted for the join. This is typically
|
||||
/// because the resident server can see that the joining user satisfies
|
||||
/// one or more conditions, such as in the case of [restricted rooms],
|
||||
/// but the resident server would be unable to meet the authorization
|
||||
/// rules.
|
||||
///
|
||||
/// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToGrantJoin,
|
||||
|
||||
/// `M_UNAUTHORIZED`
|
||||
///
|
||||
/// The request was not correctly authorized. Usually due to login failures.
|
||||
Unauthorized,
|
||||
|
||||
/// `M_UNKNOWN`
|
||||
///
|
||||
/// An unknown error has occurred.
|
||||
Unknown,
|
||||
|
||||
/// `M_UNKNOWN_TOKEN`
|
||||
///
|
||||
/// The [access or refresh token] specified was not recognized.
|
||||
///
|
||||
/// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
UnknownToken {
|
||||
/// If this is `true`, the client is in a "[soft logout]" state, i.e.
|
||||
/// the server requires re-authentication but the session is not
|
||||
/// invalidated. The client can acquire a new access token by
|
||||
/// specifying the device ID it is already using to the login API.
|
||||
///
|
||||
/// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
|
||||
soft_logout: bool,
|
||||
},
|
||||
|
||||
/// `M_UNRECOGNIZED`
|
||||
///
|
||||
/// The server did not understand the request.
|
||||
///
|
||||
/// This is expected to be returned with a 404 HTTP status code if the
|
||||
/// endpoint is not implemented or a 405 HTTP status code if the
|
||||
/// endpoint is implemented, but the incorrect HTTP method is used.
|
||||
Unrecognized,
|
||||
|
||||
/// `M_UNSUPPORTED_ROOM_VERSION`
|
||||
///
|
||||
/// The request to [`create_room`] used a room version that the server does
|
||||
/// not support.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
UnsupportedRoomVersion,
|
||||
|
||||
/// `M_URL_NOT_SET`
|
||||
///
|
||||
/// The application service doesn't have a URL configured.
|
||||
UrlNotSet,
|
||||
|
||||
/// `M_USER_DEACTIVATED`
|
||||
///
|
||||
/// The user ID associated with the request has been deactivated.
|
||||
UserDeactivated,
|
||||
|
||||
/// `M_USER_IN_USE`
|
||||
///
|
||||
/// The desired user ID is already taken.
|
||||
UserInUse,
|
||||
|
||||
/// `M_USER_LOCKED`
|
||||
///
|
||||
/// The account has been [locked] and cannot be used at this time.
|
||||
///
|
||||
/// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
|
||||
UserLocked,
|
||||
|
||||
/// `M_USER_SUSPENDED`
|
||||
///
|
||||
/// The account has been [suspended] and can only be used for limited
|
||||
/// actions at this time.
|
||||
///
|
||||
/// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
|
||||
UserSuspended,
|
||||
|
||||
/// `M_WEAK_PASSWORD`
|
||||
///
|
||||
/// The password was [rejected] by the server for being too weak.
|
||||
///
|
||||
/// [rejected]: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
|
||||
WeakPassword,
|
||||
|
||||
/// `M_WRONG_ROOM_KEYS_VERSION`
|
||||
///
|
||||
/// The version of the [room keys backup] provided in the request does not
|
||||
/// match the current backup version.
|
||||
///
|
||||
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
|
||||
WrongRoomKeysVersion {
|
||||
/// The currently active backup version.
|
||||
current_version: Option<String>,
|
||||
},
|
||||
|
||||
/// A custom API error.
|
||||
Custom { errcode: String },
|
||||
}
|
||||
|
||||
impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RumaApiErrorKind) -> Result<Self, Self::Error> {
|
||||
match &value {
|
||||
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
|
||||
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
|
||||
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
|
||||
RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
|
||||
status: status.map(|code| code.clone().as_u16()),
|
||||
body: body.clone(),
|
||||
}),
|
||||
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
|
||||
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
|
||||
}
|
||||
RumaApiErrorKind::CannotOverwriteMedia => Ok(ErrorKind::CannotOverwriteMedia),
|
||||
RumaApiErrorKind::CaptchaInvalid => Ok(ErrorKind::CaptchaInvalid),
|
||||
RumaApiErrorKind::CaptchaNeeded => Ok(ErrorKind::CaptchaNeeded),
|
||||
RumaApiErrorKind::ConnectionFailed => Ok(ErrorKind::ConnectionFailed),
|
||||
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
|
||||
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
|
||||
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
|
||||
RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
|
||||
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
|
||||
RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
|
||||
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
|
||||
}
|
||||
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
|
||||
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
|
||||
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
|
||||
RumaApiErrorKind::LimitExceeded { retry_after } => {
|
||||
let retry_after_ms = match retry_after {
|
||||
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
|
||||
Some(RetryAfter::DateTime(system_time)) => {
|
||||
let duration = system_time.duration_since(SystemTime::now()).ok();
|
||||
duration.map(|duration| duration.as_millis() as u64)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok(ErrorKind::LimitExceeded { retry_after_ms })
|
||||
}
|
||||
RumaApiErrorKind::MissingParam => Ok(ErrorKind::MissingParam),
|
||||
RumaApiErrorKind::MissingToken => Ok(ErrorKind::MissingToken),
|
||||
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
|
||||
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
|
||||
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
|
||||
RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
|
||||
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
|
||||
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
|
||||
RumaApiErrorKind::ThreepidAuthFailed => Ok(ErrorKind::ThreepidAuthFailed),
|
||||
RumaApiErrorKind::ThreepidDenied => Ok(ErrorKind::ThreepidDenied),
|
||||
RumaApiErrorKind::ThreepidInUse => Ok(ErrorKind::ThreepidInUse),
|
||||
RumaApiErrorKind::ThreepidMediumNotSupported => {
|
||||
Ok(ErrorKind::ThreepidMediumNotSupported)
|
||||
}
|
||||
RumaApiErrorKind::ThreepidNotFound => Ok(ErrorKind::ThreepidNotFound),
|
||||
RumaApiErrorKind::TooLarge => Ok(ErrorKind::TooLarge),
|
||||
RumaApiErrorKind::UnableToAuthorizeJoin => Ok(ErrorKind::UnableToAuthorizeJoin),
|
||||
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
|
||||
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
|
||||
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
|
||||
RumaApiErrorKind::UnknownToken { soft_logout } => {
|
||||
Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
|
||||
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
|
||||
RumaApiErrorKind::UrlNotSet => Ok(ErrorKind::UrlNotSet),
|
||||
RumaApiErrorKind::UserDeactivated => Ok(ErrorKind::UserDeactivated),
|
||||
RumaApiErrorKind::UserInUse => Ok(ErrorKind::UserInUse),
|
||||
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
|
||||
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
|
||||
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
|
||||
RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
|
||||
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::_Custom { .. } => {
|
||||
// There is no way to map the extra values since they're private, so we omit
|
||||
// them
|
||||
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
|
||||
}
|
||||
// In any other case, return it as the mapping not being yet implemented
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
use anyhow::{bail, Context};
|
||||
use ruma::events::{
|
||||
room::{message::Relation, redaction::SyncRoomRedactionEvent},
|
||||
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::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, NotifyType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
#[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()
|
||||
@@ -25,8 +34,8 @@ impl TimelineEvent {
|
||||
self.0.sender().to_string()
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.origin_server_ts().0.into()
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.0.origin_server_ts().into()
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
@@ -100,7 +109,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,
|
||||
@@ -197,7 +206,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
_ => None,
|
||||
});
|
||||
MessageLikeEventContent::RoomMessage {
|
||||
message_type: original_content.msgtype.into(),
|
||||
message_type: original_content.msgtype.try_into()?,
|
||||
in_reply_to_event_id,
|
||||
}
|
||||
}
|
||||
@@ -350,3 +359,71 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, uniffi::Enum)]
|
||||
pub enum RoomMessageEventMessageType {
|
||||
Audio,
|
||||
Emote,
|
||||
File,
|
||||
Image,
|
||||
Location,
|
||||
Notice,
|
||||
ServerNotice,
|
||||
Text,
|
||||
Video,
|
||||
VerificationRequest,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for RoomMessageEventMessageType {
|
||||
fn from(val: ruma::events::room::message::MessageType) -> Self {
|
||||
match val {
|
||||
RumaMessageType::Audio { .. } => Self::Audio,
|
||||
RumaMessageType::Emote { .. } => Self::Emote,
|
||||
RumaMessageType::File { .. } => Self::File,
|
||||
RumaMessageType::Image { .. } => Self::Image,
|
||||
RumaMessageType::Location { .. } => Self::Location,
|
||||
RumaMessageType::Notice { .. } => Self::Notice,
|
||||
RumaMessageType::ServerNotice { .. } => Self::ServerNotice,
|
||||
RumaMessageType::Text { .. } => Self::Text,
|
||||
RumaMessageType::Video { .. } => Self::Video,
|
||||
RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
@@ -1,24 +1,8 @@
|
||||
// TODO: target-os conditional would be good.
|
||||
|
||||
#![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,
|
||||
}
|
||||
};
|
||||
}
|
||||
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
|
||||
// lines after docs.
|
||||
|
||||
mod authentication;
|
||||
mod chunk_iterator;
|
||||
@@ -29,10 +13,13 @@ mod encryption;
|
||||
mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod identity_status_change;
|
||||
mod live_location_share;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod room;
|
||||
mod room_alias;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
@@ -49,19 +36,17 @@ mod utils;
|
||||
mod widget;
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use matrix_sdk::ruma::events::room::{
|
||||
message::RoomMessageEventContentWithoutRelation, MediaSource,
|
||||
};
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
|
||||
|
||||
use self::{
|
||||
error::ClientError,
|
||||
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
ruma::{Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
uniffi::include_scaffolding!("api");
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
fn sdk_git_sha() -> String {
|
||||
env!("VERGEN_GIT_SHA").to_owned()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 crate::ruma::LocationContent;
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
/// The most recent location content of the user.
|
||||
pub location: LocationContent,
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
pub ts: u64,
|
||||
}
|
||||
/// Details of a users live location share.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LiveLocationShare {
|
||||
/// The user's last known location.
|
||||
pub last_location: LastLocation,
|
||||
/// The live status of the live location share.
|
||||
pub(crate) is_live: bool,
|
||||
/// The user ID of the person sharing their live location.
|
||||
pub user_id: String,
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,8 +14,11 @@ use tracing_subscriber::{
|
||||
EnvFilter, Layer,
|
||||
};
|
||||
|
||||
use crate::tracing::LogLevel;
|
||||
|
||||
pub fn log_panics() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
|
||||
log_panics::init();
|
||||
}
|
||||
|
||||
@@ -228,12 +231,76 @@ pub struct TracingFileConfiguration {
|
||||
max_files: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, PartialOrd)]
|
||||
enum LogTarget {
|
||||
Hyper,
|
||||
MatrixSdkFfi,
|
||||
MatrixSdk,
|
||||
MatrixSdkClient,
|
||||
MatrixSdkCrypto,
|
||||
MatrixSdkCryptoAccount,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkSlidingSync,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogTarget::Hyper => "hyper",
|
||||
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
|
||||
LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
|
||||
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
|
||||
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
|
||||
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::Hyper, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkFfi, LogLevel::Info),
|
||||
(LogTarget::MatrixSdk, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkClient, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkCrypto, LogLevel::Debug),
|
||||
(LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkOidc, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
|
||||
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
];
|
||||
|
||||
const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
];
|
||||
|
||||
#[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,
|
||||
/// The desired log level
|
||||
log_level: LogLevel,
|
||||
|
||||
/// Additional targets that the FFI client would like to use e.g.
|
||||
/// the target names for created [`crate::tracing::Span`]
|
||||
extra_targets: Option<Vec<String>>,
|
||||
|
||||
/// Whether to log to stdout, or in the logcat on Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
@@ -242,12 +309,111 @@ pub struct TracingConfiguration {
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
// We are intentionally not setting a global log level because we don't want to
|
||||
// risk third party crates logging sensitive information.
|
||||
// As such we need to make sure that panics will be properly logged.
|
||||
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
|
||||
let mut filters = vec!["panic=error".to_owned()];
|
||||
|
||||
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
|
||||
// Use the default if the log level shouldn't be changed for this target or
|
||||
// if it's already logging more than requested
|
||||
let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
|
||||
level.as_str()
|
||||
} else {
|
||||
config.log_level.as_str()
|
||||
};
|
||||
|
||||
filters.push(format!("{}={}", target.as_str(), level));
|
||||
});
|
||||
|
||||
// Finally append the extra targets requested by the client
|
||||
if let Some(extra_targets) = &config.extra_targets {
|
||||
for target in extra_targets {
|
||||
filters.push(format!("{}={}", target, config.log_level.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
filters.join(",")
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
log_panics();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(&config.filter))
|
||||
.with(EnvFilter::new(build_tracing_filter(&config)))
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_tracing_filter;
|
||||
|
||||
#[test]
|
||||
fn test_default_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Error,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned()]),
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=debug,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=debug,\
|
||||
matrix_sdk::sliding_sync=info,\
|
||||
matrix_sdk_base::sliding_sync=info,\
|
||||
matrix_sdk_ui::timeline=info,\
|
||||
matrix_sdk::event_cache=info,\
|
||||
matrix_sdk_base::event_cache=info,\
|
||||
matrix_sdk_sqlite::event_cache_store=info,\
|
||||
super_duper_app=error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trace_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Trace,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=trace,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=trace,\
|
||||
matrix_sdk::sliding_sync=trace,\
|
||||
matrix_sdk_base::sliding_sync=trace,\
|
||||
matrix_sdk_ui::timeline=trace,\
|
||||
matrix_sdk::event_cache=trace,\
|
||||
matrix_sdk_base::event_cache=trace,\
|
||||
matrix_sdk_sqlite::event_cache_store=trace,\
|
||||
super_duper_app=trace,\
|
||||
some_other_span=trace"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+471
-107
@@ -1,16 +1,16 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
event_cache::paginator::PaginatorError,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{PaginationError, RoomExt, TimelineFocus};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
@@ -19,35 +19,43 @@ use ruma::{
|
||||
call::notify,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
message::RoomMessageEventContentWithoutRelation,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
TimelineEventType,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedTransactionId, OwnedUserId, RoomAliasId, TransactionId,
|
||||
UserId,
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
ruma::{ImageInfo, Mentions, NotifyType},
|
||||
timeline::{FocusEventError, ReceiptType, Timeline},
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
timeline::{
|
||||
configuration::{AllowedMessageTypes, TimelineConfiguration},
|
||||
ReceiptType, SendHandle, Timeline,
|
||||
},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(Debug, uniffi::Enum)]
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum Membership {
|
||||
Invited,
|
||||
Joined,
|
||||
Left,
|
||||
Knocked,
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl From<RoomState> for Membership {
|
||||
@@ -56,6 +64,8 @@ impl From<RoomState> for Membership {
|
||||
RoomState::Invited => Membership::Invited,
|
||||
RoomState::Joined => Membership::Joined,
|
||||
RoomState::Left => Membership::Left,
|
||||
RoomState::Knocked => Membership::Knocked,
|
||||
RoomState::Banned => Membership::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,12 +88,8 @@ 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()
|
||||
}
|
||||
|
||||
/// Returns the room's name from the state event if available, otherwise
|
||||
/// compute a room name based on the room's nature (DM or not) and number of
|
||||
/// members.
|
||||
@@ -159,7 +165,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
|
||||
}
|
||||
@@ -188,68 +199,42 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a timeline focused on the given event.
|
||||
///
|
||||
/// Note: this timeline is independent from that returned with
|
||||
/// [`Self::timeline`], and as such it is not cached.
|
||||
pub async fn timeline_focused_on_event(
|
||||
/// Build a new timeline instance with the given configuration.
|
||||
pub async fn timeline_with_configuration(
|
||||
&self,
|
||||
event_id: String,
|
||||
num_context_events: u16,
|
||||
internal_id_prefix: Option<String>,
|
||||
) -> Result<Arc<Timeline>, FocusEventError> {
|
||||
let parsed_event_id = EventId::parse(&event_id).map_err(|err| {
|
||||
FocusEventError::InvalidEventId { event_id: event_id.clone(), err: err.to_string() }
|
||||
})?;
|
||||
configuration: TimelineConfiguration,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
|
||||
let room = &self.inner;
|
||||
builder = builder.with_focus(configuration.focus.try_into()?);
|
||||
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
|
||||
if let AllowedMessageTypes::Only { types } = configuration.allowed_message_types {
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
types.contains(&content.msgtype.into())
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
if let Some(internal_id_prefix) = configuration.internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
let timeline = match builder
|
||||
.with_focus(TimelineFocus::Event { target: parsed_event_id, num_context_events })
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
if let matrix_sdk_ui::timeline::Error::PaginationError(
|
||||
PaginationError::Paginator(PaginatorError::EventNotFound(..)),
|
||||
) = err
|
||||
{
|
||||
return Err(FocusEventError::EventNotFound { event_id: event_id.to_string() });
|
||||
}
|
||||
return Err(FocusEventError::Other { msg: err.to_string() });
|
||||
}
|
||||
};
|
||||
builder = builder.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
let timeline = builder.build().await?;
|
||||
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 id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
@@ -269,7 +254,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> {
|
||||
@@ -290,7 +275,7 @@ impl Room {
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
Ok(RoomInfo::new(&self.inner).await?)
|
||||
RoomInfo::new(&self.inner).await
|
||||
}
|
||||
|
||||
pub fn subscribe_to_room_info_updates(
|
||||
@@ -328,6 +313,22 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a raw event to the room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_type` - The type of the event to send.
|
||||
///
|
||||
/// * `content` - The content of the event to send encoded as JSON string.
|
||||
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
|
||||
let content_json: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
|
||||
|
||||
self.inner.send_raw(&event_type, content_json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -378,15 +379,12 @@ impl Room {
|
||||
let int_score = score.map(|value| value.into());
|
||||
self.inner
|
||||
.client()
|
||||
.send(
|
||||
report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
reason,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.send(report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
reason,
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -582,6 +580,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> {
|
||||
@@ -600,7 +623,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))
|
||||
}
|
||||
|
||||
@@ -756,10 +779,8 @@ impl Room {
|
||||
pub async fn withdraw_verification_and_resend(
|
||||
&self,
|
||||
user_ids: Vec<String>,
|
||||
transaction_id: String,
|
||||
send_handle: Arc<SendHandle>,
|
||||
) -> Result<(), ClientError> {
|
||||
let transaction_id: OwnedTransactionId = transaction_id.into();
|
||||
|
||||
let user_ids: Vec<OwnedUserId> =
|
||||
user_ids.iter().map(UserId::parse).collect::<Result<_, _>>()?;
|
||||
|
||||
@@ -771,7 +792,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.send_queue().unwedge(&transaction_id).await?;
|
||||
send_handle.try_resend().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -789,10 +810,8 @@ impl Room {
|
||||
pub async fn ignore_device_trust_and_resend(
|
||||
&self,
|
||||
devices: HashMap<String, Vec<String>>,
|
||||
transaction_id: String,
|
||||
send_handle: Arc<SendHandle>,
|
||||
) -> Result<(), ClientError> {
|
||||
let transaction_id: OwnedTransactionId = transaction_id.into();
|
||||
|
||||
let encryption = self.inner.client().encryption();
|
||||
|
||||
for (user_id, device_ids) in devices.iter() {
|
||||
@@ -807,32 +826,313 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.send_queue().unwedge(&transaction_id).await?;
|
||||
send_handle.try_resend().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to manually resend messages that failed to send due to issues
|
||||
/// that should now have been fixed.
|
||||
/// Clear the event cache storage for the current room.
|
||||
///
|
||||
/// This is useful for example, when there's a
|
||||
/// `SessionRecipientCollectionError::VerifiedUserChangedIdentity` error;
|
||||
/// the user may have re-verified on a different device and would now
|
||||
/// like to send the failed message that's waiting on this device.
|
||||
/// This will remove all the information related to the event cache, in
|
||||
/// memory and in the persisted storage, if enabled.
|
||||
pub async fn clear_event_cache_storage(&self) -> Result<(), ClientError> {
|
||||
let (room_event_cache, _drop_handles) = self.inner.event_cache().await?;
|
||||
room_event_cache.clear().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to requests to join this room (knock member events), using a
|
||||
/// `listener` to be notified of the changes.
|
||||
///
|
||||
/// # Arguments
|
||||
/// The current requests to join the room will be emitted immediately
|
||||
/// when subscribing, along with a [`TaskHandle`] to cancel the
|
||||
/// subscription.
|
||||
pub async fn subscribe_to_knock_requests(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn KnockRequestsListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?;
|
||||
|
||||
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
pin_mut!(stream);
|
||||
while let Some(requests) = stream.next().await {
|
||||
listener.call(requests.into_iter().map(Into::into).collect());
|
||||
}
|
||||
// Cancel the seen ids cleanup task
|
||||
seen_ids_cleanup_handle.abort();
|
||||
})));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Return a debug representation for the internal room events data
|
||||
/// structure, one line per entry in the resulting vector.
|
||||
pub async fn room_events_debug_string(&self) -> Result<Vec<String>, ClientError> {
|
||||
let (cache, _drop_guards) = self.inner.event_cache().await?;
|
||||
Ok(cache.debug_string().await)
|
||||
}
|
||||
|
||||
/// Update the canonical alias of the room.
|
||||
///
|
||||
/// * `transaction_id` - The send queue transaction identifier of the local
|
||||
/// echo that should be unwedged.
|
||||
pub async fn try_resend(&self, transaction_id: String) -> Result<(), ClientError> {
|
||||
let transaction_id: &TransactionId = transaction_id.as_str().into();
|
||||
self.inner.send_queue().unwedge(transaction_id).await?;
|
||||
/// Note that publishing the alias in the room directory is done separately.
|
||||
pub async fn update_canonical_alias(
|
||||
&self,
|
||||
alias: Option<String>,
|
||||
alt_aliases: Vec<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let new_alias = alias.map(TryInto::try_into).transpose()?;
|
||||
let new_alt_aliases =
|
||||
alt_aliases.into_iter().map(RoomAliasId::parse).collect::<Result<_, _>>()?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_canonical_alias(new_alias, new_alt_aliases)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Publish a new room alias for this room in the room directory.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `true` if the room alias didn't exist and it's now published.
|
||||
/// - `false` if the room alias was already present so it couldn't be
|
||||
/// published.
|
||||
pub async fn publish_room_alias_in_room_directory(
|
||||
&self,
|
||||
alias: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let new_alias = RoomAliasId::parse(alias)?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.publish_room_alias_in_room_directory(&new_alias)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Remove an existing room alias for this room in the room directory.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `true` if the room alias was present and it's now removed from the
|
||||
/// room directory.
|
||||
/// - `false` if the room alias didn't exist so it couldn't be removed.
|
||||
pub async fn remove_room_alias_from_room_directory(
|
||||
&self,
|
||||
alias: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let alias = RoomAliasId::parse(alias)?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.remove_room_alias_from_room_directory(&alias)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Enable End-to-end encryption in this room.
|
||||
pub async fn enable_encryption(&self) -> Result<(), ClientError> {
|
||||
self.inner.enable_encryption().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update room history visibility for this room.
|
||||
pub async fn update_history_visibility(
|
||||
&self,
|
||||
visibility: RoomHistoryVisibility,
|
||||
) -> Result<(), ClientError> {
|
||||
let visibility: RumaHistoryVisibility = visibility.try_into()?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_room_history_visibility(visibility)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the join rule for this room.
|
||||
pub async fn update_join_rules(&self, new_rule: JoinRule) -> Result<(), ClientError> {
|
||||
let new_rule: RumaJoinRule = new_rule.try_into()?;
|
||||
self.inner.privacy_settings().update_join_rule(new_rule).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the room's visibility in the room directory.
|
||||
pub async fn update_room_visibility(
|
||||
&self,
|
||||
visibility: RoomVisibility,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_room_visibility(visibility.into())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the visibility for this room in the room directory.
|
||||
///
|
||||
/// [Public](`RoomVisibility::Public`) rooms are listed in the room
|
||||
/// directory and can be found using it.
|
||||
pub async fn get_room_visibility(&self) -> Result<RoomVisibility, ClientError> {
|
||||
let visibility = self.inner.privacy_settings().get_room_visibility().await?;
|
||||
Ok(visibility.into())
|
||||
}
|
||||
|
||||
/// Start the current users live location share in the room.
|
||||
pub async fn start_live_location_share(&self, duration_millis: u64) -> Result<(), ClientError> {
|
||||
self.inner.start_live_location_share(duration_millis, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the current users live location share in the room.
|
||||
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
|
||||
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the current users live location beacon in the room.
|
||||
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_location_beacon(geo_uri)
|
||||
.await
|
||||
.expect("Unable to send live location beacon");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to live location shares in this room, using a `listener` to
|
||||
/// be notified of the changes.
|
||||
///
|
||||
/// The current live location shares will be emitted immediately when
|
||||
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
|
||||
pub fn subscribe_to_live_location_shares(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn LiveLocationShareListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let subscription = room.observe_live_location_shares();
|
||||
let mut stream = subscription.subscribe();
|
||||
let mut pinned_stream = pin!(stream);
|
||||
|
||||
while let Some(event) = pinned_stream.next().await {
|
||||
let last_location = LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: event.last_location.location.uri.clone().to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset: None,
|
||||
};
|
||||
|
||||
let Some(beacon_info) = event.beacon_info else {
|
||||
warn!("Live location share is missing the associated beacon_info state, skipping event.");
|
||||
continue;
|
||||
};
|
||||
|
||||
listener.call(vec![LiveLocationShare {
|
||||
last_location: LastLocation {
|
||||
location: last_location,
|
||||
ts: event.last_location.ts.0.into(),
|
||||
},
|
||||
is_live: beacon_info.is_live(),
|
||||
user_id: event.user_id.to_string(),
|
||||
}])
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Forget this room.
|
||||
///
|
||||
/// This communicates to the homeserver that it should forget the room.
|
||||
///
|
||||
/// Only left or banned-from rooms can be forgotten.
|
||||
pub async fn forget(&self) -> Result<(), ClientError> {
|
||||
self.inner.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: Sync + Send {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
|
||||
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
|
||||
Self {
|
||||
event_id: request.event_id.to_string(),
|
||||
user_id: request.member_info.user_id.to_string(),
|
||||
room_id: request.room_id().to_string(),
|
||||
display_name: request.member_info.display_name.clone(),
|
||||
avatar_url: request.member_info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
reason: request.member_info.reason.clone(),
|
||||
timestamp: request.timestamp.map(|ts| ts.into()),
|
||||
is_seen: request.is_seen,
|
||||
actions: Arc::new(KnockRequestActions { inner: request }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new requests to a join a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait KnockRequestsListener: Send + Sync {
|
||||
fn call(&self, join_requests: Vec<KnockRequest>);
|
||||
}
|
||||
|
||||
/// An FFI representation of a request to join a room.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct KnockRequest {
|
||||
/// The event id of the event that contains the `knock` membership change.
|
||||
pub event_id: String,
|
||||
/// The user id of the user who's requesting to join the room.
|
||||
pub user_id: String,
|
||||
/// The room id of the room whose access was requested.
|
||||
pub room_id: String,
|
||||
/// The optional display name of the user who's requesting to join the room.
|
||||
pub display_name: Option<String>,
|
||||
/// The optional avatar url of the user who's requesting to join the room.
|
||||
pub avatar_url: Option<String>,
|
||||
/// An optional reason why the user wants join the room.
|
||||
pub reason: Option<String>,
|
||||
/// The timestamp when this request was created.
|
||||
pub timestamp: Option<u64>,
|
||||
/// Whether the knock request has been marked as `seen` so it can be
|
||||
/// filtered by the client.
|
||||
pub is_seen: bool,
|
||||
/// A set of actions to perform for this knock request.
|
||||
pub actions: Arc<KnockRequestActions>,
|
||||
}
|
||||
|
||||
/// A set of actions to perform for a knock request.
|
||||
#[derive(Debug, Clone, uniffi::Object)]
|
||||
pub struct KnockRequestActions {
|
||||
inner: matrix_sdk::room::knock_requests::KnockRequest,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl KnockRequestActions {
|
||||
/// Accepts the knock request by inviting the user to the room.
|
||||
pub async fn accept(&self) -> Result<(), ClientError> {
|
||||
self.inner.accept().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Declines the knock request by kicking the user from the room with an
|
||||
/// optional reason.
|
||||
pub async fn decline(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.decline(reason.as_deref()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Declines the knock request by banning the user from the room with an
|
||||
/// optional reason.
|
||||
pub async fn decline_and_ban(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Marks the knock request as 'seen'.
|
||||
///
|
||||
/// **IMPORTANT**: this won't update the current reference to this request,
|
||||
/// a new one with the updated value should be emitted instead.
|
||||
pub async fn mark_as_seen(&self) -> Result<(), ClientError> {
|
||||
self.inner.mark_as_seen().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -888,16 +1188,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>,
|
||||
@@ -909,7 +1214,7 @@ impl RoomMembersIterator {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomMembersIterator {
|
||||
fn len(&self) -> u32 {
|
||||
self.chunk_iterator.len()
|
||||
@@ -918,7 +1223,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -957,7 +1262,7 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
|
||||
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
|
||||
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
|
||||
match media_source.as_ref() {
|
||||
match &media_source.as_ref().media_source {
|
||||
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
}
|
||||
@@ -1057,3 +1362,62 @@ impl TryFrom<ComposerDraftType> for SdkComposerDraftType {
|
||||
Ok(draft_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum RoomHistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes to
|
||||
/// something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they joined the room onwards.
|
||||
/// Events stop being accessible when the member's state changes to
|
||||
/// something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when the member
|
||||
/// was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be shared by
|
||||
/// any participating homeserver with anyone, regardless of whether they
|
||||
/// have ever joined the room.
|
||||
WorldReadable,
|
||||
|
||||
/// A custom visibility value.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl TryFrom<RumaHistoryVisibility> for RoomHistoryVisibility {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RumaHistoryVisibility) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaHistoryVisibility::Invited => Ok(RoomHistoryVisibility::Invited),
|
||||
RumaHistoryVisibility::Shared => Ok(RoomHistoryVisibility::Shared),
|
||||
RumaHistoryVisibility::WorldReadable => Ok(RoomHistoryVisibility::WorldReadable),
|
||||
RumaHistoryVisibility::Joined => Ok(RoomHistoryVisibility::Joined),
|
||||
RumaHistoryVisibility::_Custom(_) => {
|
||||
Ok(RoomHistoryVisibility::Custom { value: value.to_string() })
|
||||
}
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RoomHistoryVisibility> for RumaHistoryVisibility {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RoomHistoryVisibility) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RoomHistoryVisibility::Invited => Ok(RumaHistoryVisibility::Invited),
|
||||
RoomHistoryVisibility::Shared => Ok(RumaHistoryVisibility::Shared),
|
||||
RoomHistoryVisibility::Joined => Ok(RumaHistoryVisibility::Joined),
|
||||
RoomHistoryVisibility::WorldReadable => Ok(RumaHistoryVisibility::WorldReadable),
|
||||
RoomHistoryVisibility::Custom { .. } => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero},
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
@@ -54,12 +57,16 @@ pub struct RoomInfo {
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
/// The currently pinned event ids
|
||||
/// The currently pinned event ids.
|
||||
pinned_event_ids: Vec<String>,
|
||||
/// The join rule for this room, if known.
|
||||
join_rule: Option<JoinRule>,
|
||||
/// The history visibility for this room, if known.
|
||||
history_visibility: RoomHistoryVisibility,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result<Self> {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
let power_levels_map = room.users_with_power_levels().await;
|
||||
@@ -67,7 +74,13 @@ 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().iter().map(|id| id.to_string()).collect();
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
let join_rule = room.join_rule().try_into();
|
||||
if let Err(e) = &join_rule {
|
||||
warn!("Failed to parse join rule: {:?}", e);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
@@ -90,7 +103,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(),
|
||||
@@ -114,6 +130,8 @@ impl RoomInfo {
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule: join_rule.ok(),
|
||||
history_visibility: room.history_visibility_or_default().try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#![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::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
assign, RoomId,
|
||||
},
|
||||
sliding_sync::http,
|
||||
use matrix_sdk::ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
@@ -19,14 +18,17 @@ use matrix_sdk_ui::{
|
||||
timeline::default_event_filter,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use ruma::{OwnedRoomOrAliasId, OwnedServerName, ServerName};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
room::{Membership, Room},
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
timeline_event_filter::TimelineEventTypeFilter,
|
||||
utils::AsyncRuntimeDropped,
|
||||
TaskHandle, RUNTIME,
|
||||
};
|
||||
|
||||
@@ -51,7 +53,7 @@ pub enum RoomListError {
|
||||
#[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: Membership, actual: Membership },
|
||||
IncorrectRoomMembership { expected: Vec<Membership>, actual: Membership },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
@@ -85,7 +87,7 @@ pub struct 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();
|
||||
@@ -135,11 +137,7 @@ impl RoomListService {
|
||||
})))
|
||||
}
|
||||
|
||||
fn subscribe_to_rooms(
|
||||
&self,
|
||||
room_ids: Vec<String>,
|
||||
settings: Option<RoomSubscription>,
|
||||
) -> Result<(), RoomListError> {
|
||||
fn subscribe_to_rooms(&self, room_ids: Vec<String>) -> Result<(), RoomListError> {
|
||||
let room_ids = room_ids
|
||||
.into_iter()
|
||||
.map(|room_id| {
|
||||
@@ -147,10 +145,7 @@ impl RoomListService {
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner.subscribe_to_rooms(
|
||||
&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>(),
|
||||
settings.map(Into::into),
|
||||
);
|
||||
self.inner.subscribe_to_rooms(&room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -162,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,
|
||||
@@ -182,40 +177,12 @@ impl RoomList {
|
||||
})
|
||||
}
|
||||
|
||||
fn entries(&self, listener: Box<dyn RoomListEntriesListener>) -> Arc<TaskHandle> {
|
||||
let this = self.inner.clone();
|
||||
let utd_hook = self.room_list_service.utd_hook.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let (entries, entries_stream) = this.entries();
|
||||
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
listener.on_update(vec![RoomListEntriesUpdate::Append {
|
||||
values: entries
|
||||
.into_iter()
|
||||
.map(|room| Arc::new(RoomListItem::from(room, utd_hook.clone())))
|
||||
.collect(),
|
||||
}]);
|
||||
|
||||
while let Some(diffs) = entries_stream.next().await {
|
||||
listener.on_update(
|
||||
diffs
|
||||
.into_iter()
|
||||
.map(|diff| RoomListEntriesUpdate::from(diff, utd_hook.clone()))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
fn entries_with_dynamic_adapters(
|
||||
self: Arc<Self>,
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self.clone();
|
||||
let client = self.room_list_service.inner.client();
|
||||
let utd_hook = self.room_list_service.utd_hook.clone();
|
||||
|
||||
// The following code deserves a bit of explanation.
|
||||
@@ -263,10 +230,7 @@ impl RoomList {
|
||||
// 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(),
|
||||
client.room_info_notable_update_receiver(),
|
||||
);
|
||||
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.
|
||||
@@ -319,7 +283,7 @@ pub struct RoomListEntriesWithDynamicAdaptersResult {
|
||||
entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListEntriesWithDynamicAdaptersResult {
|
||||
fn controller(&self) -> Arc<RoomListDynamicEntriesController> {
|
||||
self.controller.clone()
|
||||
@@ -397,17 +361,17 @@ 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);
|
||||
}
|
||||
@@ -470,7 +434,7 @@ impl RoomListEntriesUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
#[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>);
|
||||
}
|
||||
@@ -488,7 +452,7 @@ impl RoomListDynamicEntriesController {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
|
||||
self.inner.set_filter(kind.into())
|
||||
@@ -576,7 +540,7 @@ impl RoomListItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomListItem {
|
||||
fn id(&self) -> String {
|
||||
self.inner.id().to_string()
|
||||
@@ -602,7 +566,7 @@ impl RoomListItem {
|
||||
}
|
||||
|
||||
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
Ok(RoomInfo::new(self.inner.inner_room()).await?)
|
||||
RoomInfo::new(self.inner.inner_room()).await
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
@@ -611,23 +575,71 @@ impl RoomListItem {
|
||||
}
|
||||
|
||||
/// Builds a `Room` FFI from an invited room without initializing its
|
||||
/// internal timeline
|
||||
/// internal timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than invited
|
||||
/// 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
|
||||
/// 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: Membership::Invited,
|
||||
expected: vec![Membership::Invited],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited or knocked rooms.
|
||||
///
|
||||
/// An error will be returned if the room is in a state other than invited
|
||||
/// or knocked.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
.into_iter()
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Validate internal room state.
|
||||
let membership = self.membership();
|
||||
if !matches!(membership, Membership::Invited | Membership::Knocked) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited, Membership::Knocked],
|
||||
actual: membership,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
|
||||
{
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = self.inner.id().to_owned().into();
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
|
||||
/// Build a full `Room` FFI object, filling its associated timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than joined
|
||||
@@ -635,7 +647,7 @@ impl RoomListItem {
|
||||
fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Joined) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: Membership::Joined,
|
||||
expected: vec![Membership::Joined],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
@@ -702,33 +714,8 @@ impl RoomListItem {
|
||||
self.inner.is_encrypted().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
|
||||
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[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 http::request::RoomSubscription {
|
||||
fn from(val: RoomSubscription) -> Self {
|
||||
assign!(http::request::RoomSubscription::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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,7 +725,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,49 +19,64 @@ 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())
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomMember {
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
@@ -72,20 +87,24 @@ pub struct RoomMember {
|
||||
pub normalized_power_level: i64,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,114 @@
|
||||
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, RoomHero},
|
||||
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,
|
||||
heroes: info
|
||||
.heroes
|
||||
.as_ref()
|
||||
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
/// 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())
|
||||
}
|
||||
|
||||
/// Forget the room if we had access to it, and it was left or banned.
|
||||
pub async fn forget(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
|
||||
let (own_member, sender_member) = match room.own_membership_details().await {
|
||||
Ok(memberships) => memberships,
|
||||
Err(error) => {
|
||||
warn!("Couldn't get membership info: {error}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(RoomMembershipDetails {
|
||||
own_room_member: own_member.try_into().ok()?,
|
||||
sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the current user's room member info and the optional room member
|
||||
/// info of the sender of the `m.room.member` event that this info represents.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomMembershipDetails {
|
||||
pub own_room_member: RoomMember,
|
||||
pub sender_room_member: Option<RoomMember>,
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
|
||||
Self { client, inner }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 +121,62 @@ 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,
|
||||
pub is_history_world_readable: Option<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>,
|
||||
/// Room heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+212
-105
@@ -15,9 +15,7 @@
|
||||
use std::{collections::BTreeSet, sync::Arc, time::Duration};
|
||||
|
||||
use extension_trait::extension_trait;
|
||||
use matrix_sdk::attachment::{
|
||||
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
|
||||
};
|
||||
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
@@ -42,7 +40,8 @@ use ruma::{
|
||||
VideoInfo as RumaVideoInfo,
|
||||
VideoMessageEventContent as RumaVideoMessageEventContent,
|
||||
},
|
||||
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
@@ -54,6 +53,7 @@ use tracing::info;
|
||||
use crate::{
|
||||
error::{ClientError, MediaInfoError},
|
||||
helpers::unwrap_or_clone_arc,
|
||||
timeline::MessageContent,
|
||||
utils::u64_to_uint,
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ impl From<AuthData> for ruma::api::client::uiaa::AuthData {
|
||||
|
||||
/// 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 {
|
||||
@@ -153,33 +153,28 @@ impl From<&RumaMatrixId> for MatrixId {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::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,
|
||||
@@ -189,7 +184,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,
|
||||
@@ -199,21 +194,84 @@ pub fn message_event_content_from_html_as_emote(
|
||||
)))
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub impl MediaSourceExt for MediaSource {
|
||||
fn from_json(json: String) -> Result<MediaSource, ClientError> {
|
||||
let res = serde_json::from_str(&json)?;
|
||||
Ok(res)
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct MediaSource {
|
||||
pub(crate) media_source: RumaMediaSource,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaSource {
|
||||
#[uniffi::constructor]
|
||||
pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
|
||||
let media_source = RumaMediaSource::Plain(url.into());
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).expect("Media source should always be serializable ")
|
||||
pub fn url(&self) -> String {
|
||||
self.media_source.url()
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
#[uniffi::constructor]
|
||||
pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
|
||||
let media_source: RumaMediaSource = serde_json::from_str(&json)?;
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(&self.media_source)
|
||||
.expect("Media source should always be serializable ")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaSource> for RumaMediaSource {
|
||||
fn from(value: MediaSource) -> Self {
|
||||
value.media_source
|
||||
}
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub(crate) impl MediaSourceExt for RumaMediaSource {
|
||||
fn verify(&self) -> Result<(), ClientError> {
|
||||
match self {
|
||||
RumaMediaSource::Plain(url) => {
|
||||
url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
RumaMediaSource::Encrypted(file) => {
|
||||
file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
match self {
|
||||
MediaSource::Plain(url) => url.to_string(),
|
||||
MediaSource::Encrypted(file) => file.url.to_string(),
|
||||
RumaMediaSource::Plain(url) => url.to_string(),
|
||||
RumaMediaSource::Encrypted(file) => file.url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,6 +285,7 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Mentions {
|
||||
pub user_ids: Vec<String>,
|
||||
pub room: bool,
|
||||
@@ -260,8 +319,25 @@ 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;
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
@@ -271,35 +347,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().into())
|
||||
.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().into())
|
||||
.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().into())
|
||||
.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().into())
|
||||
.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 } => {
|
||||
@@ -322,9 +402,11 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for MessageType {
|
||||
fn from(value: RumaMessageType) -> Self {
|
||||
match value {
|
||||
impl TryFrom<RumaMessageType> for MessageType {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
RumaMessageType::Emote(c) => MessageType::Emote {
|
||||
content: EmoteMessageContent {
|
||||
body: c.body.clone(),
|
||||
@@ -333,19 +415,20 @@ impl From<RumaMessageType> for MessageType {
|
||||
},
|
||||
RumaMessageType::Image(c) => MessageType::Image {
|
||||
content: ImageMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
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.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
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.try_into()?),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
voice: c.voice.map(Into::into),
|
||||
@@ -353,20 +436,20 @@ impl From<RumaMessageType> for MessageType {
|
||||
},
|
||||
RumaMessageType::Video(c) => MessageType::Video {
|
||||
content: VideoMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
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.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
content: FileMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
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.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::Notice(c) => MessageType::Notice {
|
||||
@@ -402,7 +485,7 @@ impl From<RumaMessageType> for MessageType {
|
||||
msgtype: value.msgtype().to_owned(),
|
||||
body: value.body().to_owned(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,18 +521,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>,
|
||||
@@ -458,18 +543,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>,
|
||||
}
|
||||
@@ -483,6 +570,7 @@ pub struct ImageInfo {
|
||||
pub thumbnail_info: Option<ThumbnailInfo>,
|
||||
pub thumbnail_source: Option<Arc<MediaSource>>,
|
||||
pub blurhash: Option<String>,
|
||||
pub is_animated: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<ImageInfo> for RumaImageInfo {
|
||||
@@ -493,8 +581,9 @@ impl From<ImageInfo> for RumaImageInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -516,6 +605,7 @@ impl TryFrom<&ImageInfo> for BaseImageInfo {
|
||||
width: Some(width),
|
||||
size: Some(size),
|
||||
blurhash: Some(blurhash),
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -598,7 +688,7 @@ impl From<VideoInfo> for RumaVideoInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
@@ -641,7 +731,7 @@ impl From<FileInfo> for RumaFileInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -676,21 +766,6 @@ impl From<ThumbnailInfo> for RumaThumbnailInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
|
||||
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct NoticeMessageContent {
|
||||
pub body: String,
|
||||
@@ -763,8 +838,10 @@ pub enum MessageFormat {
|
||||
Unknown { format: String },
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
|
||||
impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -772,15 +849,21 @@ impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
is_animated: info.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,8 +877,10 @@ impl From<&RumaAudioInfo> for AudioInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaVideoInfo> for VideoInfo {
|
||||
fn from(info: &RumaVideoInfo) -> Self {
|
||||
impl TryFrom<&RumaVideoInfo> for VideoInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -803,21 +888,28 @@ impl From<&RumaVideoInfo> for VideoInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
duration: info.duration,
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaFileInfo> for FileInfo {
|
||||
fn from(info: &RumaFileInfo) -> Self {
|
||||
impl TryFrom<&RumaFileInfo> for FileInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -825,12 +917,17 @@ impl From<&RumaFileInfo> for FileInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
}
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,3 +958,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,18 +1,19 @@
|
||||
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;
|
||||
use crate::{error::ClientError, utils::Timestamp};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SessionVerificationEmoji {
|
||||
@@ -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: Timestamp,
|
||||
}
|
||||
|
||||
#[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().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 {
|
||||
|
||||
@@ -38,6 +38,7 @@ pub enum SyncServiceState {
|
||||
Running,
|
||||
Terminated,
|
||||
Error,
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
@@ -47,11 +48,12 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
MatrixSyncServiceState::Running => Self::Running,
|
||||
MatrixSyncServiceState::Terminated => Self::Terminated,
|
||||
MatrixSyncServiceState::Error => Self::Error,
|
||||
MatrixSyncServiceState::Offline => Self::Offline,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 +64,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 {
|
||||
@@ -72,11 +74,11 @@ impl SyncService {
|
||||
}
|
||||
|
||||
pub async fn start(&self) {
|
||||
self.inner.start().await;
|
||||
self.inner.start().await
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.stop().await?)
|
||||
pub async fn stop(&self) {
|
||||
self.inner.stop().await
|
||||
}
|
||||
|
||||
pub fn state(&self, listener: Box<dyn SyncServiceStateObserver>) -> Arc<TaskHandle> {
|
||||
@@ -110,11 +112,18 @@ 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 })
|
||||
}
|
||||
|
||||
/// Enable the "offline" mode for the [`SyncService`].
|
||||
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_offline_mode();
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
@@ -153,7 +162,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);
|
||||
}
|
||||
@@ -201,6 +210,22 @@ pub struct UnableToDecryptInfo {
|
||||
/// What we know about what caused this UTD. E.g. was this event sent when
|
||||
/// we were not a member of this room?
|
||||
pub cause: UtdCause,
|
||||
|
||||
/// The difference between the event creation time (`origin_server_ts`) and
|
||||
/// the time our device was created. If negative, this event was sent
|
||||
/// *before* our device was created.
|
||||
pub event_local_age_millis: i64,
|
||||
|
||||
/// Whether the user had verified their own identity at the point they
|
||||
/// received the UTD event.
|
||||
pub user_trusts_own_identity: bool,
|
||||
|
||||
/// The homeserver of the user that sent the undecryptable event.
|
||||
pub sender_homeserver: String,
|
||||
|
||||
/// Our local user's own homeserver, or `None` if the client is not logged
|
||||
/// in.
|
||||
pub own_homeserver: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
@@ -209,6 +234,10 @@ impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
cause: value.cause,
|
||||
event_local_age_millis: value.event_local_age_millis,
|
||||
user_trusts_own_identity: value.user_trusts_own_identity,
|
||||
sender_homeserver: value.sender_homeserver.to_string(),
|
||||
own_homeserver: value.own_homeserver.map(String::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ impl TaskHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TaskHandle {
|
||||
// Cancel a task handle.
|
||||
pub fn cancel(&self) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use ruma::EventId;
|
||||
|
||||
use super::FocusEventError;
|
||||
use crate::{error::ClientError, event::RoomMessageEventMessageType};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineFocus {
|
||||
Live,
|
||||
Event { event_id: String, num_context_events: u16 },
|
||||
PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 },
|
||||
}
|
||||
|
||||
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(
|
||||
value: TimelineFocus,
|
||||
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
|
||||
match value {
|
||||
TimelineFocus::Live => Ok(Self::Live),
|
||||
TimelineFocus::Event { event_id, num_context_events } => {
|
||||
let parsed_event_id =
|
||||
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
|
||||
event_id: event_id.clone(),
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Self::Event { target: parsed_event_id, num_context_events })
|
||||
}
|
||||
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
|
||||
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes how date dividers get inserted, either in between each day or in
|
||||
/// between each month
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DateDividerMode {
|
||||
Daily,
|
||||
Monthly,
|
||||
}
|
||||
|
||||
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
fn from(value: DateDividerMode) -> Self {
|
||||
match value {
|
||||
DateDividerMode::Daily => Self::Daily,
|
||||
DateDividerMode::Monthly => Self::Monthly,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AllowedMessageTypes {
|
||||
All,
|
||||
Only { types: Vec<RoomMessageEventMessageType> },
|
||||
}
|
||||
|
||||
/// Various options used to configure the timeline's behavior.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `internal_id_prefix` -
|
||||
///
|
||||
/// * `allowed_message_types` -
|
||||
///
|
||||
/// * `date_divider_mode` -
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TimelineConfiguration {
|
||||
/// What should the timeline focus on?
|
||||
pub focus: TimelineFocus,
|
||||
|
||||
/// A list of [`RoomMessageEventMessageType`] that will be allowed to appear
|
||||
/// in the timeline
|
||||
pub allowed_message_types: AllowedMessageTypes,
|
||||
|
||||
/// An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
pub internal_id_prefix: Option<String>,
|
||||
|
||||
/// How often to insert date dividers
|
||||
pub date_divider_mode: DateDividerMode,
|
||||
}
|
||||
@@ -16,49 +16,81 @@ 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, RoomPinnedEventsChange, TimelineDetails};
|
||||
use ruma::events::room::{message::RoomMessageEventContentWithoutRelation, MediaSource};
|
||||
use tracing::warn;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::ruma::{ImageInfo, MessageType, PollKind};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
#[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,
|
||||
match value {
|
||||
Content::Message(message) => {
|
||||
let msgtype = message.msgtype().msgtype().to_owned();
|
||||
|
||||
Content::RedactedMessage => TimelineItemContentKind::RedactedMessage,
|
||||
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
TimelineItemContentKind::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
source: Arc::new(MediaSource::from(content.source.clone())),
|
||||
match TryInto::<MessageContent>::try_into(message) {
|
||||
Ok(message) => TimelineItemContent::Message { content: message },
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: msgtype,
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
Content::CallInvite => TimelineItemContentKind::CallInvite,
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
|
||||
Content::CallNotify => TimelineItemContentKind::CallNotify,
|
||||
let media_source = RumaMediaSource::from(content.source.clone());
|
||||
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
|
||||
if let Err(error) = media_source.verify() {
|
||||
return TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
match TryInto::<ImageInfo>::try_into(&content.info) {
|
||||
Ok(info) => TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info,
|
||||
source: Arc::new(MediaSource { media_source }),
|
||||
},
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership {
|
||||
user_id: membership.user_id().to_string(),
|
||||
user_display_name: membership.display_name(),
|
||||
change: membership.change().map(Into::into),
|
||||
},
|
||||
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::CallNotify => TimelineItemContent::CallNotify,
|
||||
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
|
||||
}
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
FullStateEventContent::Original { content, .. } => content.reason.clone(),
|
||||
_ => 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
|
||||
@@ -74,7 +106,7 @@ impl TimelineItemContent {
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
TimelineItemContentKind::ProfileChange {
|
||||
TimelineItemContent::ProfileChange {
|
||||
display_name: display_name.flatten(),
|
||||
prev_display_name: prev_display_name.flatten(),
|
||||
avatar_url: avatar_url.flatten(),
|
||||
@@ -82,37 +114,68 @@ impl TimelineItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
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 TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
msg_type: value.msgtype().clone().try_into()?,
|
||||
body: value.body().to_owned(),
|
||||
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
|
||||
is_edited: value.is_edited(),
|
||||
thread_root: value.thread_root().map(|id| id.to_string()),
|
||||
mentions: value.mentions().cloned().map(|m| m.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -125,7 +188,7 @@ pub enum TimelineItemContentKind {
|
||||
max_selections: u64,
|
||||
answers: Vec<PollAnswer>,
|
||||
votes: HashMap<String, Vec<String>>,
|
||||
end_time: Option<u64>,
|
||||
end_time: Option<Timestamp>,
|
||||
has_been_edited: bool,
|
||||
},
|
||||
CallInvite,
|
||||
@@ -137,6 +200,7 @@ pub enum TimelineItemContentKind {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
change: Option<MembershipChange>,
|
||||
reason: Option<String>,
|
||||
},
|
||||
ProfileChange {
|
||||
display_name: Option<String>,
|
||||
@@ -160,36 +224,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,
|
||||
@@ -201,14 +235,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(),
|
||||
},
|
||||
@@ -221,11 +266,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 },
|
||||
}
|
||||
|
||||
@@ -275,7 +320,7 @@ pub struct Reaction {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ReactionSenderData {
|
||||
pub sender_id: String,
|
||||
pub timestamp: u64,
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
@@ -419,15 +464,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,
|
||||
@@ -437,7 +482,7 @@ impl From<PollResult> for TimelineItemContentKind {
|
||||
.map(|i| PollAnswer { id: i.id, text: i.text })
|
||||
.collect(),
|
||||
votes: value.votes,
|
||||
end_time: value.end_time,
|
||||
end_time: value.end_time.map(|t| t.into()),
|
||||
has_been_edited: value.has_been_edited,
|
||||
}
|
||||
}
|
||||
|
||||
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).
|
||||
///
|
||||
@@ -185,6 +185,16 @@ impl LogLevel {
|
||||
LogLevel::Trace => tracing::Level::TRACE,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
||||
@@ -12,12 +12,68 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::UInt;
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timestamp(u64);
|
||||
|
||||
impl From<MilliSecondsSinceUnixEpoch> for Timestamp {
|
||||
fn from(date: MilliSecondsSinceUnixEpoch) -> Self {
|
||||
Self(date.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
uniffi::custom_newtype!(Timestamp, u64);
|
||||
|
||||
pub(crate) fn u64_to_uint(u: u64) -> UInt {
|
||||
UInt::new(u).unwrap_or_else(|| {
|
||||
warn!("u64 -> UInt conversion overflowed, falling back to UInt::MAX");
|
||||
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,13 +262,38 @@ 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]
|
||||
#[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![
|
||||
// To compute the current state of the matrixRTC session.
|
||||
@@ -278,19 +304,13 @@ pub fn get_element_call_required_permissions(
|
||||
WidgetEventFilter::StateWithType {
|
||||
event_type: StateEventType::RoomEncryption.to_string(),
|
||||
},
|
||||
// To read rageshake requests from other room members
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
// To read encryption keys
|
||||
// TODO change this to the appropriate to-device version once ready
|
||||
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![
|
||||
// To send the call participation state event (main MatrixRTC event).
|
||||
// This is required for legacy state events (using only one event for all devices with
|
||||
@@ -313,15 +333,10 @@ pub fn get_element_call_required_permissions(
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("_{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// To request other room members to send rageshakes
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.rageshake_request".to_owned(),
|
||||
},
|
||||
// To send this user's encryption keys
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "io.element.call.encryption_keys".to_owned(),
|
||||
},
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.chain(read_send)
|
||||
.collect(),
|
||||
requires_client: true,
|
||||
update_delayed_event: true,
|
||||
send_delayed_event: true,
|
||||
@@ -354,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.
|
||||
///
|
||||
@@ -417,7 +432,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
|
||||
}
|
||||
|
||||
/// 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 },
|
||||
@@ -469,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;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,134 @@
|
||||
# unreleased
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.10.0] - 2025-02-04
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] `EventCacheStore` allows to control which media content is
|
||||
allowed in the media cache, and how long it should be kept, with a
|
||||
`MediaRetentionPolicy`:
|
||||
- `EventCacheStore::add_media_content()` has an extra argument,
|
||||
`ignore_policy`, which decides whether a media content should ignore the
|
||||
`MediaRetentionPolicy`. It should be stored alongside the media content.
|
||||
- `EventCacheStore` has four new methods: `media_retention_policy()`,
|
||||
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
|
||||
`clean_up_media_cache()`.
|
||||
- `EventCacheStore` implementations should delegate media cache methods to the
|
||||
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
|
||||
They need to implement the `EventCacheStoreMedia` trait that can be tested
|
||||
with the `event_cache_store_media_integration_tests!` macro.
|
||||
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Replaced `Room::compute_display_name` with the reintroduced
|
||||
`Room::display_name()`. The new method computes a display name, or return a
|
||||
cached value from the previous successful computation. If you need a sync
|
||||
variant, consider using `Room::cached_display_name()`.
|
||||
([#4470](https://github.com/matrix-org/matrix-rust-sdk/pull/4470))
|
||||
- [**breaking**]: The reexported types `SyncTimelineEvent` and `TimelineEvent`
|
||||
have been fused into a single type `TimelineEvent`, and its field
|
||||
`push_actions` has been made `Option`al (it is set to `None` when we couldn't
|
||||
compute the push actions, because we lacked some information).
|
||||
([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568))
|
||||
|
||||
## [0.9.0] - 2024-12-18
|
||||
|
||||
### Features
|
||||
|
||||
- Introduced support for
|
||||
[MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling
|
||||
the designation of certain users as service members. These flagged users are
|
||||
excluded from the room display name calculation.
|
||||
([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is
|
||||
called. Previously, items following the removed item were not shifted left by
|
||||
one position, leaving them at incorrect indices.
|
||||
([#4346](https://github.com/matrix-org/matrix-rust-sdk/pull/4346))
|
||||
|
||||
## [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
|
||||
- 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
|
||||
|
||||
- `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`.
|
||||
|
||||
- `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.
|
||||
- `Media::get_thumbnail` and `MediaFormat::Thumbnail` allow to request an animated thumbnail
|
||||
- They both take a `MediaThumbnailSettings` instead of `MediaThumbnailSize`.
|
||||
- The `StateStore` methods to access data in the media cache where moved to a separate
|
||||
`EventCacheStore` trait.
|
||||
- The `instant` module was removed, use the `ruma::time` module instead.
|
||||
|
||||
- [**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.10.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -21,16 +21,15 @@ 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"]
|
||||
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 = []
|
||||
test-send-sync = [
|
||||
"matrix-sdk-common/test-send-sync",
|
||||
"matrix-sdk-crypto?/test-send-sync",
|
||||
]
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
@@ -49,8 +48,9 @@ as_variant = { workspace = true }
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.4.0", features = ["serde"] }
|
||||
eyeball = { workspace = true }
|
||||
bitflags = { version = "2.8.0", features = ["serde"] }
|
||||
decancer = "3.2.8"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
growable-bloom-filter = { workspace = true }
|
||||
@@ -60,7 +60,16 @@ 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", "rand"] }
|
||||
regex = "1.11.1"
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc3575",
|
||||
"unstable-msc4186",
|
||||
"rand",
|
||||
] }
|
||||
unicode-normalization = { workspace = true }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -76,12 +85,13 @@ futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
similar-asserts = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,14 +17,17 @@
|
||||
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.
|
||||
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEventNoId));
|
||||
@@ -38,7 +41,7 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
pub struct DebugInvitedRoom<'a>(pub &'a InvitedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
impl fmt::Debug for DebugInvitedRoom<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InvitedRoom")
|
||||
.field("invite_state", &DebugListOfRawEvents(&self.0.invite_state.events))
|
||||
@@ -46,10 +49,24 @@ 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 fmt::Debug for DebugKnockedRoom<'_> {
|
||||
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))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEvents<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEvents<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEvent));
|
||||
|
||||
@@ -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::{
|
||||
@@ -28,9 +30,10 @@ use ruma::{
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, 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: &'static 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: &'static 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: &'static 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,29 @@ 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()),
|
||||
)
|
||||
}
|
||||
|
||||
/// The optional reason why the membership changed.
|
||||
pub fn reason(&self) -> Option<&str> {
|
||||
match self {
|
||||
MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
|
||||
MemberEvent::Stripped(e) => e.content.reason.as_deref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The optional timestamp for this member event.
|
||||
pub fn timestamp(&self) -> Option<UInt> {
|
||||
match self {
|
||||
MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,3 +504,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_cyrillic() {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use matrix_sdk_common::store_locks::LockStoreError;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::event_cache::store::EventCacheStoreError;
|
||||
|
||||
/// Result type of the rust-sdk.
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
@@ -42,6 +45,14 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
StateStore(#[from] crate::store::StoreError),
|
||||
|
||||
/// An error happened while manipulating the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheStore(#[from] EventCacheStoreError),
|
||||
|
||||
/// An error happened while attempting to lock the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheLock(#[from] LockStoreError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
@@ -61,4 +72,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::TimelineEvent;
|
||||
|
||||
pub mod store;
|
||||
|
||||
/// The kind of event the event storage holds.
|
||||
pub type Event = TimelineEvent;
|
||||
|
||||
/// 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,665 @@
|
||||
// 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 assert_matches::assert_matches;
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{
|
||||
ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk,
|
||||
Update,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
|
||||
push::Action, room_id, uint, RoomId,
|
||||
};
|
||||
|
||||
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
|
||||
/// Create a test event with all data filled, for testing that linked chunk
|
||||
/// correctly stores event data.
|
||||
///
|
||||
/// Keep in sync with [`check_test_event`].
|
||||
pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
|
||||
let encryption_info = EncryptionInfo {
|
||||
sender: (*ALICE).into(),
|
||||
sender_device: None,
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "1337".to_owned(),
|
||||
sender_claimed_keys: Default::default(),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
};
|
||||
|
||||
let event = EventFactory::new()
|
||||
.text_msg(content)
|
||||
.room(room_id)
|
||||
.sender(*ALICE)
|
||||
.into_raw_timeline()
|
||||
.cast();
|
||||
|
||||
TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event,
|
||||
encryption_info,
|
||||
unsigned_encryption_info: None,
|
||||
}),
|
||||
push_actions: Some(vec![Action::Notify]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that an event created with [`make_test_event`] contains the expected
|
||||
/// data.
|
||||
///
|
||||
/// Keep in sync with [`make_test_event`].
|
||||
#[track_caller]
|
||||
pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
// Check push actions.
|
||||
let actions = event.push_actions.as_ref().unwrap();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_matches!(&actions[0], Action::Notify);
|
||||
|
||||
// Check content.
|
||||
assert_matches!(&event.kind, TimelineEventKind::Decrypted(d) => {
|
||||
// Check encryption fields.
|
||||
assert_eq!(d.encryption_info.sender, *ALICE);
|
||||
assert_matches!(&d.encryption_info.algorithm_info, AlgorithmInfo::MegolmV1AesSha2 { curve25519_key, .. } => {
|
||||
assert_eq!(curve25519_key, "1337");
|
||||
});
|
||||
|
||||
// Check event.
|
||||
let deserialized = d.event.deserialize().unwrap();
|
||||
assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
|
||||
assert_eq!(msg.as_original().unwrap().content.body(), text);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// `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);
|
||||
|
||||
/// Test handling updates to a linked chunk and reloading these updates from
|
||||
/// the store.
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
|
||||
|
||||
/// Test that rebuilding a linked chunk from an empty store doesn't return
|
||||
/// anything.
|
||||
async fn test_rebuild_empty_linked_chunk(&self);
|
||||
|
||||
/// Test that clear all the rooms' linked chunks works.
|
||||
async fn test_clear_all_rooms_chunks(&self);
|
||||
|
||||
/// Test that removing a room from storage empties all associated data.
|
||||
async fn test_remove_room(&self);
|
||||
}
|
||||
|
||||
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
|
||||
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
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(), IgnoreMediaRetentionPolicy::No)
|
||||
.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"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found by URI 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"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
|
||||
"media still found by URI after removing"
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.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(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.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"
|
||||
);
|
||||
|
||||
// We get a file with the URI, we don't know which one.
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_some(),
|
||||
"media not found by URI though two where added"
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(
|
||||
&request_other_file,
|
||||
other_content.clone(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.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"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found by URI"
|
||||
);
|
||||
|
||||
// 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"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
|
||||
"media found by URI wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(other_uri).await.unwrap().is_some(),
|
||||
"other media found by URI 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(), IgnoreMediaRetentionPolicy::No)
|
||||
.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");
|
||||
}
|
||||
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![
|
||||
make_test_event(room_id, "hello"),
|
||||
make_test_event(room_id, "world"),
|
||||
],
|
||||
},
|
||||
// a gap chunk
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "parmesan".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![make_test_event(room_id, "sup")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
let raws = self.reload_linked_chunk(room_id).await.unwrap();
|
||||
let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
|
||||
|
||||
let mut chunks = lc.chunks();
|
||||
|
||||
{
|
||||
let first = chunks.next().unwrap();
|
||||
// Note: we can't assert the previous/next chunks, as these fields and their
|
||||
// getters are private.
|
||||
assert_eq!(first.identifier(), CId::new(0));
|
||||
|
||||
assert_matches!(first.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "hello");
|
||||
check_test_event(&events[1], "world");
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let second = chunks.next().unwrap();
|
||||
assert_eq!(second.identifier(), CId::new(1));
|
||||
|
||||
assert_matches!(second.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "parmesan");
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let third = chunks.next().unwrap();
|
||||
assert_eq!(third.identifier(), CId::new(2));
|
||||
|
||||
assert_matches!(third.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
check_test_event(&events[0], "sup");
|
||||
});
|
||||
}
|
||||
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
async fn test_rebuild_empty_linked_chunk(&self) {
|
||||
// When I rebuild a linked chunk from an empty store, it's empty.
|
||||
let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
|
||||
assert!(rebuild_linked_chunk(raw_parts).is_none());
|
||||
}
|
||||
|
||||
async fn test_clear_all_rooms_chunks(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
|
||||
// Add updates for the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add updates for the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
vec![
|
||||
// Empty items chunk.
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// a gap chunk
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![make_test_event(r0, "yummy")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some());
|
||||
|
||||
// Clear the chunks.
|
||||
self.clear_all_rooms_chunks().await.unwrap();
|
||||
|
||||
// Both rooms now have no linked chunk.
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
|
||||
}
|
||||
|
||||
async fn test_remove_room(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
|
||||
// Add updates to the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![make_test_event(r0, "hello"), make_test_event(r0, "world")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add updates to the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
vec![
|
||||
// new chunk
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![make_test_event(r0, "yummy")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to remove content from r0.
|
||||
self.remove_room(r0).await.unwrap();
|
||||
|
||||
// Check that r0 doesn't have a linked chunk anymore.
|
||||
let r0_linked_chunk = self.reload_linked_chunk(r0).await.unwrap();
|
||||
assert!(r0_linked_chunk.is_empty());
|
||||
|
||||
// Check that r1 is unaffected.
|
||||
let r1_linked_chunk = self.reload_linked_chunk(r1).await.unwrap();
|
||||
assert!(!r1_linked_chunk.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_rebuild_empty_linked_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_rebuild_empty_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_clear_all_rooms_chunks() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_clear_all_rooms_chunks().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_remove_room() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_remove_room().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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,330 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! Configuration to decide whether or not to keep media in the cache, allowing
|
||||
//! to do periodic cleanups to avoid to have the size of the media cache grow
|
||||
//! indefinitely.
|
||||
//!
|
||||
//! To proceed to a cleanup, first set the [`MediaRetentionPolicy`] to use with
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]. Then call
|
||||
//! [`EventCacheStore::clean_up_media_cache()`].
|
||||
//!
|
||||
//! In the future, other settings will allow to run automatic periodic cleanup
|
||||
//! jobs.
|
||||
//!
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]: crate::event_cache::store::EventCacheStore::set_media_retention_policy
|
||||
//! [`EventCacheStore::clean_up_media_cache()`]: crate::event_cache::store::EventCacheStore::clean_up_media_cache
|
||||
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The retention policy for media content used by the [`EventCacheStore`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct MediaRetentionPolicy {
|
||||
/// The maximum authorized size of the overall media cache, in bytes.
|
||||
///
|
||||
/// The cache size is defined as the sum of the sizes of all the (possibly
|
||||
/// encrypted) media contents in the cache, excluding any metadata
|
||||
/// associated with them.
|
||||
///
|
||||
/// If this is set and the cache size is bigger than this value, the oldest
|
||||
/// media contents in the cache will be removed during a cleanup until the
|
||||
/// cache size is below this threshold.
|
||||
///
|
||||
/// Note that it is possible for the cache size to temporarily exceed this
|
||||
/// value between two cleanups.
|
||||
///
|
||||
/// Defaults to 400 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_cache_size: Option<usize>,
|
||||
|
||||
/// The maximum authorized size of a single media content, in bytes.
|
||||
///
|
||||
/// The size of a media content is the size taken by the content in the
|
||||
/// database, after it was possibly encrypted, so it might differ from the
|
||||
/// initial size of the content.
|
||||
///
|
||||
/// The maximum authorized size of a single media content is actually the
|
||||
/// lowest value between `max_cache_size` and `max_file_size`.
|
||||
///
|
||||
/// If it is set, media content bigger than the maximum size will not be
|
||||
/// cached. If the maximum size changed after media content that exceeds the
|
||||
/// new value was cached, the corresponding content will be removed
|
||||
/// during a cleanup.
|
||||
///
|
||||
/// Defaults to 20 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<usize>,
|
||||
|
||||
/// The duration after which unaccessed media content is considered
|
||||
/// expired.
|
||||
///
|
||||
/// If this is set, media content whose last access is older than this
|
||||
/// duration will be removed from the media cache during a cleanup.
|
||||
///
|
||||
/// Defaults to 60 days.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_access_expiry: Option<Duration>,
|
||||
}
|
||||
|
||||
impl MediaRetentionPolicy {
|
||||
/// Create a [`MediaRetentionPolicy`] with the default values.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create an empty [`MediaRetentionPolicy`].
|
||||
///
|
||||
/// This means that all media will be cached and cleanups have no effect.
|
||||
pub fn empty() -> Self {
|
||||
Self { max_cache_size: None, max_file_size: None, last_access_expiry: None }
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of the overall media cache, in bytes.
|
||||
pub fn with_max_cache_size(mut self, size: Option<usize>) -> Self {
|
||||
self.max_cache_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of a single media content, in bytes.
|
||||
pub fn with_max_file_size(mut self, size: Option<usize>) -> Self {
|
||||
self.max_file_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the duration before which unaccessed media content is considered
|
||||
/// expired.
|
||||
pub fn with_last_access_expiry(mut self, duration: Option<Duration>) -> Self {
|
||||
self.last_access_expiry = duration;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether this policy has limitations.
|
||||
///
|
||||
/// If this policy has no limitations, a cleanup job would have no effect.
|
||||
///
|
||||
/// Returns `true` if at least one limitation is set.
|
||||
pub fn has_limitations(&self) -> bool {
|
||||
self.max_cache_size.is_some()
|
||||
|| self.max_file_size.is_some()
|
||||
|| self.last_access_expiry.is_some()
|
||||
}
|
||||
|
||||
/// Whether the given size exceeds the maximum authorized size of the media
|
||||
/// cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The overall size of the media cache to check, in bytes.
|
||||
pub fn exceeds_max_cache_size(&self, size: usize) -> bool {
|
||||
self.max_cache_size.is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
/// The computed maximum authorized size of a single media content, in
|
||||
/// bytes.
|
||||
///
|
||||
/// This is the lowest value between `max_cache_size` and `max_file_size`.
|
||||
pub fn computed_max_file_size(&self) -> Option<usize> {
|
||||
match (self.max_cache_size, self.max_file_size) {
|
||||
(None, None) => None,
|
||||
(None, Some(size)) => Some(size),
|
||||
(Some(size), None) => Some(size),
|
||||
(Some(max_cache_size), Some(max_file_size)) => Some(max_cache_size.min(max_file_size)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the given size, in bytes, exceeds the computed maximum
|
||||
/// authorized size of a single media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The size of the media content to check, in bytes.
|
||||
pub fn exceeds_max_file_size(&self, size: usize) -> bool {
|
||||
self.computed_max_file_size().is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
/// Whether a content whose last access was at the given time has expired.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `current_time` - The current time.
|
||||
///
|
||||
/// * `last_access_time` - The time when the media content to check was last
|
||||
/// accessed.
|
||||
pub fn has_content_expired(
|
||||
&self,
|
||||
current_time: SystemTime,
|
||||
last_access_time: SystemTime,
|
||||
) -> bool {
|
||||
self.last_access_expiry.is_some_and(|max_duration| {
|
||||
current_time
|
||||
.duration_since(last_access_time)
|
||||
// If this returns an error, the last access time is newer than the current time.
|
||||
// This shouldn't happen but in this case the content cannot be expired.
|
||||
.is_ok_and(|elapsed| elapsed >= max_duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaRetentionPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// 400 MiB.
|
||||
max_cache_size: Some(400 * 1024 * 1024),
|
||||
// 20 MiB.
|
||||
max_file_size: Some(20 * 1024 * 1024),
|
||||
// 60 days.
|
||||
last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_has_limitations() {
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_last_access_expiry(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_cache_size(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_file_size(Some(1_024));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_file_size(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
// With default values.
|
||||
assert!(MediaRetentionPolicy::new().has_limitations());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_max_cache_size() {
|
||||
let file_size = 2_048;
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), None);
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(4_096));
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(4_096));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(2_048));
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert!(policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_max_file_size() {
|
||||
let file_size = 2_048;
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert_eq!(policy.computed_max_file_size(), None);
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
// With max_file_size only.
|
||||
policy = policy.with_max_file_size(Some(4_096));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(4_096));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(1_024));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
|
||||
// With max_cache_size as well.
|
||||
policy = policy.with_max_cache_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(4_096));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_has_content_expired() {
|
||||
let epoch = SystemTime::UNIX_EPOCH;
|
||||
let last_access_time = epoch + Duration::from_secs(30);
|
||||
let epoch_plus_60 = epoch + Duration::from_secs(60);
|
||||
let epoch_plus_120 = epoch + Duration::from_secs(120);
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(120)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(30)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(0)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// 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;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{locks::Mutex, AsyncTraitDeps};
|
||||
use ruma::{time::SystemTime, MxcUri};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
|
||||
|
||||
/// API for implementors of [`EventCacheStore`] to manage their media through
|
||||
/// their implementation of [`EventCacheStoreMedia`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug)]
|
||||
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
/// The time provider.
|
||||
time_provider: Time,
|
||||
|
||||
/// The current [`MediaRetentionPolicy`].
|
||||
policy: Mutex<MediaRetentionPolicy>,
|
||||
|
||||
/// A mutex to ensure a single cleanup is running at a time.
|
||||
cleanup_guard: AsyncMutex<()>,
|
||||
}
|
||||
|
||||
impl MediaService {
|
||||
/// Construct a new default `MediaService`.
|
||||
///
|
||||
/// [`MediaService::restore()`] should be called after constructing the
|
||||
/// `MediaService` to restore its previous state.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaService {
|
||||
fn default() -> Self {
|
||||
Self::with_time_provider(DefaultTimeProvider)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Time> MediaService<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
{
|
||||
/// Construct a new `MediaService` with the given `TimeProvider` and an
|
||||
/// empty `MediaRetentionPolicy`.
|
||||
fn with_time_provider(time_provider: Time) -> Self {
|
||||
Self {
|
||||
time_provider,
|
||||
policy: Mutex::new(MediaRetentionPolicy::empty()),
|
||||
cleanup_guard: AsyncMutex::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore the previous state of the [`MediaRetentionPolicy`] from data
|
||||
/// that was persisted in the store.
|
||||
///
|
||||
/// This should be called immediately after constructing the `MediaService`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` that was persisted in the store.
|
||||
pub fn restore(&self, policy: Option<MediaRetentionPolicy>) {
|
||||
if let Some(policy) = policy {
|
||||
*self.policy.lock() = policy;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `MediaRetentionPolicy` of this service.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
store.set_media_retention_policy_inner(policy).await?;
|
||||
|
||||
*self.policy.lock() = policy;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the `MediaRetentionPolicy` of this service.
|
||||
pub fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
*self.policy.lock()
|
||||
}
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
let policy = self.media_retention_policy();
|
||||
|
||||
if ignore_policy == IgnoreMediaRetentionPolicy::No
|
||||
&& policy.exceeds_max_file_size(content.len())
|
||||
{
|
||||
// We do not cache the content.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store
|
||||
.add_media_content_inner(
|
||||
request,
|
||||
content,
|
||||
self.time_provider.now(),
|
||||
policy,
|
||||
ignore_policy,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn set_ignore_media_retention_policy<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
store.set_ignore_media_retention_policy_inner(request, ignore_policy).await
|
||||
}
|
||||
|
||||
/// Get a media file's content out of the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_inner(request, self.time_provider.now()).await
|
||||
}
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_for_uri_inner(uri, self.time_provider.now()).await
|
||||
}
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
///
|
||||
/// If there is already an ongoing cleanup, this is a noop.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
pub async fn clean_up_media_cache<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
) -> Result<(), Store::Error> {
|
||||
let Ok(_guard) = self.cleanup_guard.try_lock() else {
|
||||
// There is another ongoing cleanup.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let policy = self.media_retention_policy();
|
||||
|
||||
if !policy.has_limitations() {
|
||||
// No need to call the backend.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store.clean_up_media_cache_inner(policy, self.time_provider.now()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the media cache of the SDK.
|
||||
///
|
||||
/// The main purposes of this trait are to be able to centralize where we handle
|
||||
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
|
||||
/// simplify the implementation of tests by being able to have complete control
|
||||
/// over the `SystemTime`s provided to the store.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + Into<EventCacheStoreError>;
|
||||
|
||||
/// The persisted media retention policy in the media cache.
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
|
||||
|
||||
/// Persist the media retention policy in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to persist.
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to set the last access time of the
|
||||
/// media.
|
||||
///
|
||||
/// * `policy` - The media retention policy, to check whether the media is
|
||||
/// too big to be cached.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
|
||||
/// for this media. This setting should be persisted alongside the media
|
||||
/// and taken into account whenever the policy is used.
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
current_time: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// If the media of the given request is not found, this should be a noop.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the given policy.
|
||||
///
|
||||
/// For the integration tests, it is expected that content that does not
|
||||
/// pass the last access expiry and max file size criteria will be
|
||||
/// removed first. After that, the remaining cache size should be
|
||||
/// computed to compare against the max cache size criteria.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The media retention policy to use for the cleanup. The
|
||||
/// `cleanup_frequency` will be ignored.
|
||||
///
|
||||
/// * `current_time` - The current time, to be used to check for expired
|
||||
/// content.
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
|
||||
/// content.
|
||||
///
|
||||
/// Some media cache actions are noops when the media content that is processed
|
||||
/// is filtered out by the policy. This can break some features of the SDK, like
|
||||
/// the send queue, that expects to be able to persist all media files in the
|
||||
/// store to restore them when the client is restored.
|
||||
///
|
||||
/// This can be converted to a boolean with
|
||||
/// [`IgnoreMediaRetentionPolicy::is_yes()`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IgnoreMediaRetentionPolicy {
|
||||
/// The media retention policy will be ignored and the current action will
|
||||
/// not be a noop.
|
||||
///
|
||||
/// Any media content in this state must NOT be used when applying a
|
||||
/// `MediaRetentionPolicy`. This applies to ANY criteria, like the maximum
|
||||
/// file size, the maximum cache size or the last access expiry.
|
||||
///
|
||||
/// This state is supposed to be transient, and to only be used internally
|
||||
/// by the SDK.
|
||||
Yes,
|
||||
|
||||
/// The media retention policy will be respected and the current action
|
||||
/// might be a noop.
|
||||
No,
|
||||
}
|
||||
|
||||
impl IgnoreMediaRetentionPolicy {
|
||||
/// Whether this is an [`IgnoreMediaRetentionPolicy::Yes`] variant.
|
||||
pub fn is_yes(self) -> bool {
|
||||
matches!(self, Self::Yes)
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract trait to provide the current `SystemTime` for the
|
||||
/// [`MediaService`].
|
||||
pub trait TimeProvider {
|
||||
/// The current time.
|
||||
fn now(&self) -> SystemTime;
|
||||
}
|
||||
|
||||
/// The default time provider, that calls `ruma::time::SystemTime::now()`.
|
||||
#[derive(Debug)]
|
||||
pub struct DefaultTimeProvider;
|
||||
|
||||
impl TimeProvider for DefaultTimeProvider {
|
||||
fn now(&self) -> SystemTime {
|
||||
SystemTime::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fmt, sync::MutexGuard};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
events::room::MediaSource,
|
||||
mxc_uri,
|
||||
time::{Duration, SystemTime},
|
||||
MxcUri, OwnedMxcUri,
|
||||
};
|
||||
|
||||
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
|
||||
use crate::{
|
||||
event_cache::store::{media::MediaRetentionPolicy, EventCacheStoreError},
|
||||
media::{MediaFormat, MediaRequestParameters, UniqueKey},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockEventCacheStoreMedia {
|
||||
inner: Mutex<MockEventCacheStoreMediaInner>,
|
||||
}
|
||||
|
||||
impl MockEventCacheStoreMedia {
|
||||
/// Whether the store was accessed.
|
||||
fn accessed(&self) -> bool {
|
||||
self.inner.lock().accessed
|
||||
}
|
||||
|
||||
/// Reset the `accessed` boolean.
|
||||
fn reset_accessed(&self) {
|
||||
self.inner.lock().accessed = false;
|
||||
}
|
||||
|
||||
/// Access the inner store.
|
||||
///
|
||||
/// Should be called for every access to the inner store as it also sets
|
||||
/// the `accessed` boolean.
|
||||
fn inner(&self) -> MutexGuard<'_, MockEventCacheStoreMediaInner> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.accessed = true;
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockEventCacheStoreMediaInner {
|
||||
/// Whether this store was accessed.
|
||||
///
|
||||
/// Must be set to `true` for any operation that unlocks the store.
|
||||
accessed: bool,
|
||||
|
||||
/// The persisted media retention policy.
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
|
||||
/// The list of media content.
|
||||
media_list: Vec<MediaContent>,
|
||||
|
||||
/// The time of the last cleanup.
|
||||
cleanup_time: Option<SystemTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MediaContent {
|
||||
/// The unique key for the media content.
|
||||
key: String,
|
||||
|
||||
/// The original URI of the media content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The media content.
|
||||
content: Vec<u8>,
|
||||
|
||||
/// Whether the `MediaRetentionPolicy` should be ignored for this media
|
||||
/// content;
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the media content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockEventCacheStoreMediaError;
|
||||
|
||||
impl fmt::Display for MockEventCacheStoreMediaError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "MockEventCacheStoreMediaError")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MockEventCacheStoreMediaError {}
|
||||
|
||||
impl From<MockEventCacheStoreMediaError> for EventCacheStoreError {
|
||||
fn from(value: MockEventCacheStoreMediaError) -> Self {
|
||||
Self::backend(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
|
||||
type Error = MockEventCacheStoreMediaError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
|
||||
Ok(self.inner().media_retention_policy)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner().media_retention_policy = Some(policy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
current_time: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(content.len()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inner = self.inner();
|
||||
let key = request.unique_key();
|
||||
|
||||
if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
|
||||
let media_content = &mut inner.media_list[pos];
|
||||
media_content.content = content;
|
||||
media_content.last_access = current_time;
|
||||
media_content.ignore_policy = ignore_policy;
|
||||
} else {
|
||||
inner.media_list.push(MediaContent {
|
||||
key,
|
||||
uri: request.uri().to_owned(),
|
||||
content,
|
||||
ignore_policy,
|
||||
last_access: current_time,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let key = request.unique_key();
|
||||
let mut inner = self.inner();
|
||||
|
||||
if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
|
||||
inner.media_list[pos].ignore_policy = ignore_policy.is_yes();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let key = request.unique_key();
|
||||
let mut inner = self.inner();
|
||||
|
||||
let Some(media_content) =
|
||||
inner.media_list.iter_mut().find(|content| content.key == key)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
media_content.last_access = current_time;
|
||||
|
||||
Ok(Some(media_content.content.clone()))
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner();
|
||||
|
||||
let Some(media_content) =
|
||||
inner.media_list.iter_mut().find(|content| content.uri == uri)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
media_content.last_access = current_time;
|
||||
|
||||
Ok(Some(media_content.content.clone()))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
_policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error> {
|
||||
// This is mostly a noop. We don't care about this test implementation, only
|
||||
// whether this method was called with the right time.
|
||||
self.inner().cleanup_time = Some(current_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockTimeProvider {
|
||||
now: Mutex<SystemTime>,
|
||||
}
|
||||
|
||||
impl MockTimeProvider {
|
||||
/// Construct a `MockTimeProvider` with the given current time.
|
||||
fn new(now: SystemTime) -> Self {
|
||||
Self { now: Mutex::new(now) }
|
||||
}
|
||||
|
||||
/// Set the current time.
|
||||
fn set_now(&self, now: SystemTime) {
|
||||
*self.now.lock() = now;
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeProvider for MockTimeProvider {
|
||||
fn now(&self) -> SystemTime {
|
||||
*self.now.lock()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_service_empty_policy() {
|
||||
let content = b"some text content";
|
||||
let uri = mxc_uri!("mxc://server.local/AbcDe1234");
|
||||
let request = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// By default an empty policy is used.
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
service.restore(None);
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
assert!(!store.accessed());
|
||||
|
||||
// Add media.
|
||||
service
|
||||
.add_media_content(&store, &request, content.to_vec(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
|
||||
let media_content = store.inner().media_list[0].clone();
|
||||
assert_eq!(media_content.uri, uri);
|
||||
assert_eq!(media_content.content, content);
|
||||
assert!(!media_content.ignore_policy);
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
let loaded_content = service.get_media_content(&store, &request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
let loaded_content = service.get_media_content_for_uri(&store, uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
// Update ignore_policy.
|
||||
service
|
||||
.set_ignore_media_retention_policy(&store, &request, IgnoreMediaRetentionPolicy::Yes)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
|
||||
let media_content = store.inner().media_list[0].clone();
|
||||
assert!(media_content.ignore_policy);
|
||||
|
||||
// Try a cleanup. With the empty policy the store should not be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_service_non_empty_policy() {
|
||||
// Content of less than 32 bytes.
|
||||
let small_content = b"some text content";
|
||||
let small_uri = mxc_uri!("mxc://server.local/small");
|
||||
let small_request = MediaRequestParameters {
|
||||
source: MediaSource::Plain(small_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
// Content of more than 32 bytes.
|
||||
let big_content = b"some much much larger text content";
|
||||
let big_uri = mxc_uri!("mxc://server.local/big");
|
||||
let big_request = MediaRequestParameters {
|
||||
source: MediaSource::Plain(big_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
// Limit the file size to 32 bytes in the retention policy.
|
||||
let policy = MediaRetentionPolicy { max_file_size: Some(32), ..Default::default() };
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Check that restoring the policy works.
|
||||
service.restore(Some(MediaRetentionPolicy::default()));
|
||||
assert_eq!(service.media_retention_policy(), MediaRetentionPolicy::default());
|
||||
assert!(!store.accessed());
|
||||
|
||||
// Set the media retention policy.
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(service.media_retention_policy(), policy);
|
||||
assert_eq!(store.inner().media_retention_policy, Some(policy));
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
// Add small media, it should work because its size is lower than the max file
|
||||
// size.
|
||||
service
|
||||
.add_media_content(
|
||||
&store,
|
||||
&small_request,
|
||||
small_content.to_vec(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
|
||||
let media_content = store.inner().media_list[0].clone();
|
||||
assert_eq!(media_content.uri, small_uri);
|
||||
assert_eq!(media_content.content, small_content);
|
||||
assert!(!media_content.ignore_policy);
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
let loaded_content = service.get_media_content(&store, &small_request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(small_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
let loaded_content = service.get_media_content_for_uri(&store, small_uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(small_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[0].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Add big media, it will not work because it is bigger than the max file size.
|
||||
service
|
||||
.add_media_content(
|
||||
&store,
|
||||
&big_request,
|
||||
big_content.to_vec(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.inner().media_list.len(), 1);
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content, None);
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
let loaded_content = service.get_media_content_for_uri(&store, big_uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content, None);
|
||||
|
||||
// Add big media, but this time ignore the policy.
|
||||
service
|
||||
.add_media_content(
|
||||
&store,
|
||||
&big_request,
|
||||
big_content.to_vec(),
|
||||
IgnoreMediaRetentionPolicy::Yes,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.inner().media_list.len(), 2);
|
||||
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(big_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[1].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
let loaded_content = service.get_media_content_for_uri(&store, big_uri).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(loaded_content.as_deref(), Some(big_content.as_slice()));
|
||||
|
||||
// The last access time was updated.
|
||||
let media = store.inner().media_list[1].clone();
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
// Try a cleanup, the store should be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, Some(now));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! Types and traits regarding media caching of the event cache store.
|
||||
|
||||
mod media_retention_policy;
|
||||
mod media_service;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreMediaIntegrationTests;
|
||||
pub use self::{
|
||||
media_retention_policy::MediaRetentionPolicy,
|
||||
media_service::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService},
|
||||
};
|
||||
@@ -0,0 +1,450 @@
|
||||
// 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};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{
|
||||
time::{Instant, SystemTime},
|
||||
MxcUri, OwnedMxcUri, RoomId,
|
||||
};
|
||||
|
||||
use super::{
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
EventCacheStore, EventCacheStoreError, Result,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
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 {
|
||||
inner: StdRwLock<MemoryStoreInner>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
events: RelationalLinkedChunk<Event, Gap>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
}
|
||||
|
||||
/// A media content in the `MemoryStore`.
|
||||
#[derive(Debug)]
|
||||
struct MediaContent {
|
||||
/// The URI of the content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The unique key of the content.
|
||||
key: String,
|
||||
|
||||
/// The bytes of the content.
|
||||
data: Vec<u8>,
|
||||
|
||||
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
inner: StdRwLock::new(MemoryStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
media_retention_policy: None,
|
||||
}),
|
||||
// No need to call `restore()` since nothing is persisted.
|
||||
media_service: MediaService::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.events.apply_updates(room_id, updates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reload_linked_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.reload_chunks(room_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().events.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<()> {
|
||||
self.media_service.add_media_content(self, request, data, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some(media_content) =
|
||||
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
|
||||
{
|
||||
media_content.uri = to.uri().to_owned();
|
||||
media_content.key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
self.media_service.get_media_content(self, request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) =
|
||||
inner.media.iter().position(|media_content| media_content.key == expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
inner.media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.media_service.get_media_content_for_uri(self, uri).await
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let positions = inner
|
||||
.media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
inner.media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_media_retention_policy(self, policy).await
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.media_service.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.media_service.clean_up_media_cache(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreMedia for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
|
||||
Ok(self.inner.read().unwrap().media_retention_policy)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().media_retention_policy = Some(policy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
last_access: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len()) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push(MediaContent {
|
||||
uri: request.uri().to_owned(),
|
||||
key: request.unique_key(),
|
||||
data,
|
||||
ignore_policy,
|
||||
last_access,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
|
||||
{
|
||||
media_content.ignore_policy = ignore_policy.is_yes();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
expected_uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error> {
|
||||
if !policy.has_limitations() {
|
||||
// We can safely skip all the checks.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First, check media content that exceed the max filesize.
|
||||
if policy.computed_max_file_size().is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len())
|
||||
});
|
||||
}
|
||||
|
||||
// Then, clean up expired media content.
|
||||
if policy.last_access_expiry.is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy
|
||||
|| !policy.has_content_expired(current_time, content.last_access)
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, if the cache size is too big, remove old items until it fits.
|
||||
if let Some(max_cache_size) = policy.max_cache_size {
|
||||
// Reverse the iterator because in case the cache size is overflowing, we want
|
||||
// to count the number of old items to remove. Items are sorted by last access
|
||||
// and old items are at the start.
|
||||
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
|
||||
(0usize, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
|(mut cache_size, mut items_to_remove), (index, content)| {
|
||||
if content.ignore_policy {
|
||||
// Do not count it.
|
||||
return (cache_size, items_to_remove);
|
||||
}
|
||||
|
||||
let remove_item = if items_to_remove.is_empty() {
|
||||
// We have not reached the max cache size yet.
|
||||
if let Some(sum) = cache_size.checked_add(content.data.len()) {
|
||||
cache_size = sum;
|
||||
// Start removing items if we have exceeded the max cache size.
|
||||
cache_size > max_cache_size
|
||||
} else {
|
||||
// The cache size is overflowing, remove the remaining items, since the
|
||||
// max cache size cannot be bigger than
|
||||
// usize::MAX.
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// We have reached the max cache size already, just remove it.
|
||||
true
|
||||
};
|
||||
|
||||
if remove_item {
|
||||
items_to_remove.push(index);
|
||||
}
|
||||
|
||||
(cache_size, items_to_remove)
|
||||
},
|
||||
);
|
||||
|
||||
// The indexes are already in reverse order so we can just iterate in that order
|
||||
// to remove them starting by the end.
|
||||
for index in items_to_remove {
|
||||
inner.media.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MemoryStore, Result};
|
||||
use crate::event_cache_store_media_integration_tests;
|
||||
|
||||
async fn get_event_cache_store() -> Result<MemoryStore> {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
event_cache_store_integration_tests_time!();
|
||||
event_cache_store_media_integration_tests!(with_media_size_tests);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// 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;
|
||||
pub mod media;
|
||||
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, DEFAULT_CHUNK_CAPACITY},
|
||||
};
|
||||
|
||||
/// 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: Arc<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: Arc::new(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 fmt::Debug for EventCacheStoreLockGuard<'_> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
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 store failed to serialize or deserialize some data.
|
||||
#[error("Error serializing or deserializing data from the event cache store: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
/// 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),
|
||||
|
||||
/// The store contains invalid data.
|
||||
#[error("The store contains invalid data: {details}")]
|
||||
InvalidData {
|
||||
/// Details why the data contained in the store was invalid.
|
||||
details: String,
|
||||
},
|
||||
}
|
||||
|
||||
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,351 @@
|
||||
// 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::{
|
||||
linked_chunk::{RawChunk, Update},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{MxcUri, RoomId};
|
||||
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
EventCacheStoreError,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
};
|
||||
|
||||
/// A default capacity for linked chunks, when manipulating in conjunction with
|
||||
/// an `EventCacheStore` implementation.
|
||||
// TODO: move back?
|
||||
pub const DEFAULT_CHUNK_CAPACITY: usize = 128;
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the event cache of the SDK.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
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>;
|
||||
|
||||
/// An [`Update`] reflects an operation that has happened inside a linked
|
||||
/// chunk. The linked chunk is used by the event cache to store the events
|
||||
/// in-memory. This method aims at forwarding this update inside this store.
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Remove all data tied to a given room from the cache.
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
|
||||
// Right now, this means removing all the linked chunk. If implementations
|
||||
// override this behavior, they should *also* include this code.
|
||||
self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await
|
||||
}
|
||||
|
||||
/// Return all the raw components of a linked chunk, so the caller may
|
||||
/// reconstruct the linked chunk later.
|
||||
async fn reload_linked_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Clear persisted events for all the rooms.
|
||||
///
|
||||
/// This will empty and remove all the linked chunks stored previously,
|
||||
/// using the above [`Self::handle_linked_chunk_updates`] methods.
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// 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>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> 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>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// In theory, there could be several files stored using the same URI and a
|
||||
/// different `MediaFormat`. This API is meant to be used with a media file
|
||||
/// that has only been stored with a single format.
|
||||
///
|
||||
/// If there are several media files for a given URI in different formats,
|
||||
/// this API will only return one of them. Which one is left as an
|
||||
/// implementation detail.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
async fn get_media_content_for_uri(&self, uri: &MxcUri)
|
||||
-> Result<Option<Vec<u8>>, 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>;
|
||||
|
||||
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
|
||||
/// keep media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the current `MediaRetentionPolicy`.
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
///
|
||||
/// If there is already an ongoing cleanup, this is a noop.
|
||||
async fn clean_up_media_cache(&self) -> 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 handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn reload_linked_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.reload_linked_chunk(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content, ignore_policy).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 get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content_for_uri(uri).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)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.0.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.0.clean_up_media_cache().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) }
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
// 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, MediaRequest, 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);
|
||||
}
|
||||
|
||||
#[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 =
|
||||
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
|
||||
let request_thumbnail = MediaRequest {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
|
||||
Method::Crop,
|
||||
uint!(100),
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// 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::{num::NonZeroUsize, sync::RwLock as StdRwLock};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::ring_buffer::RingBuffer;
|
||||
use ruma::{MxcUri, OwnedMxcUri};
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use crate::media::{MediaRequest, 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>)>>,
|
||||
}
|
||||
|
||||
// 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)) }
|
||||
}
|
||||
}
|
||||
|
||||
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 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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[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!();
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// 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::str::Utf8Error;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
|
||||
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},
|
||||
};
|
||||
|
||||
/// 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>;
|
||||
@@ -1,145 +0,0 @@
|
||||
// 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::MediaRequest;
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// 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: &MediaRequest,
|
||||
content: Vec<u8>,
|
||||
) -> 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: &MediaRequest,
|
||||
) -> 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: &MediaRequest) -> Result<(), Self::Error>;
|
||||
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # 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 add_media_content(
|
||||
&self,
|
||||
request: &MediaRequest,
|
||||
content: Vec<u8>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequest,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> 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) }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,24 @@
|
||||
//! Utilities for working with events to decide whether they are suitable for
|
||||
//! use as a [crate::Room::latest_event].
|
||||
|
||||
#![cfg(any(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::message::SyncRoomMessageEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
message::SyncRoomMessageEvent,
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
},
|
||||
UserId,
|
||||
};
|
||||
use ruma::{events::sticker::SyncStickerEvent, MxcUri, OwnedEventId};
|
||||
use ruma::{MxcUri, OwnedEventId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
@@ -37,6 +43,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
|
||||
@@ -50,14 +60,17 @@ 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)) => {
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
if let Some(original_message) = message.as_original() {
|
||||
let is_replacement =
|
||||
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
|
||||
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
relation_type == RelationType::Replacement
|
||||
} else {
|
||||
@@ -66,13 +79,13 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
|
||||
});
|
||||
|
||||
if is_replacement {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
} else {
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
|
||||
@@ -103,8 +116,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +164,7 @@ pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLat
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LatestEvent {
|
||||
/// The actual event.
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -144,7 +178,7 @@ pub struct LatestEvent {
|
||||
#[derive(Deserialize)]
|
||||
struct SerializedLatestEvent {
|
||||
/// The actual event.
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -177,7 +211,7 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
|
||||
match serde_json::from_str::<SyncTimelineEvent>(raw.get()) {
|
||||
match serde_json::from_str::<TimelineEvent>(raw.get()) {
|
||||
Ok(value) => {
|
||||
return Ok(LatestEvent {
|
||||
event: value,
|
||||
@@ -196,13 +230,13 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
|
||||
impl LatestEvent {
|
||||
/// Create a new [`LatestEvent`] without the sender's profile.
|
||||
pub fn new(event: SyncTimelineEvent) -> Self {
|
||||
pub fn new(event: TimelineEvent) -> Self {
|
||||
Self { event, sender_profile: None, sender_name_is_ambiguous: None }
|
||||
}
|
||||
|
||||
/// Create a new [`LatestEvent`] with maybe the sender's profile.
|
||||
pub fn new_with_sender_details(
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
sender_profile: Option<MinimalRoomMemberEvent>,
|
||||
sender_name_is_ambiguous: Option<bool>,
|
||||
) -> Self {
|
||||
@@ -210,17 +244,17 @@ impl LatestEvent {
|
||||
}
|
||||
|
||||
/// Transform [`Self`] into an event.
|
||||
pub fn into_event(self) -> SyncTimelineEvent {
|
||||
pub fn into_event(self) -> TimelineEvent {
|
||||
self.event
|
||||
}
|
||||
|
||||
/// Get a reference to the event.
|
||||
pub fn event(&self) -> &SyncTimelineEvent {
|
||||
pub fn event(&self) -> &TimelineEvent {
|
||||
&self.event
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the event.
|
||||
pub fn event_mut(&mut self) -> &mut SyncTimelineEvent {
|
||||
pub fn event_mut(&mut self) -> &mut TimelineEvent {
|
||||
&mut self.event
|
||||
}
|
||||
|
||||
@@ -260,11 +294,16 @@ impl LatestEvent {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches::assert_matches;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::serde::Raw;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{
|
||||
@@ -302,14 +341,16 @@ mod tests {
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
UnsignedRoomRedactionEvent,
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
|
||||
VoipVersionId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
|
||||
use super::LatestEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{is_suitable_for_latest_event, PossibleLatestEvent};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
@@ -328,12 +369,13 @@ 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");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_polls_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
|
||||
@@ -351,12 +393,13 @@ 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?");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
|
||||
@@ -375,10 +418,11 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
|
||||
@@ -397,10 +441,11 @@ mod tests {
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
@@ -418,11 +463,12 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event =
|
||||
@@ -440,11 +486,12 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
@@ -468,11 +515,12 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
@@ -490,9 +538,13 @@ mod tests {
|
||||
}),
|
||||
));
|
||||
|
||||
assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted);
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
@@ -507,11 +559,12 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_replacement_events_are_unsuitable() {
|
||||
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
|
||||
@@ -531,7 +584,7 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
@@ -543,7 +596,7 @@ mod tests {
|
||||
latest_event: LatestEvent,
|
||||
}
|
||||
|
||||
let event = SyncTimelineEvent::new(
|
||||
let event = TimelineEvent::new(
|
||||
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
|
||||
);
|
||||
|
||||
@@ -562,9 +615,12 @@ mod tests {
|
||||
json!({
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"encryption_info": null,
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
"kind": {
|
||||
"PlainText": {
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -578,6 +634,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,15 +28,15 @@ mod client;
|
||||
pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
mod error;
|
||||
pub mod event_cache_store;
|
||||
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")]
|
||||
pub mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
@@ -54,12 +55,12 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMemberships, RoomState,
|
||||
RoomStateFilter,
|
||||
apply_redaction, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate,
|
||||
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,7 +26,7 @@ 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,
|
||||
@@ -43,9 +44,9 @@ impl UniqueKey for MediaFormat {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,19 +57,6 @@ pub struct MediaThumbnailSize {
|
||||
/// The desired height of the thumbnail. The actual thumbnail may not match
|
||||
/// the size specified.
|
||||
pub height: UInt,
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaThumbnailSize {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
/// The desired settings of a media thumbnail.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaThumbnailSettings {
|
||||
/// The desired size of the thumbnail.
|
||||
pub size: MediaThumbnailSize,
|
||||
|
||||
/// If we want to request an animated thumbnail from the homeserver.
|
||||
///
|
||||
@@ -82,14 +70,24 @@ pub struct MediaThumbnailSettings {
|
||||
impl MediaThumbnailSettings {
|
||||
/// Constructs a new `MediaThumbnailSettings` with the given method, width
|
||||
/// and height.
|
||||
pub fn new(method: Method, width: UInt, height: UInt) -> Self {
|
||||
Self { size: MediaThumbnailSize { method, width, height }, animated: false }
|
||||
///
|
||||
/// 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 {
|
||||
let mut key = self.size.unique_key();
|
||||
let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height);
|
||||
|
||||
if self.animated {
|
||||
key.push_str(UNIQUE_SEPARATOR);
|
||||
@@ -109,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,
|
||||
|
||||
@@ -119,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 {
|
||||
@@ -129,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())
|
||||
}
|
||||
@@ -225,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,
|
||||
|
||||
@@ -123,7 +123,7 @@ use std::{
|
||||
};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use ruma::{
|
||||
events::{
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
@@ -202,15 +202,19 @@ 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) {
|
||||
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
|
||||
if marks_as_unread(event.raw(), user_id) {
|
||||
self.num_unread += 1;
|
||||
}
|
||||
|
||||
let mut has_notify = false;
|
||||
let mut has_mention = false;
|
||||
|
||||
for action in &event.push_actions {
|
||||
let Some(actions) = event.push_actions.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for action in actions.iter() {
|
||||
if !has_notify && action.should_notify() {
|
||||
self.num_notifications += 1;
|
||||
has_notify = true;
|
||||
@@ -236,7 +240,7 @@ impl RoomReadReceipts {
|
||||
&mut self,
|
||||
receipt_event_id: &EventId,
|
||||
user_id: &UserId,
|
||||
events: impl IntoIterator<Item = &'a SyncTimelineEvent>,
|
||||
events: impl IntoIterator<Item = &'a TimelineEvent>,
|
||||
) -> bool {
|
||||
let mut counting_receipts = false;
|
||||
|
||||
@@ -269,11 +273,11 @@ impl RoomReadReceipts {
|
||||
pub trait PreviousEventsProvider: Send + Sync {
|
||||
/// Returns the list of known timeline events, in sync order, for the given
|
||||
/// room.
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<SyncTimelineEvent>;
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<TimelineEvent>;
|
||||
}
|
||||
|
||||
impl PreviousEventsProvider for () {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<SyncTimelineEvent> {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<TimelineEvent> {
|
||||
Vector::new()
|
||||
}
|
||||
}
|
||||
@@ -292,7 +296,7 @@ struct ReceiptSelector {
|
||||
|
||||
impl ReceiptSelector {
|
||||
fn new(
|
||||
all_events: &Vector<SyncTimelineEvent>,
|
||||
all_events: &Vector<TimelineEvent>,
|
||||
latest_active_receipt_event: Option<&EventId>,
|
||||
) -> Self {
|
||||
let event_id_to_pos = Self::create_sync_index(all_events.iter());
|
||||
@@ -310,7 +314,7 @@ impl ReceiptSelector {
|
||||
/// Create a mapping of `event_id` -> sync order for all events that have an
|
||||
/// `event_id`.
|
||||
fn create_sync_index<'a>(
|
||||
events: impl Iterator<Item = &'a SyncTimelineEvent> + 'a,
|
||||
events: impl Iterator<Item = &'a TimelineEvent> + 'a,
|
||||
) -> BTreeMap<OwnedEventId, usize> {
|
||||
// TODO: this should be cached and incrementally updated.
|
||||
BTreeMap::from_iter(
|
||||
@@ -405,10 +409,10 @@ impl ReceiptSelector {
|
||||
/// Try to match an implicit receipt, that is, the one we get for events we
|
||||
/// sent ourselves.
|
||||
#[instrument(skip_all)]
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) {
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[TimelineEvent]) {
|
||||
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 };
|
||||
@@ -432,13 +436,13 @@ impl ReceiptSelector {
|
||||
/// Returns true if there's an event common to both groups of events, based on
|
||||
/// their event id.
|
||||
fn events_intersects<'a>(
|
||||
previous_events: impl Iterator<Item = &'a SyncTimelineEvent>,
|
||||
new_events: &[SyncTimelineEvent],
|
||||
previous_events: impl Iterator<Item = &'a TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
) -> bool {
|
||||
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
|
||||
new_events
|
||||
.iter()
|
||||
.any(|ev| ev.event_id().map_or(false, |event_id| previous_events_ids.contains(&event_id)))
|
||||
.any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id)))
|
||||
}
|
||||
|
||||
/// Given a set of events coming from sync, for a room, update the
|
||||
@@ -454,8 +458,8 @@ pub(crate) fn compute_unread_counts(
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
receipt_event: Option<&ReceiptEventContent>,
|
||||
previous_events: Vector<SyncTimelineEvent>,
|
||||
new_events: &[SyncTimelineEvent],
|
||||
previous_events: Vector<TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
read_receipts: &mut RoomReadReceipts,
|
||||
) {
|
||||
debug!(?read_receipts, "Starting.");
|
||||
@@ -620,11 +624,14 @@ mod tests {
|
||||
use std::{num::NonZeroUsize, ops::Not as _};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::{sync_timeline_event, EventBuilder};
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::receipt::{ReceiptThread, ReceiptType},
|
||||
events::{
|
||||
receipt::{ReceiptThread, ReceiptType},
|
||||
room::{member::MembershipState, message::MessageType},
|
||||
},
|
||||
owned_event_id, owned_user_id,
|
||||
push::Action,
|
||||
room_id, user_id, EventId, UserId,
|
||||
@@ -638,24 +645,14 @@ mod tests {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
let f = EventFactory::new();
|
||||
|
||||
// A message from somebody else marks the room as unread...
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
});
|
||||
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(other_user_id).into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id));
|
||||
|
||||
// ... but a message from ourselves doesn't.
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
});
|
||||
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(user_id).into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
@@ -665,24 +662,16 @@ mod tests {
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// An edit to a message from somebody else doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": {
|
||||
"body": " * edited message",
|
||||
"m.new_content": {
|
||||
"body": "edited message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$someeventid:localhost",
|
||||
"rel_type": "m.replace"
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.text_msg("* edited message")
|
||||
.edit(
|
||||
event_id!("$someeventid:localhost"),
|
||||
MessageType::text_plain("edited message").into(),
|
||||
)
|
||||
.event_id(event_id!("$ida"))
|
||||
.sender(other_user_id)
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
@@ -692,19 +681,11 @@ mod tests {
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// A redact of a message from somebody else doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"reason": "🛑"
|
||||
},
|
||||
"event_id": "$151957878228ssqrJ:localhost",
|
||||
"origin_server_ts": 151957878000000_u64,
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.redaction",
|
||||
"redacts": "$151957878228ssqrj:localhost",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.redaction(event_id!("$151957878228ssqrj:localhost"))
|
||||
.sender(other_user_id)
|
||||
.event_id(event_id!("$151957878228ssqrJ:localhost"))
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
@@ -715,22 +696,11 @@ mod tests {
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// A reaction from somebody else to a message doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"origin_server_ts": 159027581000000_u64,
|
||||
"sender": other_user_id,
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.reaction(event_id!("$15275047031IXQRj:localhost"), "👍")
|
||||
.sender(other_user_id)
|
||||
.event_id(event_id!("$15275047031IXQRi:localhost"))
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
@@ -739,18 +709,13 @@ mod tests {
|
||||
fn test_state_event_doesnt_mark_as_unread() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let event_id = event_id!("$1");
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"displayname": "Alice",
|
||||
"membership": "join",
|
||||
},
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 1432135524678u64,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
});
|
||||
|
||||
let ev = EventFactory::new()
|
||||
.member(user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.display_name("Alice")
|
||||
.event_id(event_id)
|
||||
.into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
@@ -759,17 +724,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_count_unread_and_mentions() {
|
||||
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new_with_push_actions(
|
||||
sync_timeline_event!({
|
||||
"sender": user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
}),
|
||||
push_actions,
|
||||
)
|
||||
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> TimelineEvent {
|
||||
let mut ev = EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$ida"))
|
||||
.into_event();
|
||||
ev.push_actions = Some(push_actions);
|
||||
ev
|
||||
}
|
||||
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
@@ -843,14 +805,12 @@ mod tests {
|
||||
|
||||
// When provided with one event, that's not the receipt event, we don't count
|
||||
// it.
|
||||
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.room.message",
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
}))
|
||||
fn make_event(event_id: &EventId) -> TimelineEvent {
|
||||
EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id!("@bob:example.org"))
|
||||
.event_id(event_id)
|
||||
.into()
|
||||
}
|
||||
|
||||
let mut receipts = RoomReadReceipts {
|
||||
@@ -948,20 +908,6 @@ mod tests {
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
}
|
||||
|
||||
fn sync_timeline_message(
|
||||
sender: &UserId,
|
||||
event_id: impl serde::Serialize,
|
||||
body: impl serde::Serialize,
|
||||
) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": sender,
|
||||
"type": "m.room.message",
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 42,
|
||||
"content": { "body": body, "msgtype": "m.text" },
|
||||
}))
|
||||
}
|
||||
|
||||
/// Smoke test for `compute_unread_counts`.
|
||||
#[test]
|
||||
fn test_basic_compute_unread_counts() {
|
||||
@@ -972,15 +918,14 @@ mod tests {
|
||||
|
||||
let mut previous_events = Vector::new();
|
||||
|
||||
let ev1 = sync_timeline_message(other_user_id, receipt_event_id, "A");
|
||||
let ev2 = sync_timeline_message(other_user_id, "$2", "A");
|
||||
let f = EventFactory::new();
|
||||
let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event();
|
||||
let ev2 = f.text_msg("A").sender(other_user_id).event_id(event_id!("$2")).into_event();
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
receipt_event_id.to_owned(),
|
||||
ReceiptType::Read,
|
||||
user_id.to_owned(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
let mut read_receipts = Default::default();
|
||||
compute_unread_counts(
|
||||
@@ -999,7 +944,8 @@ mod tests {
|
||||
previous_events.push_back(ev1);
|
||||
previous_events.push_back(ev2);
|
||||
|
||||
let new_event = sync_timeline_message(other_user_id, "$3", "A");
|
||||
let new_event =
|
||||
f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event();
|
||||
compute_unread_counts(
|
||||
user_id,
|
||||
room_id,
|
||||
@@ -1013,13 +959,14 @@ mod tests {
|
||||
assert_eq!(read_receipts.num_unread, 2);
|
||||
}
|
||||
|
||||
fn make_test_events(user_id: &UserId) -> Vector<SyncTimelineEvent> {
|
||||
let ev1 = sync_timeline_message(user_id, "$1", "With the lights out, it's less dangerous");
|
||||
let ev2 = sync_timeline_message(user_id, "$2", "Here we are now, entertain us");
|
||||
let ev3 = sync_timeline_message(user_id, "$3", "I feel stupid and contagious");
|
||||
let ev4 = sync_timeline_message(user_id, "$4", "Here we are now, entertain us");
|
||||
let ev5 = sync_timeline_message(user_id, "$5", "Hello, hello, hello, how low?");
|
||||
vec![ev1, ev2, ev3, ev4, ev5].into()
|
||||
fn make_test_events(user_id: &UserId) -> Vector<TimelineEvent> {
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
|
||||
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
|
||||
let ev3 = f.text_msg("I feel stupid and contagious").event_id(event_id!("$3"));
|
||||
let ev4 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$4"));
|
||||
let ev5 = f.text_msg("Hello, hello, hello, how low?").event_id(event_id!("$5"));
|
||||
[ev1, ev2, ev3, ev4, ev5].into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Test that when multiple receipts come in a single event, we can still
|
||||
@@ -1035,30 +982,32 @@ mod tests {
|
||||
|
||||
// Given a receipt event marking events 1-3 as read using a combination of
|
||||
// different thread and privacy types,
|
||||
let f = EventFactory::new();
|
||||
for receipt_type_1 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
for receipt_thread_1 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
|
||||
for receipt_type_2 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
for receipt_thread_2 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([
|
||||
(
|
||||
owned_event_id!("$2"),
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(
|
||||
event_id!("$2"),
|
||||
user_id,
|
||||
receipt_type_1.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_1.clone(),
|
||||
),
|
||||
(
|
||||
owned_event_id!("$3"),
|
||||
)
|
||||
.add(
|
||||
event_id!("$3"),
|
||||
user_id,
|
||||
receipt_type_2.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_2.clone(),
|
||||
),
|
||||
(
|
||||
owned_event_id!("$1"),
|
||||
)
|
||||
.add(
|
||||
event_id!("$1"),
|
||||
user_id,
|
||||
receipt_type_1.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_2.clone(),
|
||||
),
|
||||
]);
|
||||
)
|
||||
.build();
|
||||
|
||||
// When I compute the notifications for this room (with no new events),
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
@@ -1118,12 +1067,10 @@ mod tests {
|
||||
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$6"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
assert!(read_receipts.pending.is_empty());
|
||||
@@ -1154,12 +1101,10 @@ mod tests {
|
||||
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$1"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
// Sync with a read receipt *and* a single event that was already known: in that
|
||||
// case, only consider the new events in isolation, and compute the
|
||||
@@ -1190,12 +1135,7 @@ mod tests {
|
||||
let events = make_test_events(uid);
|
||||
|
||||
// An event with no id.
|
||||
let ev6 = SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": uid,
|
||||
"type": "m.room.message",
|
||||
"origin_server_ts": 42,
|
||||
"content": { "body": "yolo", "msgtype": "m.text" },
|
||||
}));
|
||||
let ev6 = EventFactory::new().text_msg("yolo").sender(uid).no_event_id().into_event();
|
||||
|
||||
let index = ReceiptSelector::create_sync_index(events.iter().chain(&[ev6]));
|
||||
|
||||
@@ -1261,8 +1201,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_noop() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1296,8 +1237,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_doesnt_match_known_events() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1332,8 +1274,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_matches_known_events_no_initial() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1373,8 +1316,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_matches_known_events_with_initial() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1412,21 +1356,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_new_receipt() {
|
||||
let myself = owned_user_id!("@alice:example.org");
|
||||
let myself = user_id!("@alice:example.org");
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let f = EventFactory::new();
|
||||
{
|
||||
// Thread receipts are ignored.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$5"),
|
||||
ReceiptType::Read,
|
||||
myself.clone(),
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(
|
||||
event_id!("$5"),
|
||||
myself,
|
||||
ReceiptType::Read,
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1440,14 +1388,12 @@ mod tests {
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$6"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
assert_eq!(pending.len(), 1);
|
||||
|
||||
@@ -1460,14 +1406,12 @@ mod tests {
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1479,14 +1423,12 @@ mod tests {
|
||||
// better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$4")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1498,14 +1440,12 @@ mod tests {
|
||||
// new better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1519,23 +1459,14 @@ mod tests {
|
||||
// new better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([
|
||||
(
|
||||
owned_event_id!("$4"),
|
||||
ReceiptType::ReadPrivate,
|
||||
myself.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
),
|
||||
(
|
||||
owned_event_id!("$6"),
|
||||
ReceiptType::ReadPrivate,
|
||||
myself.clone(),
|
||||
ReceiptThread::Main,
|
||||
),
|
||||
(owned_event_id!("$3"), ReceiptType::Read, myself.clone(), ReceiptThread::Main),
|
||||
]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$4"), myself, ReceiptType::ReadPrivate, ReceiptThread::Unthreaded)
|
||||
.add(event_id!("$6"), myself, ReceiptType::ReadPrivate, ReceiptThread::Main)
|
||||
.add(event_id!("$3"), myself, ReceiptType::Read, ReceiptThread::Main)
|
||||
.build();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending.len(), 1);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
|
||||
@@ -1560,8 +1491,16 @@ mod tests {
|
||||
assert!(best_receipt.is_none());
|
||||
|
||||
// Now, if there are events I've written too...
|
||||
events.push_back(sync_timeline_message(&myself, "$6", "A mulatto, an albino"));
|
||||
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(&myself)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
// And I search for my implicit read receipt,
|
||||
@@ -1573,7 +1512,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_compute_unread_counts_with_implicit_receipt() {
|
||||
let user_id = owned_user_id!("@alice:example.org");
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let bob = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!room:example.org");
|
||||
|
||||
@@ -1581,28 +1520,36 @@ mod tests {
|
||||
let mut events = make_test_events(bob);
|
||||
|
||||
// One by me,
|
||||
events.push_back(sync_timeline_message(&user_id, "$6", "A mulatto, an albino"));
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
|
||||
// And others by Bob,
|
||||
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
|
||||
events.push_back(sync_timeline_message(bob, "$8", "A denial, a denial"));
|
||||
events.push_back(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
|
||||
);
|
||||
|
||||
let events: Vec<_> = events.into_iter().collect();
|
||||
|
||||
// I have a read receipt attached to one of Bob's event sent before my message,
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
|
||||
// And I compute the unread counts for all those new events (no previous events
|
||||
// in that room),
|
||||
compute_unread_counts(
|
||||
&user_id,
|
||||
user_id,
|
||||
room_id,
|
||||
Some(&receipt_event),
|
||||
Vector::new(),
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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::{
|
||||
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
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<OwnedDirectUserIdentifier>>::new();
|
||||
for (user_identifier, rooms) in direct_event.content.iter() {
|
||||
for room_id in rooms {
|
||||
new_dms.entry(room_id).or_default().insert(user_identifier.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let rooms = store.rooms();
|
||||
let 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user