Compare commits
739 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 742a0db07b | |||
| 4701faf039 | |||
| 4ea0418abe | |||
| 2412403a1e | |||
| ed72d3439a | |||
| 9eee20b4f0 | |||
| bc45457d0e | |||
| 76651aec69 | |||
| e1cda064ee | |||
| 94e5dbea0c | |||
| c6e7a17f65 | |||
| 1e7bc1286e | |||
| b04cc9fe27 | |||
| 054dc31ce4 | |||
| aaff9c5d72 | |||
| b1773d33c2 | |||
| 090351c6ac | |||
| 045eb3486b | |||
| 40738ae119 | |||
| acc66266c7 | |||
| e6094e6b07 | |||
| ea538351e9 | |||
| b4d7881a58 | |||
| 8c4a19bb85 | |||
| 017644864a | |||
| 59604713e8 | |||
| d563cebcfc | |||
| 19b7036119 | |||
| d6b942d3ac | |||
| c1302c417a | |||
| 176684a07c | |||
| be9e7ac9bf | |||
| 407621f055 | |||
| bbc6df78ae | |||
| 9f02dcd412 | |||
| ab98028a2e | |||
| 7c7cbb2566 | |||
| 32b4bbc1b0 | |||
| e47867f232 | |||
| 4411274b12 | |||
| 32b72580da | |||
| 73449f4f57 | |||
| bbe35e8190 | |||
| 107fc07d08 | |||
| 4585d5f4d8 | |||
| 239203a813 | |||
| 24d7518a01 | |||
| e6059251d0 | |||
| f4fef6e995 | |||
| 850b7dde6d | |||
| 700c17f383 | |||
| 9e842a5d07 | |||
| 18175c1cd0 | |||
| 100a04ae2c | |||
| 3a655083d6 | |||
| 46947be662 | |||
| fccafd8c80 | |||
| 1e30d5f0b0 | |||
| 4ab12543ce | |||
| a82ccf1069 | |||
| 45a9d96573 | |||
| c9324b2f30 | |||
| 8e93bb5373 | |||
| 8df55fa3e7 | |||
| 478df4af33 | |||
| 04fdf7f2f6 | |||
| 179136a9a4 | |||
| e51996a47c | |||
| 12e5614fc8 | |||
| 14d550739a | |||
| fbcd8ef546 | |||
| e5f6153f54 | |||
| badba6eebc | |||
| 72f2296809 | |||
| b1af16ef09 | |||
| 1cf0601ba3 | |||
| e034a51b7b | |||
| 9e6a6c0e71 | |||
| d1633f2a78 | |||
| 4dbee471ac | |||
| 997f992d15 | |||
| 3d5b32494e | |||
| c5893f882c | |||
| 68e8866bcf | |||
| c98d9db185 | |||
| cee2b1bebf | |||
| 19a96b41df | |||
| 80decaebf4 | |||
| 20ee85bd0f | |||
| 813c5fc9f9 | |||
| a349b8e753 | |||
| ec44c74d53 | |||
| 7475f03b13 | |||
| f13dc4b070 | |||
| 9757ff54ba | |||
| aa79e34794 | |||
| d5f09dffaa | |||
| ae9070815c | |||
| f49c588ade | |||
| 8e0dba641d | |||
| 2f7d2b3b9b | |||
| d228bde8ef | |||
| 83a7d591bd | |||
| 247bb4960e | |||
| 83f9d74626 | |||
| eed5f11f26 | |||
| 75a977cc47 | |||
| 5b396d0b0d | |||
| 7de210a88f | |||
| 127154fcfa | |||
| 0e46732ede | |||
| 1352bd74d6 | |||
| 762135ba22 | |||
| 7a1fadddc3 | |||
| f343c98b63 | |||
| fd4821c3ec | |||
| 5dea64b0ef | |||
| 2388acaf33 | |||
| 91bc1ef28f | |||
| b1eaa5edca | |||
| 0b5e1fb9c5 | |||
| 2eb4323fe1 | |||
| db806f6b8d | |||
| 64a51af18d | |||
| da52532b60 | |||
| f846eea7a3 | |||
| 475db3e640 | |||
| efe511e5e8 | |||
| 4ae82dd634 | |||
| d860749f95 | |||
| 012a9825a4 | |||
| 1c22d0b25b | |||
| be86fe4aa9 | |||
| 385f7aa86d | |||
| 5f996f77c6 | |||
| 02491fc6ec | |||
| 0f62ff991d | |||
| 6b245264e1 | |||
| f7b92c84e7 | |||
| 4eb3cc9812 | |||
| 95d8ba94e1 | |||
| ca436016b4 | |||
| 5b82550199 | |||
| 5d396e4795 | |||
| e9c8f101d6 | |||
| 6e97607c2d | |||
| 4e71b7c351 | |||
| 9ab886fa2b | |||
| 60072b3456 | |||
| 822b1c9787 | |||
| 52344fad77 | |||
| e0427767aa | |||
| 927c82f97a | |||
| 97ba0b1bbb | |||
| 17df3f84d0 | |||
| 4fbc83af44 | |||
| 9508675aca | |||
| 0d08ed0758 | |||
| 913ebe9fa9 | |||
| f702364fe9 | |||
| 1db4a4cb9a | |||
| 2e9e9aedd7 | |||
| 38df621b8a | |||
| f9c23b3612 | |||
| 952c5af07c | |||
| 717f016f21 | |||
| 3ad70623bb | |||
| 84a21a42d0 | |||
| d2eab603c1 | |||
| 8883b9db5a | |||
| a3424a7c4a | |||
| fa3ca980e9 | |||
| cbd4722dcb | |||
| a22caa32c0 | |||
| f61ba4f47c | |||
| 9a3857d3a7 | |||
| e79f832160 | |||
| 46d05d877b | |||
| 610f82aeb2 | |||
| 60490f4eff | |||
| 6a828e31dd | |||
| fff270d997 | |||
| a50ecb5b18 | |||
| 18654444b6 | |||
| 10ff5d0cc6 | |||
| f9584f5b2a | |||
| 66619e9d1d | |||
| 2ea1c42a1a | |||
| f6ef5fbfd1 | |||
| a6062a6cfd | |||
| 3f3f6c2fc6 | |||
| d7d4730b21 | |||
| 4c4cd41457 | |||
| 4a519bd547 | |||
| 4109fddc97 | |||
| 7e98858815 | |||
| 3a0a5b9888 | |||
| 621d936b4c | |||
| a2f89e85b9 | |||
| 4ed239351a | |||
| 5c3bca86a4 | |||
| e934235045 | |||
| f2cc6c650a | |||
| 8103b9cc23 | |||
| d3c839a2d0 | |||
| 3b1418463b | |||
| 9f248affa9 | |||
| edf7604d30 | |||
| f7a767ce97 | |||
| 6c922e69d0 | |||
| 01d75e939c | |||
| 8b805b1ea5 | |||
| a1768ea518 | |||
| 0b66019632 | |||
| c064ca8b18 | |||
| fa6d18b55f | |||
| 17de97e98e | |||
| c60f92a917 | |||
| 0865e96f08 | |||
| 8f726e4fb9 | |||
| b4ebc8bc25 | |||
| da1369b9c2 | |||
| d122f10147 | |||
| bcf81c89e9 | |||
| d3dd9d28c8 | |||
| dcd08e8d3b | |||
| 82c583b5bc | |||
| 81ff96d569 | |||
| 49db60d951 | |||
| 8f4267332a | |||
| 950c42742d | |||
| f91ffb4c31 | |||
| 301ca5e2b8 | |||
| 1a384f0049 | |||
| 781df5526d | |||
| ea07d0199a | |||
| ddfd2fb570 | |||
| 3b5f1eee27 | |||
| 99ae08ebfe | |||
| 9efb0de4d7 | |||
| 05b40af2c1 | |||
| 09ee1375cd | |||
| a96485c07a | |||
| 9680fc3a0f | |||
| 422f925033 | |||
| 3695d76dec | |||
| 0faf3eecea | |||
| 13a30f7b7a | |||
| 444fcfa098 | |||
| cadbd33957 | |||
| 8189010d58 | |||
| ee828614fb | |||
| ef3c6719cf | |||
| 55ef066eb4 | |||
| e3105bfca8 | |||
| 9fff07dfbb | |||
| ce7f2fb24f | |||
| b60b042cfe | |||
| 046d8ebdd1 | |||
| 896f4114a2 | |||
| e2d42cef67 | |||
| 12e39f5ef1 | |||
| 38875b021d | |||
| 0bbfc3ce41 | |||
| 7c6ff517d5 | |||
| 8e25c36289 | |||
| 200fde8850 | |||
| d4e0ec302a | |||
| c3e01a6902 | |||
| 25b1c85998 | |||
| 5a5b8afd4a | |||
| 9af8fad880 | |||
| b748148d36 | |||
| 513a69c547 | |||
| 2f58109853 | |||
| deda2ec75a | |||
| 01a0e136dc | |||
| 5ab792e68e | |||
| 89d46cd342 | |||
| 155a7b481b | |||
| 0ac943b4c4 | |||
| 18fe2b20e6 | |||
| 64a0f62631 | |||
| c169cab3b0 | |||
| 91858e0913 | |||
| f70c036ff9 | |||
| 70d6d557ca | |||
| a52df18740 | |||
| ce6ef90f74 | |||
| 5f00e71f5f | |||
| 539bd9c79a | |||
| 23785a3023 | |||
| 3d31b81abf | |||
| 3b2ef02749 | |||
| c887819809 | |||
| 780b782660 | |||
| 4d4ae79b7a | |||
| af0a3aa91b | |||
| 84e5ce0a98 | |||
| 574df8951e | |||
| 6ff186b744 | |||
| 6036f19af6 | |||
| f78bac2fc6 | |||
| 1ae3b79c08 | |||
| b4d702f1ef | |||
| c258368925 | |||
| 43f19e411a | |||
| 4e8ddde2f2 | |||
| dfb3713f1e | |||
| 105fa53a4c | |||
| 7238d3ca23 | |||
| 3c522f9505 | |||
| 0796b71bd3 | |||
| 547ab31b82 | |||
| 3f5d51a203 | |||
| 4ea0b7d984 | |||
| d2faa1be1a | |||
| ca0929876f | |||
| c9d3088701 | |||
| 68b902e4bc | |||
| 8bb8bbae9c | |||
| 3733ee8534 | |||
| b045462f76 | |||
| 8bb5e501a4 | |||
| 7aead98863 | |||
| 7607c4ef82 | |||
| 02fe0c9f53 | |||
| d117532fae | |||
| 34c5e24b72 | |||
| 5bdb7ae732 | |||
| 5729ad4dd5 | |||
| d36b68b7d1 | |||
| 34d71b0392 | |||
| 430304f392 | |||
| 5f54237f4f | |||
| f78f1795eb | |||
| dcd8aa13f0 | |||
| a4e68ba885 | |||
| e8fb133cbf | |||
| a11daf24e5 | |||
| b012512a21 | |||
| 818b1b6000 | |||
| 5b523d21e4 | |||
| fcab05e44b | |||
| 70fb53612d | |||
| 059b5e7c1f | |||
| 9d7c21f508 | |||
| 3705b73256 | |||
| 3fb874f901 | |||
| 215087f2c1 | |||
| 1e2bf39a7c | |||
| c1bc814ac2 | |||
| 7185fcbac8 | |||
| 01e2e4877c | |||
| 3622355a08 | |||
| c388332e47 | |||
| 6042a9e9b0 | |||
| 04260458ef | |||
| 7c3a8b335a | |||
| a8aa8761d8 | |||
| bfc96181dd | |||
| eb1ee434b3 | |||
| e7fa8a429a | |||
| 8b6572bb23 | |||
| 43e94bcfb4 | |||
| 588d604653 | |||
| 90cf669f94 | |||
| 70a608b3b5 | |||
| bcd4337985 | |||
| 58972ca9d9 | |||
| ef672271c2 | |||
| 1bc417956c | |||
| 1127184db2 | |||
| 63a3c2d51a | |||
| 3f55c217c1 | |||
| 19632683f7 | |||
| 03f964a5a3 | |||
| 752beadb83 | |||
| 6244721ab4 | |||
| 6e034b0d7b | |||
| e4a717dff5 | |||
| 6acf628fc5 | |||
| db91bb35ee | |||
| 3b7dbf5c04 | |||
| 22bfe8fbd3 | |||
| e4243b7af3 | |||
| f891c1ca06 | |||
| c2839d7594 | |||
| c45ede972e | |||
| 9b485013e1 | |||
| cb3d281f8f | |||
| a72c19a240 | |||
| cf4a1dee4b | |||
| 487470be8f | |||
| b94823216d | |||
| 232119cf57 | |||
| 64818e2ef9 | |||
| a2ded93234 | |||
| fc892564d8 | |||
| 358803783f | |||
| ef440eed2b | |||
| 79e1930b22 | |||
| 6191e2c24e | |||
| ba2fe1d387 | |||
| c7b4b5dc05 | |||
| 87d9bd14e3 | |||
| 44a4ca94be | |||
| 3b43a7e5e8 | |||
| 773d304f9e | |||
| b47174e394 | |||
| b1e0339159 | |||
| 7fbc4144b1 | |||
| a2a26ae45e | |||
| 06301bc2f8 | |||
| 85abe76121 | |||
| 97ff61081a | |||
| 0017ccb0c1 | |||
| 350fdd8ad4 | |||
| f937bf60e2 | |||
| da394f5015 | |||
| 158e3925b7 | |||
| 4828f4c555 | |||
| 92e7cb3af2 | |||
| a6a590aa1f | |||
| d01a28c9b2 | |||
| 68075b65fb | |||
| d4d40945e8 | |||
| 2b69a7f741 | |||
| d85b45ed64 | |||
| b43237536d | |||
| fbafae42bb | |||
| 08563d4096 | |||
| 32f3670aeb | |||
| 8a23aae9dc | |||
| 2822815384 | |||
| a7cb094aaf | |||
| 764a8a4c77 | |||
| 2e6790d0a5 | |||
| 67d8db3d93 | |||
| 52518e0e2e | |||
| b8b54246c4 | |||
| 95e93ca00b | |||
| bb6ba08dfb | |||
| 8c515b0c12 | |||
| 81a69f82d2 | |||
| f4a6d12979 | |||
| 203a3783ae | |||
| 8eb7264e5d | |||
| a4bd36cbe8 | |||
| 8e8ad0167a | |||
| 0f78959c9a | |||
| 7a431a3afd | |||
| a6d033ea4c | |||
| ad41cbc368 | |||
| cf0c3e7009 | |||
| 3a60d34f3f | |||
| 9114c22b70 | |||
| 8655afd117 | |||
| f5ec9b6427 | |||
| 2eab7cf818 | |||
| 6072618e85 | |||
| 70b19cc907 | |||
| 57d21ccdf6 | |||
| 37ee5d5075 | |||
| 681b22142f | |||
| 248d77a4d9 | |||
| f4451b5c82 | |||
| 59b7da247c | |||
| be5bd449b5 | |||
| 2eb29518dc | |||
| 16d0840115 | |||
| d90576bf0d | |||
| bbeb2d21b1 | |||
| 187b646c07 | |||
| 8a47e3cd1c | |||
| 973d71f54e | |||
| 943b048fa0 | |||
| 2c70c31c56 | |||
| be7129bacc | |||
| 2ec33183c4 | |||
| d6d720c015 | |||
| 5b52c729a5 | |||
| 7d649e92d4 | |||
| 021d3fb5d7 | |||
| 5f02212312 | |||
| e158e8abc0 | |||
| e2ec8bcbd6 | |||
| 36e0d4bfb8 | |||
| 05362be89a | |||
| 03fc5dacbe | |||
| 0a0e31af83 | |||
| 290f27a343 | |||
| 0033de1f49 | |||
| 688eb6880d | |||
| 07704c7835 | |||
| 60c3b3dd43 | |||
| d00cfb0ba8 | |||
| f7e1866bda | |||
| b316c534ea | |||
| fa7fd5df42 | |||
| adc8276162 | |||
| abecb33e34 | |||
| b26ce417f0 | |||
| 5a1bd54bb1 | |||
| 0d0e2aa472 | |||
| 88ed0afcb3 | |||
| 9938ab8b1f | |||
| eed7384934 | |||
| 32255cd178 | |||
| c51536a054 | |||
| 6099928b40 | |||
| fac1f295b2 | |||
| 24d02a72e3 | |||
| 2a073043fd | |||
| c5b35209b3 | |||
| 8e759befd3 | |||
| 9faffa5b10 | |||
| 31200357a0 | |||
| 75b8c9fe93 | |||
| 004d98230c | |||
| eb37a0d2e1 | |||
| 8a0e61e95b | |||
| a2e2765298 | |||
| 6e1e0981b1 | |||
| b2120a8f3d | |||
| 5d169ae765 | |||
| 34dd7ea3cd | |||
| 4930c589a8 | |||
| f6d2e73cab | |||
| 2bd5ec30d1 | |||
| 425b502977 | |||
| 0dfecd78d6 | |||
| 4754ac2cbf | |||
| 72bb452b5b | |||
| c17dbf9ebe | |||
| 5fd7c9e179 | |||
| 840ce43fed | |||
| e71d565346 | |||
| 5a06f5f351 | |||
| 1e01e3fc62 | |||
| bcba5f4571 | |||
| 7736b50c04 | |||
| 6e1dc121a5 | |||
| 08f0200174 | |||
| eda561e00e | |||
| 578320cefc | |||
| 59ed28d3f8 | |||
| a0eecac8e0 | |||
| 27ba6d070b | |||
| 14ca34b09b | |||
| 2166de7b0d | |||
| fd66ae9226 | |||
| 56100dfa00 | |||
| 2b567e18bc | |||
| a5e84230c7 | |||
| bf4a46e8de | |||
| 659ae57218 | |||
| dff6cb4414 | |||
| 76348977d4 | |||
| a66e6822ed | |||
| b494303c07 | |||
| 0f0e37b677 | |||
| fc12a7340f | |||
| 80390346b1 | |||
| 4b87dfea0b | |||
| 79aa0ab60d | |||
| a8ef44306a | |||
| b929f3e569 | |||
| 1c737e6569 | |||
| 8ae88e1e45 | |||
| 768f9bfdb6 | |||
| 864d6c1a43 | |||
| da70aea5b0 | |||
| 1a9c7d5e2f | |||
| 9cd7760858 | |||
| 8575ed3f64 | |||
| 1834f36136 | |||
| 0bbefa000b | |||
| a84c97b292 | |||
| 94267d9597 | |||
| 62eb1996d9 | |||
| ec5c31a19d | |||
| 7e474c3a52 | |||
| 8706ad74b3 | |||
| 3cc88e5008 | |||
| a937780623 | |||
| 9dc27698dd | |||
| 49d72cd992 | |||
| 98e799da80 | |||
| ea386c9e64 | |||
| bba2af9882 | |||
| 51934dd249 | |||
| 07924ad4e4 | |||
| 1312a27597 | |||
| 038207870a | |||
| eb62ac9fad | |||
| 79154bd03d | |||
| c7990e6e33 | |||
| c839c01205 | |||
| 62d2d0ff94 | |||
| bcae429062 | |||
| b0e9f3c666 | |||
| a325105190 | |||
| 3835a7ff94 | |||
| ead9400702 | |||
| 78172bb7b6 | |||
| bbf2164ab2 | |||
| 46a2ee6177 | |||
| f1caf8f27f | |||
| 60bfc48b6b | |||
| afa339c02b | |||
| c9b7fc7007 | |||
| 2f6bb3a1eb | |||
| a259860221 | |||
| 2765c18e61 | |||
| b44b6478c0 | |||
| 0bebf144d1 | |||
| 975b08c019 | |||
| 453613c13f | |||
| cb94969e2a | |||
| 0e4e4eae2b | |||
| b9410dff61 | |||
| 013bb9a5ac | |||
| 891ed0efff | |||
| 2e3be13b4d | |||
| efcb7125ad | |||
| 681863423c | |||
| 8c6922d5a9 | |||
| 8c60ef2635 | |||
| 3e9e74a888 | |||
| a06403c12f | |||
| d3a7d26c7d | |||
| e83f37e68b | |||
| efda12058f | |||
| f12ee861b0 | |||
| 48cc68c466 | |||
| acaff39594 | |||
| bdc564bb55 | |||
| ac68c4a47d | |||
| 75a5c19f91 | |||
| 7a53615d80 | |||
| 802e137ae5 | |||
| 68cb3fb6a4 | |||
| cc1fbf9882 | |||
| 89c1c8e4fa | |||
| 423f15a125 | |||
| 4f2cd1c5ec | |||
| 8a6c4fdcb4 | |||
| b8cbd6c448 | |||
| 5ccbc1c378 | |||
| 0002ea46ab | |||
| bb46dc74d0 | |||
| c2bc465c06 | |||
| b788ba0d73 | |||
| 53a74f3949 | |||
| 215ca3d798 | |||
| 87032a36bd | |||
| 4f881b55f9 | |||
| bb9bdee4a7 | |||
| 4216ec6113 | |||
| b2df2742bd | |||
| 163ed929fe | |||
| 9776ae6acd | |||
| 68c2b89bf5 | |||
| b744e5789a | |||
| 231840f6ae | |||
| fc224b17c7 | |||
| 5522509e6b | |||
| 89fd0b5e53 | |||
| eee1fa2b71 | |||
| 62763ca000 | |||
| 5e573417cb | |||
| c3621f2bd1 | |||
| 0eac2a099f | |||
| 90eb403c18 | |||
| 878e02b652 | |||
| bbe8f17b1a | |||
| f65bb6016c | |||
| 976eacb624 | |||
| 5fe5cfd85f | |||
| 0233ac906e | |||
| 4cc1cd1913 | |||
| 2248bbf6ab | |||
| 2afbdfae0b | |||
| d2ca0262ae | |||
| f1064425bd | |||
| 5ef3ecac8c | |||
| 6c537d74de | |||
| 476fe5f9d2 | |||
| 186132c248 | |||
| 0f90631d4a | |||
| 80262f2f36 | |||
| 91c5f8a01a | |||
| 502d6d3095 | |||
| db4ce0bea5 | |||
| b0c0e0e0c4 | |||
| 5c78ddec13 | |||
| 59a62550e6 | |||
| fd356a9e17 | |||
| fc69b2683f | |||
| 5c94177581 | |||
| 0335785e67 | |||
| c860be4969 | |||
| 8ff7e58bc0 | |||
| 01c0775e59 | |||
| bd3ddc19e9 | |||
| 8156bc25f8 | |||
| 441b006c5f | |||
| ce3b67f801 | |||
| 260037c4c7 | |||
| a93274de36 | |||
| 77b426e1aa | |||
| 251530b6f4 | |||
| 46c7338509 | |||
| 9e2f2b3534 | |||
| 03c6dd9bfc | |||
| 3b7a626b8f | |||
| 2e7bea9253 | |||
| c7de40b54d | |||
| 086233ad5f | |||
| 412b7bbc7b | |||
| 0617c88c1c | |||
| 83b390204d | |||
| 4ef249dc6e | |||
| 0aef2559bd | |||
| 653a00351c | |||
| 8606ac3dfb | |||
| db5503e30e | |||
| c51b4f03a2 | |||
| 0fc0a5514d | |||
| e83af1aae2 | |||
| ab58c376dd | |||
| 6c8fb507a2 | |||
| b1c28f4bc1 | |||
| 6dbdffd36e | |||
| e45387b65b | |||
| b8803cb465 | |||
| 3c88b46c54 | |||
| d25632507d |
@@ -7,3 +7,7 @@ crates-io = "https://docs.rs/"
|
||||
|
||||
[unstable]
|
||||
rustdoc-map = true
|
||||
|
||||
[target.aarch64-linux-android]
|
||||
# These rust flags improve the performance on Android on arm64
|
||||
rustflags = ["-C", "target-feature=+neon,+aes,+sha2,+sha3,+pmuv3"]
|
||||
|
||||
+3
-3
@@ -10,6 +10,7 @@ exclude = [
|
||||
version = 2
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate, not critical." },
|
||||
{ id = "RUSTSEC-2024-0388", reason = "Unmaintained derivative crate, not a direct dependency" },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
@@ -27,9 +28,6 @@ allow = [
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
exceptions = [
|
||||
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
|
||||
]
|
||||
|
||||
[bans]
|
||||
# We should disallow this, but it's currently a PITA.
|
||||
@@ -53,4 +51,6 @@ allow-git = [
|
||||
# We can release vodozemac whenever we need but let's not block development
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
# A patch override for the bindings: https://github.com/Alorel/rust-indexed-db/pull/72
|
||||
"https://github.com/matrix-org/rust-indexed-db",
|
||||
]
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
* @matrix-org/rust
|
||||
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
|
||||
/crates/matrix-sdk-indexeddb/src/crypto_store @matrix-org/rust @matrix-org/rust-crypto-reviewers
|
||||
|
||||
@@ -19,9 +19,67 @@ jobs:
|
||||
- linked_chunk
|
||||
- store_bench
|
||||
- timeline
|
||||
- room_list
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
# This CI workflow can run into space issue, so we're cleaning up some
|
||||
# space here.
|
||||
- name: Create some more space
|
||||
run: |
|
||||
echo "Disk space before cleanup"
|
||||
df -h
|
||||
|
||||
cd /opt
|
||||
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
|
||||
rm -rf /opt/hostedtoolcache
|
||||
|
||||
# Get rid of binaries and libs we're not interested in.
|
||||
sudo rm -rf \
|
||||
/usr/local/julia* \
|
||||
/usr/local/aws*
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/bin/minikube \
|
||||
/usr/local/bin/node \
|
||||
/usr/local/bin/stack \
|
||||
/usr/local/bin/bicep \
|
||||
/usr/local/bin/pulumi* \
|
||||
/usr/local/bin/helm \
|
||||
/usr/local/bin/azcopy \
|
||||
/usr/local/bin/packer \
|
||||
/usr/local/bin/cmake-gui \
|
||||
/usr/local/bin/cpack
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/share/powershell \
|
||||
/usr/local/share/chromium
|
||||
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
|
||||
echo "::group::/usr/local/bin/*"
|
||||
du -hsc /usr/local/bin/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/share/*"
|
||||
du -hsc /usr/local/share/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/*"
|
||||
du -hsc /usr/local/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/lib/*"
|
||||
du -hsc /usr/local/lib/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/opt/*"
|
||||
du -hsc /opt/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "Disk space after cleanup"
|
||||
df -h
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670
|
||||
@@ -31,10 +89,11 @@ jobs:
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build -p benchmarks ${{ matrix.benchmark }} --features codspeed
|
||||
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c
|
||||
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
mode: "instrumentation"
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -69,10 +69,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
|
||||
+19
-30
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -125,6 +125,8 @@ jobs:
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -167,7 +169,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -239,7 +241,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -248,7 +250,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.1
|
||||
uses: qmaru/wasm-pack-action@v0.5.2
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.13.1
|
||||
@@ -289,10 +291,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.35.7
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -301,7 +303,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -311,7 +313,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-08-08
|
||||
toolchain: nightly-2025-10-01
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -360,7 +362,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -384,24 +386,11 @@ jobs:
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
|
||||
cargo nextest run --profile ci -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
|
||||
|
||||
compile-bench:
|
||||
name: 🚄 Compile benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Compile benchmarks (no run)
|
||||
run: |
|
||||
cargo bench --profile dev --no-run
|
||||
files: ./target/nextest/ci/junit.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
df -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
- name: Store coverage report in artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
|
||||
@@ -10,5 +10,5 @@ jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46.0.5
|
||||
uses: tj-actions/changed-files@v47.0.0
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
|
||||
@@ -7,6 +7,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@v0.9.1
|
||||
uses: bnjbvr/cargo-machete@72602674bc341ca927683caddbf578672c352476
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -31,10 +31,10 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-08-08
|
||||
toolchain: nightly-2025-10-01
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Block Fixup Commit Merge
|
||||
uses: 13rac1/block-fixup-merge-action@v2.0.0
|
||||
|
||||
@@ -11,6 +11,6 @@ jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- name: 'Fetch coverage report from artifacts'
|
||||
id: prepare_report
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
path: repo_root
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
|
||||
+108
-47
@@ -1,4 +1,4 @@
|
||||
# Contributing to matrix-rust-sdk
|
||||
# Contributing to `matrix-rust-sdk`
|
||||
|
||||
## Chat rooms
|
||||
|
||||
@@ -29,50 +29,55 @@ 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.
|
||||
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
You can add/review snapshot tests using [insta.rs](https://insta.rs)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool:
|
||||
And for an improved review experience it's recommended (but not necessary) to
|
||||
install the `cargo-insta` tool:
|
||||
|
||||
Unix:
|
||||
```
|
||||
|
||||
```shell
|
||||
curl -LsSf https://insta.rs/install.sh | sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
```
|
||||
|
||||
```shell
|
||||
powershell -c "irm https://insta.rs/install.ps1 | iex"
|
||||
```
|
||||
|
||||
Usual flow is to first run the test, then review them.
|
||||
```
|
||||
|
||||
```shell
|
||||
cargo insta test
|
||||
cargo insta review
|
||||
```
|
||||
|
||||
### Intermittent failure policy
|
||||
|
||||
While we strive to add test coverage for as many features as we can, it sometimes happens that the
|
||||
tests will be intermittently failing in CI (such tests are sometimes called "flaky"). This can be
|
||||
caused by race conditions of all sorts, either in the test code itself, but sometimes in the
|
||||
underlying feature being tested too, and as such, it requires some investigation, usually from the
|
||||
original author of the test.
|
||||
While we strive to add test coverage for as many features as we can, it
|
||||
sometimes happens that the tests will be intermittently failing in CI (such
|
||||
tests are sometimes called "flaky"). This can be caused by race conditions
|
||||
of all sorts, either in the test code itself, but sometimes in the underlying
|
||||
feature being tested too, and as such, it requires some investigation, usually
|
||||
from the original author of the test.
|
||||
|
||||
Whenever such an intermittent failure happens, we try to open an issue to track the failures,
|
||||
adding the
|
||||
Whenever such an intermittent failure happens, we try to open an issue to track
|
||||
the failures, adding the
|
||||
[`intermittent-failure`](https://github.com/matrix-org/matrix-rust-sdk/issues?q=is%3Aissue%20state%3Aopen%20label%3Aintermittent-failure)
|
||||
label to it, and commenting with links to CI runs where the failure happened.
|
||||
|
||||
If a test has been intermittently failing for **two weeks** or more, and no one is actively working
|
||||
on fixing it, then we might decide to mark the test as `ignored` until it is fixed, to not cause
|
||||
unrelated failures in other contributors' pull requests and pushes.
|
||||
If a test has been intermittently failing for **two weeks** or more, and no one
|
||||
is actively working on fixing it, then we might decide to mark the test as
|
||||
`ignored` until it is fixed, to not cause unrelated failures in other
|
||||
contributors' pull requests and pushes.
|
||||
|
||||
## Pull requests
|
||||
|
||||
@@ -87,7 +92,7 @@ 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
|
||||
## 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
|
||||
@@ -122,12 +127,17 @@ 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.
|
||||
* 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)).
|
||||
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
|
||||
@@ -139,14 +149,15 @@ git trailers are supported and have special meaning (see below).
|
||||
|
||||
Conventional Commits are structured as follows:
|
||||
|
||||
```
|
||||
```text
|
||||
<type>(<scope>): <short summary>
|
||||
```
|
||||
|
||||
The type of changes which will be included in changelogs is one of the following:
|
||||
The type of changes which will be included in changelogs is one of the
|
||||
following:
|
||||
|
||||
* `feat`: A new feature
|
||||
* `fix`: A bug fix
|
||||
* `fix`: A bugfix
|
||||
* `doc`: Documentation changes
|
||||
* `refactor`: Code refactoring
|
||||
* `perf`: Performance improvements
|
||||
@@ -163,15 +174,16 @@ 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.
|
||||
* `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.
|
||||
Please include all the fields that are available.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
```text
|
||||
fix(crypto): Use a constant-time Base64 encoder for secret key material
|
||||
|
||||
This patch fixes a security issue around a side-channel vulnerability[1]
|
||||
@@ -213,9 +225,9 @@ your contributions, follow these basic rules:
|
||||
|
||||
5. Keep PRs on topic and small. Large PRs are harder to review and more prone to
|
||||
delays. Create small, focused commits that address a single topic. Use a
|
||||
combination of [git add] -p or git checkout -p to split changes into logical
|
||||
units. This makes your work easier to review and reduces the chance of
|
||||
introducing unrelated changes.
|
||||
combination of [git add] -p or [git checkout] -p to split changes into
|
||||
logical units. This makes your work easier to review and reduces the chance
|
||||
of introducing unrelated changes.
|
||||
|
||||
[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch
|
||||
[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch
|
||||
@@ -227,12 +239,12 @@ guidelines to make the maintainers life easier and increase the chances that
|
||||
your PR will be reviewed swiftly.
|
||||
|
||||
1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup
|
||||
commits. These commits mark your changes as corrections of specific previous
|
||||
commits in the PR.
|
||||
commits. These commits mark your changes as corrections of specific previous
|
||||
commits in the PR.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
```shell
|
||||
git commit --fixup=<commit-hash>
|
||||
```
|
||||
|
||||
@@ -247,7 +259,7 @@ requested.
|
||||
3. Once the PR has been approved, rebase your PR to squash all the fixup
|
||||
commits, the [autosquash] option can help with this.
|
||||
|
||||
```bash
|
||||
```shell
|
||||
git rebase main --interactive --autosquash
|
||||
```
|
||||
|
||||
@@ -257,14 +269,16 @@ git rebase main --interactive --autosquash
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the [Linux Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
|
||||
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO ([Developer Certificate of Origin](http://developercertificate.org/)).
|
||||
This is a simple declaration that you wrote the contribution or otherwise have the right
|
||||
to contribute it to Matrix:
|
||||
and you agree to license it under the same terms as the project's
|
||||
license, we've adopted the same lightweight approach that the [Linux
|
||||
Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
|
||||
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md),
|
||||
and many other projects use: the DCO ([Developer Certificate of
|
||||
Origin](http://developercertificate.org/)). This is a simple declaration that
|
||||
you wrote the contribution or otherwise have the right to contribute it to
|
||||
Matrix:
|
||||
|
||||
```
|
||||
```text
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
@@ -305,7 +319,7 @@ By making a contribution to this project, I certify that:
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
```text
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
@@ -316,7 +330,7 @@ Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
```text
|
||||
git rebase --signoff origin/main
|
||||
```
|
||||
|
||||
@@ -324,8 +338,55 @@ git rebase --signoff origin/main
|
||||
|
||||
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
|
||||
with all features enabled, causing an error in `matrix-sdk` ("only one of the
|
||||
features 'native-tls' or 'rustls-tls' can be enabled"). To work around this,
|
||||
features `native-tls` or `rustls-tls` can be enabled"). To work around this,
|
||||
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
|
||||
`native-tls` or `rustls-tls` feature definitions:
|
||||
|
||||

|
||||
|
||||
## AI policy
|
||||
|
||||
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
|
||||
|
||||
### Terminology
|
||||
|
||||
This does not necessarily reflect the official or commonly used terminology.
|
||||
|
||||
Software and services that heavily rely on large language model technology to
|
||||
generate their outcomes are referred to as _Artificial Intelligence_ (AI).
|
||||
Examples of products that fit this definition: GitHub Copilot, ChatGPT, Claude
|
||||
Sonnet, DeepSeek, Llama and Gemini.
|
||||
|
||||
There is a distinction between _general_ and _narrow_ AI, all the aforementioned
|
||||
examples fall under general AI as they were not trained to execute a specific
|
||||
well-defined task. Narrow AI is trained to be used for specific well-defined
|
||||
tasks where the problem space is known in advance.
|
||||
|
||||
_Vibe coding_ is the practice where AI creates a code change (feature, bugfix,
|
||||
tests, refactor) with a human that describes what needs to be implemented.
|
||||
|
||||
_AI agents_ are AIs that are configured to perform interactions or make changes
|
||||
with little to no human supervision.
|
||||
|
||||
### Agreement
|
||||
|
||||
1. If content was made with the help of AI, you **must** convey that this is
|
||||
the case. This includes content that you authored but was motivated by a
|
||||
suggestion of AI.
|
||||
2. If at any point you used AI's work in your contribution you should make
|
||||
an effort to **verify** that you can submit this under the license of the
|
||||
repository.
|
||||
3. The **accountability** of using AI in a contribution lies with the person
|
||||
that makes that contribution.
|
||||
4. All communication, that includes: commit messages, pull request messages,
|
||||
documentation, code comments and issues (and comments on issues/pull
|
||||
requests), that is intended to be read by people to understand your thoughts
|
||||
and work **must not** have been generated with AI. We exclude machine
|
||||
translation and tooling that helps with grammar and spelling check.
|
||||
5. Using general AI for review is **forbidden**. If the change contains changes
|
||||
to the user experience it has to be approved by a human reviewer.
|
||||
6. It is **not allowed** to use AI in an autonomous-looking way to contribute to
|
||||
the Matrix Rust SDK. This also applies when someone engages in _vibe coding_
|
||||
or uses so-called _agent mode_.
|
||||
|
||||
[Forgejo]: https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md
|
||||
|
||||
Generated
+686
-494
File diff suppressed because it is too large
Load Diff
+48
-45
@@ -13,13 +13,13 @@ members = [
|
||||
exclude = ["testing/data"]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
aquamarine = "0.6.0"
|
||||
as_variant = "1.3.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
@@ -28,19 +28,19 @@ assert_matches2 = "0.1.2"
|
||||
async-compat = "0.2.5"
|
||||
async-rx = "0.1.3"
|
||||
# Bumping this to 0.3.6 produces a test failure because the semantic between the
|
||||
# versions changed subtly.
|
||||
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.3"
|
||||
bitflags = "2.10.0"
|
||||
byteorder = "1.5.0"
|
||||
cfg-if = "1.0.3"
|
||||
clap = "4.5.46"
|
||||
chrono = "0.4.41"
|
||||
cfg-if = "1.0.4"
|
||||
clap = "4.5.53"
|
||||
chrono = "0.4.42"
|
||||
dirs = "6.0.0"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.7.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.9.0"
|
||||
eyeball-im = { version = "0.8.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.10.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
@@ -50,11 +50,12 @@ growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.3.1"
|
||||
imbl = "5.0.0"
|
||||
indexmap = "2.11.0"
|
||||
insta = { version = "1.43.1", features = ["json", "redactions"] }
|
||||
imbl = "6.1.0"
|
||||
indexed_db_futures = { version = "0.7.0", package = "matrix_indexed_db_futures" }
|
||||
indexmap = "2.12.1"
|
||||
insta = { version = "1.44.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.77"
|
||||
js-sys = "0.3.82"
|
||||
mime = "0.3.17"
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
|
||||
once_cell = "1.21.3"
|
||||
@@ -62,9 +63,10 @@ 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.23", default-features = false }
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.12.24", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
ruma = { version = "0.13.0", features = [
|
||||
ruma = { version = "0.14.0", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-arbitrary-length-ids",
|
||||
@@ -72,6 +74,7 @@ ruma = { version = "0.13.0", features = [
|
||||
"compat-encrypted-stickers",
|
||||
"compat-lax-room-create-deser",
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3230",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
@@ -79,54 +82,54 @@ ruma = { version = "0.13.0", features = [
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4143",
|
||||
"unstable-msc4171",
|
||||
"unstable-msc4222",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
"unstable-msc4306",
|
||||
"unstable-msc4308"
|
||||
"unstable-msc4308",
|
||||
"unstable-msc4310",
|
||||
] }
|
||||
sentry = { version = "0.42.0", default-features = false }
|
||||
sentry-tracing = "0.42.0"
|
||||
serde = { version = "1.0.219", features = ["rc"] }
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.143"
|
||||
sentry = { version = "0.46.0", default-features = false }
|
||||
sentry-tracing = "0.46.0"
|
||||
serde = { version = "1.0.228", features = ["rc"] }
|
||||
serde_html_form = "0.2.8"
|
||||
serde_json = "1.0.145"
|
||||
sha2 = "0.10.9"
|
||||
similar-asserts = "1.7.0"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.21.0"
|
||||
thiserror = "2.0.16"
|
||||
tokio = { version = "1.47.1", default-features = false, features = ["sync"] }
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.41", default-features = false, features = ["std"] }
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-core = "0.1.34"
|
||||
tracing-subscriber = "0.3.20"
|
||||
unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
unicode-normalization = "0.1.25"
|
||||
uniffi = { version = "0.30.0" }
|
||||
uniffi_bindgen = { version = "0.30.0" }
|
||||
url = "2.5.7"
|
||||
uuid = "1.18.0"
|
||||
uuid = "1.18.1"
|
||||
vergen-gitcl = "1.0.8"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.69"
|
||||
wasm-bindgen = "0.2.105"
|
||||
wasm-bindgen-test = "0.3.55"
|
||||
web-sys = "0.3.82"
|
||||
wiremock = "0.6.5"
|
||||
zeroize = "1.8.1"
|
||||
zeroize = "1.8.2"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.14.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.14.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.14.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.16.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.16.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.16.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.14.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.14.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.14.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.14.0" }
|
||||
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.14.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.14.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.16.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.16.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.16.0" }
|
||||
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.16.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.16.0" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
|
||||
@@ -14,13 +14,16 @@ release = false
|
||||
codspeed = []
|
||||
|
||||
[dependencies]
|
||||
assert_matches.workspace = true
|
||||
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
|
||||
futures-util.workspace = true
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-ui.workspace = true
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -51,3 +54,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "event_cache"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_list"
|
||||
harness = false
|
||||
|
||||
@@ -317,6 +317,7 @@ fn find_event_relations(c: &mut Criterion) {
|
||||
let (target, relations) = room_event_cache
|
||||
.find_event_with_relations(target_event_id, filter)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
|
||||
assert_eq!(relations.len(), num_related_events as usize);
|
||||
|
||||
@@ -181,6 +181,8 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.as_clean()
|
||||
.unwrap()
|
||||
.clear_all_linked_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
use assert_matches::assert_matches;
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{stream::StreamExt, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::{
|
||||
RoomListService, eyeball_im::VectorDiff, room_list_service::filters::new_filter_non_left,
|
||||
};
|
||||
use rand::{distributions::Uniform, prelude::Distribution};
|
||||
use ruma::{EventId, RoomId, owned_user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a room list.
|
||||
pub fn create(c: &mut Criterion) {
|
||||
const NUMBER_OF_ROOMS: usize = 1000;
|
||||
const NUMBER_OF_EVENTS_PER_ROOM: usize = 1000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
|
||||
let (server, client) = runtime.block_on(async {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
(server, client)
|
||||
});
|
||||
|
||||
let sender_id = owned_user_id!("@mnt_io:matrix.org");
|
||||
let mut rand = rand::thread_rng();
|
||||
let server_ts_range = Uniform::from(100..1000);
|
||||
|
||||
for room_nth in 0..NUMBER_OF_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!r{room_nth}")).unwrap();
|
||||
let first_server_ts = server_ts_range.sample(&mut rand);
|
||||
let event_factory = EventFactory::new().room(&room_id).server_ts(first_server_ts);
|
||||
|
||||
let events = (0..NUMBER_OF_EVENTS_PER_ROOM)
|
||||
.map(|event_nth| {
|
||||
let event_id = EventId::parse(format!("$ev{room_nth}_{event_nth}")).unwrap();
|
||||
|
||||
event_factory.text_msg("a").sender(&sender_id).event_id(&event_id).into_raw_sync()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let _room = runtime.block_on(async {
|
||||
server
|
||||
.sync_room(&client, JoinedRoomBuilder::new(&room_id).add_timeline_bulk(events))
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
let mut group = c.benchmark_group("RoomList");
|
||||
group.throughput(Throughput::Elements(NUMBER_OF_ROOMS.try_into().unwrap()));
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
"Create",
|
||||
format!("{NUMBER_OF_ROOMS} rooms × {NUMBER_OF_EVENTS_PER_ROOM} events"),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
let room_list_service = RoomListService::new(client.clone())
|
||||
.await
|
||||
.expect("build the room list service");
|
||||
let room_list = room_list_service.all_rooms().await.expect("fetch `all_rooms`");
|
||||
let (entries_stream, entries_controller) =
|
||||
room_list.entries_with_dynamic_adapters(20);
|
||||
|
||||
// Setting the filter will trigger the entries stream computation.
|
||||
entries_controller.set_filter(Box::new(new_filter_non_left()));
|
||||
|
||||
pin_mut!(entries_stream);
|
||||
let update = entries_stream.next().await.expect("receiving the reset update");
|
||||
assert_eq!(update.len(), 1);
|
||||
assert_matches!(&update[0], VectorDiff::Reset { values } => {
|
||||
assert_eq!(values.len(), 20);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room_list;
|
||||
config = Criterion::default();
|
||||
targets = create
|
||||
}
|
||||
criterion_main!(room_list);
|
||||
@@ -1,7 +1,7 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::TimelineBuilder;
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineReadReceiptTracking};
|
||||
use ruma::{
|
||||
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
|
||||
owned_user_id,
|
||||
@@ -103,7 +103,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
@@ -869,9 +869,18 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `room_id` - The unique id of the room where the event was sent to.
|
||||
///
|
||||
/// * `handle_verification_events` - if the supplied event is a verification
|
||||
/// event, use it to update the verification state. **Note**: it is
|
||||
/// recommended to avoid setting this flag to true and use the explicit
|
||||
/// [`OlmMachine::receive_verification_event`] method instead:
|
||||
/// verification events sometimes need preparation before we can handle
|
||||
/// them: see the documentation for
|
||||
/// [`OlmMachine::receive_verification_event`].
|
||||
///
|
||||
/// * `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,
|
||||
@@ -1100,6 +1109,14 @@ impl OlmMachine {
|
||||
///
|
||||
/// This method can be used to pass verification events that are happening
|
||||
/// in rooms to the `OlmMachine`. The event should be in the decrypted form.
|
||||
///
|
||||
/// **Note**: If the supplied event is an `m.room.message` event with
|
||||
/// `msgtype: m.key.verification.request`, then the device information for
|
||||
/// the sending user must be up-to-date before calling this method
|
||||
/// (otherwise, the request will be ignored). It is hard to guarantee this
|
||||
/// is the case, but you can maximize your chances by explicitly making a
|
||||
/// request to /keys/query for the user's device info, and processing the
|
||||
/// response with [`OlmMachine::mark_request_as_sent`].
|
||||
pub fn receive_verification_event(
|
||||
&self,
|
||||
event: String,
|
||||
|
||||
@@ -6,12 +6,114 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.16.0] - 2025-12-04
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- `TimelineConfiguration::track_read_receipts`'s type is now an enum to allow tracking to be enabled for all events
|
||||
(like before) or only for message-like events (which prevents read receipts from being placed on state events).
|
||||
([#5900](https://github.com/matrix-org/matrix-rust-sdk/pull/5900))
|
||||
- `Client::reset_server_info()` has been split into `reset_supported_versions()`
|
||||
and `reset_well_known()`.
|
||||
([#5910](https://github.com/matrix-org/matrix-rust-sdk/pull/5910))
|
||||
- Add `HumanQrLoginError::NotFound` for non-existing / expired rendezvous sessions
|
||||
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
|
||||
- Add `HumanQrGrantLoginError::NotFound` for non-existing / expired rendezvous sessions
|
||||
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
|
||||
- The `LatestEventValue::Local` type gains 2 new fields: `sender` and `profile`.
|
||||
([#5885](https://github.com/matrix-org/matrix-rust-sdk/pull/5885))
|
||||
- The `Encryption::user_identity()` method has received a new argument. The
|
||||
`fallback_to_server` argument controls if we should attempt to fetch the user
|
||||
identity from the homeserver if it wasn't found in the local storage.
|
||||
([#5870](https://github.com/matrix-org/matrix-rust-sdk/pull/5870))
|
||||
- Expose the power level required to modify `m.space.child` on
|
||||
`room::power_levels::RoomPowerLevelsValues`.
|
||||
- Rename `Client::login_with_qr_code` to `Client::new_login_with_qr_code_handler`.
|
||||
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
|
||||
- Add the `sqlite` feature, along with the `indexeddb` feature, to enable either
|
||||
the SQLite or IndexedDB store. The `session_paths`, `session_passphrase`,
|
||||
`session_pool_max_size`, `session_cache_size` and `session_journal_size_limit`
|
||||
methods on `ClientBuilder` have been removed. New methods are added:
|
||||
`ClientBuilder::in_memory_store` if one wants non-persistent stores,
|
||||
`ClientBuilder::sqlite_store` to configure and to use SQLite stores (if
|
||||
the `sqlite` feature is enabled), and `ClientBuilder::indexeddb_store` to
|
||||
configure and to use IndexedDB stores (if the `indexeddb` feature is enabled).
|
||||
([#5811](https://github.com/matrix-org/matrix-rust-sdk/pull/5811))
|
||||
|
||||
The code:
|
||||
|
||||
```rust
|
||||
client_builder
|
||||
.session_paths("data_path", "cache_path")
|
||||
.passphrase("foobar")
|
||||
```
|
||||
|
||||
now becomes:
|
||||
|
||||
```rust
|
||||
client_builder
|
||||
.sqlite_store(
|
||||
SqliteSessionStoreBuilder::new("data_path", "cache_path")
|
||||
.passphrase("foobar")
|
||||
)
|
||||
```
|
||||
|
||||
- UniFFI was upgraded to `v0.30.0` ([#5808](https://github.com/matrix-org/matrix-rust-sdk/pull/5808)).
|
||||
- The `waveform` parameter in `Timeline::send_voice_message` format changed to a list of `f32`
|
||||
between 0 and 1.
|
||||
([#5732](https://github.com/matrix-org/matrix-rust-sdk/pull/5732))
|
||||
- The `normalized_power_level` field has been removed from the `RoomMember`
|
||||
struct.
|
||||
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635))
|
||||
- Remove the deprecated `CallNotify` event (`org.matrix.msc4075.call.notify`) in favor of the new
|
||||
`RtcNotification` event (`org.matrix.msc4075.rtc.notification`).
|
||||
([#5668](https://github.com/matrix-org/matrix-rust-sdk/pull/5668))
|
||||
- Add `QrLoginProgress::SyncingSecrets` to indicate that secrets are being synced between the two
|
||||
devices.
|
||||
([#5760](https://github.com/matrix-org/matrix-rust-sdk/pull/5760))
|
||||
- Add `Room::subscribe_to_send_queue_updates` to observe room send queue updates.
|
||||
([#5761](https://github.com/matrix-org/matrix-rust-sdk/pull/5761))
|
||||
- `Client::login_with_qr_code` now returns a handler that allows performing the flow with either the
|
||||
current device scanning or generating the QR code. Additionally, new errors `HumanQrLoginError::CheckCodeAlreadySent`
|
||||
and `HumanQrLoginError::CheckCodeCannotBeSent` were added.
|
||||
([#5786](https://github.com/matrix-org/matrix-rust-sdk/pull/5786))
|
||||
- `ComposerDraft` now includes attachments alongside the text message.
|
||||
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
|
||||
- Add `Client::subscribe_to_send_queue_updates` to observe global send queue updates.
|
||||
([#5784](https://github.com/matrix-org/matrix-rust-sdk/pull/5784))
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Client::get_store_sizes()` so to query the size of the existing stores, if available. ([#5911](https://github.com/matrix-org/matrix-rust-sdk/pull/5911))
|
||||
- Expose `is_space` in `NotificationRoomInfo`, allowing clients to determine if the room that triggered the notification is a space.
|
||||
- Add push actions to `NotificationItem` and replace `SyncNotification` with `NotificationItem`.
|
||||
([#5835](https://github.com/matrix-org/matrix-rust-sdk/pull/5835))
|
||||
- Add `Client::new_grant_login_with_qr_code_handler` for granting login to a new device by way of
|
||||
a QR code.
|
||||
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
|
||||
- Add `Client::register_notification_handler` for observing notifications generated from sync responses.
|
||||
([#5831](https://github.com/matrix-org/matrix-rust-sdk/pull/5831))
|
||||
- Add `Room::mark_as_fully_read_unchecked` so clients can mark a room as read without needing a `Timeline` instance. Note this method is not recommended as it can potentially cause incorrect read receipts, but it can needed in certain cases.
|
||||
- Add `Timeline::latest_event_id` to be able to fetch the event id of the latest event of the timeline.
|
||||
- Add `Room::load_or_fetch_event` so we can get a `TimelineEvent` given its event id ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
|
||||
- Add `TimelineEvent::thread_root_event_id` to expose the thread root event id for this type too ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
|
||||
- Add `NotificationSettings::get_raw_push_rules` so clients can fetch the raw JSON content of the push rules of the current user and include it in bug reports ([#5706](https://github.com/matrix-org/matrix-rust-sdk/pull/5706)).
|
||||
- Add new API to decline calls ([MSC4310](https://github.com/matrix-org/matrix-spec-proposals/pull/4310)): `Room::decline_call` and `Room::subscribe_to_call_decline_events`
|
||||
([#5614](https://github.com/matrix-org/matrix-rust-sdk/pull/5614))
|
||||
- Expose `m.federate` in `OtherState::RoomCreate` and `history_visibility` in `OtherState::RoomHistoryVisibility`, allowing clients to know whether a room federates and how its history is shared in the appropriate timeline events.
|
||||
- Expose `join_rule` in `OtherState::RoomJoinRules`, allowing clients to know the join rules of a room from the appropriate timeline events.
|
||||
|
||||
### Changes
|
||||
|
||||
- `Timeline::latest_event_id` now uses its `ui::Timeline::latest_event_id` counterpart, instead of getting the latest event from the timeline and then its id.([#5864](https://github.com/matrix-org/matrix-rust-sdk/pull/5864))
|
||||
- Build Android ARM64 bindings using better default RUSTFLAGS (the same used for iOS ARM64). This should improve performance. [(#5854)](https://github.com/matrix-org/matrix-rust-sdk/pull/5854)
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features:
|
||||
|
||||
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
|
||||
rooms based on their low priority status. These filters allow clients to show only low priority rooms
|
||||
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
|
||||
rooms based on their low priority status. These filters allow clients to show only low priority rooms
|
||||
or exclude low priority rooms from the room list.
|
||||
([#5508](https://github.com/matrix-org/matrix-rust-sdk/pull/5508))
|
||||
- Add `room_version` and `privileged_creators_role` to `RoomInfo` ([#5449](https://github.com/matrix-org/matrix-rust-sdk/pull/5449)).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
@@ -24,8 +24,13 @@ crate-type = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite", "unstable-msc4274"]
|
||||
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
|
||||
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"]
|
||||
# Use SQLite for the session storage.
|
||||
sqlite = ["matrix-sdk/sqlite"]
|
||||
# Use an embedded version of SQLite.
|
||||
bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
|
||||
# Use IndexedDB for the session storage.
|
||||
indexeddb = ["matrix-sdk/indexeddb"]
|
||||
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
|
||||
# Required when targeting a Javascript environment, like Wasm in a browser.
|
||||
js = ["matrix-sdk-ui/js"]
|
||||
@@ -36,28 +41,31 @@ rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
|
||||
# Enable sentry error monitoring, not compatible with Wasm platforms.
|
||||
sentry = ["dep:sentry", "dep:sentry-tracing"]
|
||||
|
||||
experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-emojis"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
extension-trait = "1.0.1"
|
||||
extension-trait = "1.0.2"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
language-tags = "0.3.2"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
||||
matrix-sdk = { workspace = true, features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
"federation-api",
|
||||
] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.16"
|
||||
mime = "0.3.17"
|
||||
once_cell.workspace = true
|
||||
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278", "unstable-hydra"] }
|
||||
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sentry = { workspace = true, optional = true, default-features = false, features = [
|
||||
@@ -82,7 +90,8 @@ oauth2.workspace = true
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
tokio = { workspace = true, features = ["sync", "macros"] }
|
||||
uniffi.workspace = true
|
||||
uniffi = { workspace = true, features = ["wasm-unstable-single-threaded"] }
|
||||
futures-executor.workspace = true
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
async-compat.workspace = true
|
||||
@@ -90,7 +99,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = "0.2.1"
|
||||
paranoid-android = "0.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
similar-asserts.workspace = true
|
||||
|
||||
@@ -3,31 +3,33 @@
|
||||
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
|
||||
|
||||
## Features
|
||||
|
||||
Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
### Platform specific
|
||||
### Platform specific
|
||||
|
||||
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
|
||||
### Functionality
|
||||
|
||||
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
|
||||
- `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one.
|
||||
- `sqlite`: Use SQLite for the session storage.
|
||||
- `bundled-sqlite`: Use an embedded version of SQLite instead of the system provided one.
|
||||
- `indexeddb`: Use IndexedDB for the session storage.
|
||||
|
||||
### Unstable specs
|
||||
|
||||
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
|
||||
|
||||
## Platforms
|
||||
|
||||
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
|
||||
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
|
||||
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
- Javascript/Wasm: `"unstable-msc4274,native-tls"`
|
||||
- JavaScript/Wasm: `"indexeddb,unstable-msc4274,native-tls"`
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
|
||||
|
||||
### Swift/iOS async
|
||||
|
||||
TBD
|
||||
|
||||
@@ -43,6 +43,23 @@ fn setup_x86_64_android_workaround() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a workaround for watchOS simulator builds to manually link against the
|
||||
/// CoreFoundation framework in order to avoid linker errors. Otherwise, errors
|
||||
/// like the following may occur:
|
||||
///
|
||||
/// = note: Undefined symbols for architecture arm64:
|
||||
/// "_CFArrayCreate", referenced from:
|
||||
/// "_CFDataCreate", referenced from:
|
||||
/// "_CFRelease", referenced from:
|
||||
/// etc.
|
||||
fn setup_watchos_simulator_workaround() {
|
||||
let target = env::var("TARGET").expect("TARGET not set");
|
||||
if target.ends_with("watchos-sim") {
|
||||
println!("cargo:rustc-link-arg=-framework");
|
||||
println!("cargo:rustc-link-arg=CoreFoundation");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 =
|
||||
@@ -58,6 +75,7 @@ fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
setup_watchos_simulator_workaround();
|
||||
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
|
||||
|
||||
let git_config = GitclBuilder::default().sha(true).build()?;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
namespace matrix_sdk_ffi {};
|
||||
|
||||
[Remote]
|
||||
dictionary Mentions {
|
||||
sequence<string> user_ids;
|
||||
boolean room;
|
||||
};
|
||||
|
||||
[Remote]
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
@@ -10,11 +10,13 @@ use anyhow::{anyhow, Context as _};
|
||||
use futures_util::pin_mut;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use matrix_sdk::STATE_STORE_DATABASE_NAME;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
event_cache::EventCacheError,
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
|
||||
ruma::{
|
||||
api::client::{
|
||||
@@ -39,8 +41,7 @@ use matrix_sdk::{
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
STATE_STORE_DATABASE_NAME,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
@@ -73,13 +74,14 @@ use ruma::{
|
||||
join_rules::{
|
||||
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
|
||||
},
|
||||
message::OriginalSyncRoomMessageEvent,
|
||||
message::{OriginalSyncRoomMessageEvent, Relation},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
},
|
||||
tag::TagEventContent,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
@@ -101,10 +103,13 @@ use crate::{
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClient,
|
||||
notification::{
|
||||
NotificationClient, NotificationEvent, NotificationItem, NotificationRoomInfo,
|
||||
NotificationSenderInfo,
|
||||
},
|
||||
notification_settings::NotificationSettings,
|
||||
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
|
||||
room::{RoomHistoryVisibility, RoomInfoListener},
|
||||
qr_code::{GrantLoginWithQrCodeHandler, LoginWithQrCodeHandler},
|
||||
room::{RoomHistoryVisibility, RoomInfoListener, RoomSendQueueUpdate},
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{
|
||||
@@ -196,6 +201,13 @@ pub trait ProgressWatcher: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn transmission_progress(&self, progress: TransmissionProgress);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) update reporter of the send queue.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueRoomUpdateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called every time the send queue emits an update for a given room.
|
||||
fn on_update(&self, room_id: String, update: RoomSendQueueUpdate);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) error reporter of the send queue.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueRoomErrorListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
@@ -218,6 +230,16 @@ pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_change(&self, event: RoomAccountDataEvent, room_id: String);
|
||||
}
|
||||
|
||||
/// A listener for notifications generated from sync responses.
|
||||
///
|
||||
/// This is called during sync for each event that triggers a notification
|
||||
/// based on the user's push rules.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SyncNotificationListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called when a notifying event is received during sync.
|
||||
fn on_notification(&self, notification: NotificationItem, room_id: String);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct TransmissionProgress {
|
||||
pub current: u64,
|
||||
@@ -236,13 +258,18 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Client {
|
||||
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
|
||||
|
||||
delegate: OnceLock<Arc<dyn ClientDelegate>>,
|
||||
|
||||
pub(crate) utd_hook_manager: OnceLock<Arc<UtdHookManager>>,
|
||||
|
||||
session_verification_controller:
|
||||
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
|
||||
|
||||
/// The path to the directory where the state store and the crypto store are
|
||||
/// located, if the `Client` instance has been built with a SQLite store
|
||||
/// backend.
|
||||
/// located, if the `Client` instance has been built with a store (either
|
||||
/// SQLite or IndexedDB).
|
||||
#[cfg_attr(not(feature = "sqlite"), allow(unused))]
|
||||
store_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -332,6 +359,17 @@ impl Client {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Perform database optimizations if any are available, i.e. vacuuming in
|
||||
/// SQLite.
|
||||
pub async fn optimize_stores(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.optimize_stores().await?)
|
||||
}
|
||||
|
||||
/// Returns the sizes of the existing stores, if known.
|
||||
pub async fn get_store_sizes(&self) -> Result<StoreSizes, ClientError> {
|
||||
Ok(self.inner.get_store_sizes().await?.into())
|
||||
}
|
||||
|
||||
/// Information about login options for the client's homeserver.
|
||||
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
|
||||
let oauth = self.inner.oauth();
|
||||
@@ -544,43 +582,24 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Log in using the provided [`QrCodeData`]. The `Client` must be built
|
||||
/// by providing [`QrCodeData::server_name`] as the server name for this
|
||||
/// login to succeed.
|
||||
/// Create a handler for requesting an existing device to grant login to
|
||||
/// this device by way of a QR code.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such
|
||||
/// this method requires OAuth 2.0 support as well as sliding sync support.
|
||||
/// # Arguments
|
||||
///
|
||||
/// The usage of the progress_listener is required to transfer the
|
||||
/// [`CheckCode`] to the existing client.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn login_with_qr_code(
|
||||
/// * `oidc_configuration` - The data to restore or register the client with
|
||||
/// the server.
|
||||
pub fn new_login_with_qr_code_handler(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
oidc_configuration: OidcConfiguration,
|
||||
) -> LoginWithQrCodeHandler {
|
||||
LoginWithQrCodeHandler::new(self.inner.oauth(), oidc_configuration)
|
||||
}
|
||||
|
||||
let oauth = self.inner.oauth();
|
||||
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data));
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
/// Create a handler for granting login from this device to a new device by
|
||||
/// way of a QR code.
|
||||
pub fn new_grant_login_with_qr_code_handler(self: Arc<Self>) -> GrantLoginWithQrCodeHandler {
|
||||
GrantLoginWithQrCodeHandler::new(self.inner.oauth())
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
@@ -634,6 +653,49 @@ impl Client {
|
||||
self.inner.send_queue().enable_upload_progress(enable);
|
||||
}
|
||||
|
||||
/// Subscribe to the global send queue update reporter, at the
|
||||
/// client-wide level.
|
||||
///
|
||||
/// The given listener will be immediately called with
|
||||
/// `RoomSendQueueUpdate::NewLocalEvent` for each local echo existing in
|
||||
/// the queue.
|
||||
pub async fn subscribe_to_send_queue_updates(
|
||||
&self,
|
||||
listener: Box<dyn SendQueueRoomUpdateListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let q = self.inner.send_queue();
|
||||
let local_echoes = q.local_echoes().await?;
|
||||
let mut subscriber = q.subscribe();
|
||||
|
||||
for (room_id, local_echoes) in local_echoes {
|
||||
for local_echo in local_echoes {
|
||||
listener.on_update(
|
||||
room_id.clone().into(),
|
||||
RoomSendQueueUpdate::NewLocalEvent {
|
||||
transaction_id: local_echo.transaction_id.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(update) => {
|
||||
let room_id = update.room_id.to_string();
|
||||
match update.update.try_into() {
|
||||
Ok(update) => listener.on_update(room_id, update),
|
||||
Err(err) => error!("error when converting send queue update: {err}"),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("error when listening to the send queue update reporter: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Subscribe to the global enablement status of the send queue, at the
|
||||
/// client-wide level.
|
||||
///
|
||||
@@ -789,6 +851,153 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a handler for notifications generated from sync responses.
|
||||
///
|
||||
/// The handler will be called during sync for each event that triggers
|
||||
/// a notification based on the user's push rules.
|
||||
///
|
||||
/// The handler receives:
|
||||
/// - The notification with push actions and event data
|
||||
/// - The room ID where the notification occurred
|
||||
///
|
||||
/// This is useful for implementing custom notification logic, such as
|
||||
/// displaying local notifications or updating notification badges.
|
||||
pub async fn register_notification_handler(&self, listener: Box<dyn SyncNotificationListener>) {
|
||||
let listener = Arc::new(listener);
|
||||
self.inner
|
||||
.register_notification_handler(move |notification, room, _client| {
|
||||
let listener = listener.clone();
|
||||
let room_id = room.room_id().to_string();
|
||||
|
||||
async move {
|
||||
// Extract information about the actions
|
||||
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
|
||||
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
|
||||
|
||||
// Convert SDK actions to FFI type
|
||||
let actions: Vec<crate::notification_settings::Action> = notification
|
||||
.actions
|
||||
.into_iter()
|
||||
.filter_map(|action| action.try_into().ok())
|
||||
.collect();
|
||||
|
||||
// Convert SDK event to FFI type
|
||||
let (sender, event, thread_id) = match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let thread_id = match &deserialized {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
match event.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(
|
||||
content,
|
||||
)) => match content.relates_to {
|
||||
Some(Relation::Thread(thread)) => {
|
||||
Some(thread.event_id.to_string())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let event = NotificationEvent::Timeline {
|
||||
event: Arc::new(crate::event::TimelineEvent(Box::new(
|
||||
deserialized,
|
||||
))),
|
||||
};
|
||||
(sender, event, thread_id)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to deserialize timeline event: {err}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let event =
|
||||
NotificationEvent::Invite { sender: sender.to_string() };
|
||||
let thread_id = None;
|
||||
(sender, event, thread_id)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to deserialize stripped state event: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Compile sender info
|
||||
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
|
||||
let sender_info = if let Some(sender) = sender.as_ref() {
|
||||
NotificationSenderInfo {
|
||||
display_name: sender.display_name().map(|name| name.to_owned()),
|
||||
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
|
||||
is_name_ambiguous: sender.name_ambiguous(),
|
||||
}
|
||||
} else {
|
||||
NotificationSenderInfo {
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
is_name_ambiguous: false,
|
||||
}
|
||||
};
|
||||
|
||||
// Compile room info
|
||||
let display_name = match room.display_name().await {
|
||||
Ok(name) => name.to_string(),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to calculate the room's display name: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let is_direct = match room.is_direct().await {
|
||||
Ok(is_direct) => is_direct,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to determine if room is direct or not: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let room_info = NotificationRoomInfo {
|
||||
display_name,
|
||||
avatar_url: room.avatar_url().map(Into::into),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
topic: room.topic(),
|
||||
join_rule: room
|
||||
.join_rule()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
is_encrypted: Some(room.encryption_state().is_encrypted()),
|
||||
is_direct,
|
||||
is_space: room.is_space(),
|
||||
};
|
||||
|
||||
listener.on_notification(
|
||||
NotificationItem {
|
||||
event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
has_mention: Some(has_mention),
|
||||
thread_id,
|
||||
actions: Some(actions),
|
||||
},
|
||||
room_id,
|
||||
);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Allows generic GET requests to be made through the SDK's internal HTTP
|
||||
/// client. This is useful when the caller's native HTTP client wouldn't
|
||||
/// have the same configuration (such as certificates, proxies, etc.) This
|
||||
@@ -811,15 +1020,23 @@ impl Client {
|
||||
|
||||
/// Empty the server version and unstable features cache.
|
||||
///
|
||||
/// Since the SDK caches server info (versions, unstable features,
|
||||
/// well-known etc), it's possible to have a stale entry in the cache.
|
||||
/// This functions makes it possible to force reset it.
|
||||
pub async fn reset_server_info(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_server_info().await?)
|
||||
/// Since the SDK caches the supported versions, it's possible to have a
|
||||
/// stale entry in the cache. This functions makes it possible to force
|
||||
/// reset it.
|
||||
pub async fn reset_supported_versions(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_supported_versions().await?)
|
||||
}
|
||||
|
||||
/// Empty the well-known cache.
|
||||
///
|
||||
/// Since the SDK caches the well-known, it's possible to have a stale
|
||||
/// entry in the cache. This functions makes it possible to force reset
|
||||
/// it.
|
||||
pub async fn reset_well_known(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_well_known().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Retrieves a media file from the media source
|
||||
@@ -833,22 +1050,60 @@ impl Client {
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
) -> Result<Arc<MediaFileHandle>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters {
|
||||
source: source.media_source,
|
||||
format: MediaFormat::File,
|
||||
},
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
}
|
||||
|
||||
/// MediaFileHandle uses SdkMediaFileHandle which requires an
|
||||
/// intermediate TempFile which is not available on wasm
|
||||
/// platforms due to lack of an accessible file system.
|
||||
#[cfg(target_family = "wasm")]
|
||||
Err(ClientError::Generic {
|
||||
msg: "get_media_file is not supported on wasm platforms".to_owned(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
self.inner
|
||||
.account()
|
||||
.set_display_name(Some(name.as_str()))
|
||||
.await
|
||||
.context("Unable to set display name")?;
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
self.inner.account().set_display_name(Some(name.as_str())).await.map_err(|e| {
|
||||
ClientError::Generic {
|
||||
msg: "Unable to set display name".to_owned(),
|
||||
details: Some(e.to_string()),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -984,15 +1239,6 @@ impl Client {
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.account()
|
||||
.set_display_name(Some(name.as_str()))
|
||||
.await
|
||||
.context("Unable to set display name")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner.account().upload_avatar(&mime, data).await?;
|
||||
@@ -1510,8 +1756,8 @@ impl Client {
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, EventCacheError> {
|
||||
let store = self.inner.event_cache_store().lock().await?;
|
||||
let closure = async || -> Result<_, Error> {
|
||||
let store = self.inner.media_store().lock().await?;
|
||||
Ok(store.set_media_retention_policy(policy).await?)
|
||||
};
|
||||
|
||||
@@ -1559,13 +1805,13 @@ impl Client {
|
||||
|
||||
// Clean up the media cache according to the current media retention policy.
|
||||
self.inner
|
||||
.event_cache_store()
|
||||
.media_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?
|
||||
.clean_up_media_cache()
|
||||
.map_err(Error::from)?
|
||||
.clean()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?;
|
||||
.map_err(Error::from)?;
|
||||
|
||||
// Clear all the room chunks. It's important to *not* call
|
||||
// `EventCacheStore::clear_all_linked_chunks` here, because there might be live
|
||||
@@ -1574,6 +1820,7 @@ impl Client {
|
||||
self.inner.event_cache().clear_all_rooms().await?;
|
||||
|
||||
// Delete the state store file, if it exists.
|
||||
#[cfg(feature = "sqlite")]
|
||||
if let Some(store_path) = &self.store_path {
|
||||
debug!("Removing the state store: {}", store_path.display());
|
||||
|
||||
@@ -1748,6 +1995,42 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
mod recent_emoji {
|
||||
use crate::{client::Client, error::ClientError};
|
||||
|
||||
/// Represents an emoji recently used for reactions.
|
||||
#[derive(Debug, uniffi::Record)]
|
||||
pub struct RecentEmoji {
|
||||
/// The actual emoji text representation.
|
||||
pub emoji: String,
|
||||
/// The number of times this emoji has been used for reactions.
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Adds a recently used emoji to the list and uploads the updated
|
||||
/// `io.element.recent_emoji` content to the global account data.
|
||||
pub async fn add_recent_emoji(&self, emoji: String) -> Result<(), ClientError> {
|
||||
Ok(self.inner.account().add_recent_emoji(&emoji).await?)
|
||||
}
|
||||
|
||||
/// Gets the list of recently used emojis from the
|
||||
/// `io.element.recent_emoji` global account data.
|
||||
pub async fn get_recent_emojis(&self) -> Result<Vec<RecentEmoji>, ClientError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.account()
|
||||
.get_recent_emojis(false)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(emoji, count)| RecentEmoji { emoji, count: count.into() })
|
||||
.collect::<Vec<RecentEmoji>>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait MediaPreviewConfigListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_change(&self, media_preview_config: Option<MediaPreviewConfig>);
|
||||
@@ -2048,24 +2331,24 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
if value.is_encrypted {
|
||||
let content =
|
||||
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
|
||||
}
|
||||
|
||||
if let Some(url) = value.avatar {
|
||||
let mut content = RoomAvatarEventContent::new();
|
||||
content.url = Some(url.into());
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(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());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
|
||||
}
|
||||
|
||||
if let Some(history_visibility_override) = value.history_visibility_override {
|
||||
let content =
|
||||
RoomHistoryVisibilityEventContent::new(history_visibility_override.try_into()?);
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
|
||||
}
|
||||
|
||||
request.initial_state = initial_state;
|
||||
@@ -2307,25 +2590,25 @@ fn gen_transaction_id() -> String {
|
||||
|
||||
/// A file handle that takes ownership of a media file on disk. When the handle
|
||||
/// is dropped, the file will be removed from the disk.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct MediaFileHandle {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
inner: std::sync::RwLock<Option<SdkMediaFileHandle>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl MediaFileHandle {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn new(handle: SdkMediaFileHandle) -> Self {
|
||||
Self { inner: std::sync::RwLock::new(Some(handle)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaFileHandle {
|
||||
/// Get the media file's path.
|
||||
pub fn path(&self) -> Result<String, ClientError> {
|
||||
Ok(self
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
return Ok(self
|
||||
.inner
|
||||
.read()
|
||||
.unwrap()
|
||||
@@ -2334,24 +2617,37 @@ impl MediaFileHandle {
|
||||
.path()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned())
|
||||
.to_owned());
|
||||
#[cfg(target_family = "wasm")]
|
||||
Err(ClientError::Generic {
|
||||
msg: "MediaFileHandle.path() is not supported on WASM targets".to_string(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn persist(&self, path: String) -> Result<bool, ClientError> {
|
||||
let mut guard = self.inner.write().unwrap();
|
||||
Ok(
|
||||
match guard
|
||||
.take()
|
||||
.context("MediaFileHandle was already persisted")?
|
||||
.persist(path.as_ref())
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
*guard = Some(e.file);
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut guard = self.inner.write().unwrap();
|
||||
Ok(
|
||||
match guard
|
||||
.take()
|
||||
.context("MediaFileHandle was already persisted")?
|
||||
.persist(path.as_ref())
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
*guard = Some(e.file);
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
Err(ClientError::Generic {
|
||||
msg: "MediaFileHandle.persist() is not supported on WASM targets".to_string(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2566,3 +2862,28 @@ impl TryFrom<RumaAllowRule> for AllowRule {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the disk size of the different stores, if known. It won't be
|
||||
/// available for in-memory stores.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct StoreSizes {
|
||||
/// The size of the CryptoStore.
|
||||
crypto_store: Option<u64>,
|
||||
/// The size of the StateStore.
|
||||
state_store: Option<u64>,
|
||||
/// The size of the EventCacheStore.
|
||||
event_cache_store: Option<u64>,
|
||||
/// The size of the MediaStore.
|
||||
media_store: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::StoreSizes> for StoreSizes {
|
||||
fn from(value: matrix_sdk::StoreSizes) -> Self {
|
||||
Self {
|
||||
crypto_store: value.crypto_store.map(|v| v as u64),
|
||||
state_store: value.state_store.map(|v| v as u64),
|
||||
event_cache_store: value.event_cache_store.map(|v| v as u64),
|
||||
media_store: value.media_store.map(|v| v as u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
// Allow UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
crypto::{CollectStrategy, DecryptionSettings, TrustRequirement},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
ruma::{ServerName, UserId},
|
||||
@@ -12,14 +14,21 @@ use matrix_sdk::{
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, SqliteStoreConfig, ThreadingSupport,
|
||||
RumaApiError, ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
use zeroize::Zeroizing;
|
||||
use tracing::debug;
|
||||
|
||||
use super::client::Client;
|
||||
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
|
||||
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
|
||||
use crate::store;
|
||||
use crate::{
|
||||
client::ClientSessionDelegate,
|
||||
error::ClientError,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
store::{StoreBuilder, StoreBuilderOutcome},
|
||||
};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
pub type CertificateBytes = Vec<u8>;
|
||||
@@ -100,11 +109,7 @@ impl From<ClientError> for ClientBuildError {
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ClientBuilder {
|
||||
session_paths: Option<SessionPaths>,
|
||||
session_passphrase: Zeroizing<Option<String>>,
|
||||
session_pool_max_size: Option<usize>,
|
||||
session_cache_size: Option<u32>,
|
||||
session_journal_size_limit: Option<u32>,
|
||||
store: Option<StoreBuilder>,
|
||||
system_is_memory_constrained: bool,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
@@ -143,23 +148,24 @@ impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
session_paths: None,
|
||||
session_passphrase: Zeroizing::new(None),
|
||||
session_pool_max_size: None,
|
||||
session_cache_size: None,
|
||||
session_journal_size_limit: None,
|
||||
store: None,
|
||||
system_is_memory_constrained: false,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
user_agent: None,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
proxy: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
cross_process_store_locks_holder_name: None,
|
||||
enable_oidc_refresh_lock: false,
|
||||
session_delegate: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Default::default(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_built_in_root_certificates: false,
|
||||
encryption_settings: EncryptionSettings {
|
||||
auto_enable_cross_signing: false,
|
||||
@@ -201,80 +207,13 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Sets the paths that the client will use to store its data and caches.
|
||||
/// Both paths **must** be unique per session as the SDK stores aren't
|
||||
/// capable of handling multiple users, however it is valid to use the
|
||||
/// same path for both stores on a single session.
|
||||
///
|
||||
/// Leaving this unset tells the client to use an in-memory data store.
|
||||
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_paths = Some(SessionPaths { data_path, cache_path });
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the passphrase for the stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
pub fn session_passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_passphrase = Zeroizing::new(passphrase);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the pool max size for the SQLite stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store exposes an async pool of connections. This method controls
|
||||
/// the size of the pool. The larger the pool is, the more memory is
|
||||
/// consumed, but also the more the app is reactive because it doesn't need
|
||||
/// to wait on a pool to be available to run queries.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
|
||||
pub fn session_pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_pool_max_size = pool_max_size
|
||||
.map(|size| size.try_into().expect("`pool_max_size` is too large to fit in `usize`"));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the cache size for the SQLite stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store exposes a SQLite connection. This method controls the cache
|
||||
/// size, in **bytes (!)**.
|
||||
///
|
||||
/// The cache represents data SQLite holds in memory at once per open
|
||||
/// database file. The default cache implementation does not allocate the
|
||||
/// full amount of cache memory all at once. Cache memory is allocated
|
||||
/// in smaller chunks on an as-needed basis.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::cache_size`] to learn more.
|
||||
pub fn session_cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_cache_size = cache_size;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the size limit for the SQLite WAL files of stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store uses the WAL journal mode. This method controls the size
|
||||
/// limit of the WAL files, in **bytes (!)**.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
|
||||
pub fn session_journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_journal_size_limit = limit;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Tell the client that the system is memory constrained, like in a push
|
||||
/// notification process for example.
|
||||
///
|
||||
/// So far, at the time of writing (2025-04-07), it changes the defaults of
|
||||
/// [`SqliteStoreConfig`], so one might not need to call
|
||||
/// [`ClientBuilder::session_cache_size`] and siblings for example. Please
|
||||
/// check [`SqliteStoreConfig::with_low_memory_config`].
|
||||
/// `matrix_sdk::SqliteStoreConfig` (if the `sqlite` feature is enabled).
|
||||
/// Please check
|
||||
/// `matrix_sdk::SqliteStoreConfig::with_low_memory_config`.
|
||||
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.system_is_memory_constrained = true;
|
||||
@@ -404,6 +343,13 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Use in-memory session storage.
|
||||
pub fn in_memory_store(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::InMemory);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
@@ -413,48 +359,26 @@ impl ClientBuilder {
|
||||
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
|
||||
}
|
||||
|
||||
let store_path = if let Some(session_paths) = &builder.session_paths {
|
||||
// This is the path where both the state store and the crypto store will live.
|
||||
let data_path = Path::new(&session_paths.data_path);
|
||||
// This is the path where the event cache store will live.
|
||||
let cache_path = Path::new(&session_paths.cache_path);
|
||||
let store_path = if let Some(store) = &builder.store {
|
||||
match store.build()? {
|
||||
#[cfg(feature = "sqlite")]
|
||||
StoreBuilderOutcome::Sqlite { config, cache_path, store_path: data_path } => {
|
||||
inner_builder = inner_builder
|
||||
.sqlite_store_with_config_and_cache_path(config, Some(cache_path));
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
event_cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data (state and crypto) and cache stores.",
|
||||
);
|
||||
Some(data_path)
|
||||
}
|
||||
#[cfg(feature = "indexeddb")]
|
||||
StoreBuilderOutcome::IndexedDb { name, passphrase } => {
|
||||
inner_builder = inner_builder.indexeddb_store(&name, passphrase.as_deref());
|
||||
|
||||
fs::create_dir_all(data_path)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
None
|
||||
}
|
||||
|
||||
let mut sqlite_store_config = if builder.system_is_memory_constrained {
|
||||
SqliteStoreConfig::with_low_memory_config(data_path)
|
||||
} else {
|
||||
SqliteStoreConfig::new(data_path)
|
||||
};
|
||||
|
||||
sqlite_store_config =
|
||||
sqlite_store_config.passphrase(builder.session_passphrase.as_deref());
|
||||
|
||||
if let Some(size) = builder.session_pool_max_size {
|
||||
sqlite_store_config = sqlite_store_config.pool_max_size(size);
|
||||
StoreBuilderOutcome::InMemory => None,
|
||||
}
|
||||
|
||||
if let Some(size) = builder.session_cache_size {
|
||||
sqlite_store_config = sqlite_store_config.cache_size(size);
|
||||
}
|
||||
|
||||
if let Some(limit) = builder.session_journal_size_limit {
|
||||
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder
|
||||
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
|
||||
|
||||
Some(data_path.to_owned())
|
||||
} else {
|
||||
debug!("Not using a store path.");
|
||||
debug!("Not using a session store");
|
||||
None
|
||||
};
|
||||
|
||||
@@ -574,21 +498,6 @@ impl ClientBuilder {
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
// Disable retries for this request to prevent it from being retried
|
||||
// indefinitely
|
||||
let config = sdk_client.request_config().disable_retry();
|
||||
|
||||
// Log server version information at info level.
|
||||
if let Ok(server_info) = sdk_client.server_vendor_info(Some(config)).await {
|
||||
tracing::info!(
|
||||
server_name = %server_info.server_name,
|
||||
version = %server_info.version,
|
||||
"Connected to Matrix server"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!("Could not retrieve server version information");
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(
|
||||
sdk_client,
|
||||
@@ -601,18 +510,62 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
/// Use SQLite as the session storage.
|
||||
pub fn sqlite_store(self: Arc<Self>, config: Arc<store::SqliteStoreBuilder>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::Sqlite(unwrap_or_clone_arc(config)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Sets the paths that the client will use to store its data and caches
|
||||
/// with SQLite.
|
||||
///
|
||||
/// Both paths **must** be unique per session as the SDK
|
||||
/// stores aren't capable of handling multiple users, however it is
|
||||
/// valid to use the same path for both stores on a single session.
|
||||
#[deprecated = "Use `ClientBuilder::session_store_with_sqlite` instead"]
|
||||
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store =
|
||||
Some(StoreBuilder::Sqlite(store::SqliteStoreBuilder::raw_new(data_path, cache_path)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
/// Use IndexedDB as the session storage.
|
||||
pub fn indexeddb_store(
|
||||
self: Arc<Self>,
|
||||
config: Arc<store::IndexedDbStoreBuilder>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::IndexedDb(unwrap_or_clone_arc(config)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.proxy = Some(url);
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_ssl_verification = true;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.disable_ssl_verification = true;
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -621,7 +574,11 @@ impl ClientBuilder {
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.additional_root_certificates = certificates;
|
||||
}
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
@@ -631,29 +588,25 @@ impl ClientBuilder {
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.user_agent = Some(user_agent);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.user_agent = Some(user_agent);
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// The store paths the client will use when built.
|
||||
#[derive(Clone)]
|
||||
struct SessionPaths {
|
||||
/// The path that the client will use to store its data.
|
||||
data_path: String,
|
||||
/// The path that the client will use to store its caches. This path can be
|
||||
/// the same as the data path if you prefer to keep everything in one place.
|
||||
cache_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
/// The config to use for HTTP requests by default in this client.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RequestConfig {
|
||||
/// Max number of retries.
|
||||
retry_limit: Option<u64>,
|
||||
|
||||
@@ -79,9 +79,14 @@ pub enum RecoveryError {
|
||||
#[error(transparent)]
|
||||
Client { source: crate::ClientError },
|
||||
|
||||
/// Error in the secret storage subsystem.
|
||||
/// Error in the secret storage subsystem, except for when importing a
|
||||
/// secret.
|
||||
#[error("Error in the secret-storage subsystem: {error_message}")]
|
||||
SecretStorage { error_message: String },
|
||||
|
||||
/// Error when importing a secret from secret storage.
|
||||
#[error("Error importing a secret: {error_message}")]
|
||||
Import { error_message: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
|
||||
@@ -89,6 +94,9 @@ impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
|
||||
match value {
|
||||
recovery::RecoveryError::BackupExistsOnServer => Self::BackupExistsOnServer,
|
||||
recovery::RecoveryError::Sdk(e) => Self::Client { source: ClientError::from(e) },
|
||||
recovery::RecoveryError::SecretStorage(
|
||||
matrix_sdk::encryption::secret_storage::SecretStorageError::ImportError { .. },
|
||||
) => Self::Import { error_message: value.to_string() },
|
||||
recovery::RecoveryError::SecretStorage(e) => {
|
||||
Self::SecretStorage { error_message: e.to_string() }
|
||||
}
|
||||
@@ -287,6 +295,15 @@ impl Encryption {
|
||||
Ok(self.inner.recovery().is_last_device().await?)
|
||||
}
|
||||
|
||||
/// Does the user have other devices that the current device can verify
|
||||
/// against?
|
||||
///
|
||||
/// The device must be signed by the user's cross-signing key, must have an
|
||||
/// identity, and must not be a dehydrated device.
|
||||
pub async fn has_devices_to_verify_against(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.has_devices_to_verify_against().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
&self,
|
||||
progress_listener: Option<Box<dyn BackupSteadyStateListener>>,
|
||||
@@ -417,11 +434,13 @@ impl Encryption {
|
||||
/// 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.
|
||||
/// be made to the homeserver unless `fallback_to_server` is set to `false`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user that the identity belongs to.
|
||||
/// * `fallback_to_server` - Should we request the user identity from the
|
||||
/// homeserver if one isn't found locally.
|
||||
///
|
||||
/// 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
|
||||
@@ -431,6 +450,7 @@ impl Encryption {
|
||||
pub async fn user_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
fallback_to_server: bool,
|
||||
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
|
||||
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
|
||||
Ok(Some(identity)) => {
|
||||
@@ -446,8 +466,12 @@ impl Encryption {
|
||||
|
||||
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 })))
|
||||
if fallback_to_server {
|
||||
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
|
||||
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ use matrix_sdk::{
|
||||
encryption::{identities::RequestVerificationError, CryptoStoreError},
|
||||
event_cache::EventCacheError,
|
||||
reqwest,
|
||||
room::edit::EditError,
|
||||
room::{calls::CallError, 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 matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
|
||||
use ruma::{
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter},
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use tracing::warn;
|
||||
@@ -64,7 +64,9 @@ impl From<matrix_sdk::Error> for ClientError {
|
||||
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 {
|
||||
if let ErrorBody::Standard(StandardErrorBody { 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
|
||||
@@ -187,6 +189,12 @@ impl From<EditError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallError> for ClientError {
|
||||
fn from(e: CallError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomSendQueueError> for ClientError {
|
||||
fn from(e: RoomSendQueueError) -> Self {
|
||||
Self::from_err(e)
|
||||
@@ -211,6 +219,12 @@ impl From<RequestVerificationError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<spaces::Error> for ClientError {
|
||||
fn from(e: spaces::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
encrypted,
|
||||
message::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
@@ -18,7 +17,7 @@ use ruma::{
|
||||
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, NotifyType},
|
||||
ruma::{MessageType, RtcNotificationType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
@@ -41,7 +40,7 @@ impl TimelineEvent {
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
let event_type = match self.0.deref() {
|
||||
let event_type = match &*self.0 {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
TimelineEventType::MessageLike { content: event.clone().try_into()? }
|
||||
}
|
||||
@@ -51,6 +50,20 @@ impl TimelineEvent {
|
||||
};
|
||||
Ok(event_type)
|
||||
}
|
||||
|
||||
/// Returns the thread root event id for the event, if it's part of a
|
||||
/// thread.
|
||||
pub fn thread_root_event_id(&self) -> Option<String> {
|
||||
match &*self.0 {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
match event.original_content().and_then(|content| content.relation()) {
|
||||
Some(encrypted::Relation::Thread(thread)) => Some(thread.event_id.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
AnySyncTimelineEvent::State(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyTimelineEvent> for TimelineEvent {
|
||||
@@ -153,7 +166,11 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
pub enum MessageLikeEventContent {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
CallNotify { notify_type: NotifyType },
|
||||
RtcNotification {
|
||||
notification_type: RtcNotificationType,
|
||||
/// The timestamp at which this notification is considered invalid.
|
||||
expiration_ts: Timestamp,
|
||||
},
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
@@ -163,11 +180,21 @@ pub enum MessageLikeEventContent {
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationDone,
|
||||
Poll { question: String },
|
||||
ReactionContent { related_event_id: String },
|
||||
Poll {
|
||||
question: String,
|
||||
},
|
||||
ReactionContent {
|
||||
related_event_id: String,
|
||||
},
|
||||
RoomEncrypted,
|
||||
RoomMessage { message_type: MessageType, in_reply_to_event_id: Option<String> },
|
||||
RoomRedaction { redacted_event_id: Option<String>, reason: Option<String> },
|
||||
RoomMessage {
|
||||
message_type: MessageType,
|
||||
in_reply_to_event_id: Option<String>,
|
||||
},
|
||||
RoomRedaction {
|
||||
redacted_event_id: Option<String>,
|
||||
reason: Option<String>,
|
||||
},
|
||||
Sticker,
|
||||
}
|
||||
|
||||
@@ -178,10 +205,13 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
let content = match value {
|
||||
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
|
||||
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
|
||||
AnySyncMessageLikeEvent::CallNotify(content) => {
|
||||
let original_content = get_message_like_event_original_content(content)?;
|
||||
MessageLikeEventContent::CallNotify {
|
||||
notify_type: original_content.notify_type.into(),
|
||||
AnySyncMessageLikeEvent::RtcNotification(event) => {
|
||||
let origin_server_ts = event.origin_server_ts();
|
||||
let original_content = get_message_like_event_original_content(event)?;
|
||||
let expiration_ts = original_content.expiration_ts(origin_server_ts, None).into();
|
||||
MessageLikeEventContent::RtcNotification {
|
||||
notification_type: original_content.notification_type.into(),
|
||||
expiration_ts,
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
|
||||
@@ -331,7 +361,7 @@ pub enum MessageLikeEventType {
|
||||
CallCandidates,
|
||||
CallHangup,
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
RtcNotification,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationDone,
|
||||
@@ -350,6 +380,7 @@ pub enum MessageLikeEventType {
|
||||
UnstablePollEnd,
|
||||
UnstablePollResponse,
|
||||
UnstablePollStart,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
@@ -357,7 +388,7 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
match val {
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallNotify => Self::CallNotify,
|
||||
MessageLikeEventType::RtcNotification => Self::RtcNotification,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
@@ -378,6 +409,7 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
|
||||
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
|
||||
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
|
||||
MessageLikeEventType::Other(msgtype) => Self::from(msgtype),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::crypto::IdentityState;
|
||||
use matrix_sdk_base::crypto::IdentityState;
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct IdentityStatusChange {
|
||||
|
||||
@@ -26,6 +26,7 @@ mod ruma;
|
||||
mod runtime;
|
||||
mod session_verification;
|
||||
mod spaces;
|
||||
mod store;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct NotificationRoomInfo {
|
||||
pub joined_members_count: u64,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_space: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -51,6 +52,9 @@ pub struct NotificationItem {
|
||||
pub is_noisy: Option<bool>,
|
||||
pub has_mention: Option<bool>,
|
||||
pub thread_id: Option<String>,
|
||||
|
||||
/// The push actions for this notification (notify, sound, highlight, etc.).
|
||||
pub actions: Option<Vec<crate::notification_settings::Action>>,
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
@@ -79,10 +83,14 @@ impl NotificationItem {
|
||||
joined_members_count: item.joined_members_count,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_space: item.is_space,
|
||||
},
|
||||
is_noisy: item.is_noisy,
|
||||
has_mention: item.has_mention,
|
||||
thread_id: item.thread_id.map(|t| t.to_string()),
|
||||
actions: item
|
||||
.actions
|
||||
.map(|a| a.into_iter().filter_map(|action| action.try_into().ok()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use matrix_sdk::{
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
events::push_rules::PushRulesEventContent,
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
|
||||
@@ -20,7 +21,7 @@ use ruma::{
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
|
||||
use crate::error::NotificationSettingsError;
|
||||
use crate::error::{ClientError, NotificationSettingsError};
|
||||
|
||||
#[derive(Clone, Default, uniffi::Enum)]
|
||||
pub enum ComparisonOperator {
|
||||
@@ -167,6 +168,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
#[allow(deprecated)]
|
||||
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
SdkPushCondition::RoomMemberCount { is } => {
|
||||
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
|
||||
@@ -189,6 +191,7 @@ impl From<PushCondition> for SdkPushCondition {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
#[allow(deprecated)]
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
|
||||
is: RoomMemberCountIs {
|
||||
@@ -770,4 +773,11 @@ impl NotificationSettings {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the raw push rules in JSON format.
|
||||
pub async fn get_raw_push_rules(&self) -> Result<Option<String>, ClientError> {
|
||||
let raw_push_rules =
|
||||
self.sdk_client.account().account_data::<PushRulesEventContent>().await?;
|
||||
Ok(raw_push_rules.map(|raw| serde_json::to_string(&raw)).transpose()?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing::warn;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing_core::Level;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
field::RecordFields,
|
||||
@@ -21,6 +23,8 @@ use tracing_subscriber::{
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use crate::tracing::BRIDGE_SPAN_NAME;
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
@@ -274,7 +278,8 @@ enum LogTarget {
|
||||
MatrixSdkBaseResponseProcessors,
|
||||
|
||||
// SDK common modules.
|
||||
MatrixSdkCommonStoreLocks,
|
||||
MatrixSdkCommonCrossProcessLock,
|
||||
MatrixSdkCommonDeserializedResponses,
|
||||
|
||||
// SDK modules.
|
||||
MatrixSdk,
|
||||
@@ -302,7 +307,10 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
|
||||
LogTarget::MatrixSdkBaseResponseProcessors => "matrix_sdk_base::response_processors",
|
||||
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock => "matrix_sdk_common::cross_process_lock",
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses => {
|
||||
"matrix_sdk_common::deserialized_responses"
|
||||
}
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
|
||||
@@ -335,18 +343,19 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkCommonCrossProcessLock, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkCommonDeserializedResponses, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseResponseProcessors, LogLevel::Debug),
|
||||
];
|
||||
|
||||
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
LogTarget::MatrixSdkCommonStoreLocks, // Too verbose
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock, // Too verbose
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
|
||||
];
|
||||
|
||||
/// A log pack can be used to set the trace log level for a group of multiple
|
||||
@@ -374,15 +383,21 @@ impl TraceLogPacks {
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
LogTarget::MatrixSdkBaseEventCache,
|
||||
LogTarget::MatrixSdkEventCacheStore,
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
|
||||
TraceLogPacks::Timeline => {
|
||||
&[LogTarget::MatrixSdkUiTimeline, LogTarget::MatrixSdkCommonDeserializedResponses]
|
||||
}
|
||||
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
|
||||
TraceLogPacks::SyncProfiling => &[
|
||||
LogTarget::MatrixSdkSlidingSync,
|
||||
LogTarget::MatrixSdkBaseSlidingSync,
|
||||
LogTarget::MatrixSdkBaseResponseProcessors,
|
||||
LogTarget::MatrixSdkCrypto,
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -454,7 +469,14 @@ impl TracingConfiguration {
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sample_rate: 0.0,
|
||||
traces_sampler: Some(Arc::new(|ctx| {
|
||||
// Make sure bridge spans are always uploaded
|
||||
if ctx.name() == BRIDGE_SPAN_NAME {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})),
|
||||
attach_stacktrace: true,
|
||||
release: Some(env!("VERGEN_GIT_SHA").into()),
|
||||
..sentry::ClientOptions::default()
|
||||
@@ -488,7 +510,10 @@ impl TracingConfiguration {
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
sentry_tracing::default_span_filter(metadata)
|
||||
matches!(
|
||||
metadata.level(),
|
||||
&Level::ERROR | &Level::WARN | &Level::INFO | &Level::DEBUG
|
||||
)
|
||||
} else {
|
||||
// Ignore, if sentry is globally disabled.
|
||||
false
|
||||
@@ -723,7 +748,8 @@ mod tests {
|
||||
matrix_sdk::event_cache=info,
|
||||
matrix_sdk_base::event_cache=info,
|
||||
matrix_sdk_sqlite::event_cache_store=info,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
@@ -767,7 +793,8 @@ mod tests {
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=trace,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=trace,
|
||||
matrix_sdk_base::response_processors=trace,
|
||||
@@ -812,7 +839,8 @@ mod tests {
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=trace,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
|
||||
@@ -1,11 +1,221 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
qrcode::{
|
||||
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
|
||||
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
|
||||
},
|
||||
OAuth,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use tracing::error;
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
/// Handler for logging in with a QR code.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LoginWithQrCodeHandler {
|
||||
oauth: OAuth,
|
||||
oidc_configuration: OidcConfiguration,
|
||||
}
|
||||
|
||||
impl LoginWithQrCodeHandler {
|
||||
pub(crate) fn new(oauth: OAuth, oidc_configuration: OidcConfiguration) -> Self {
|
||||
Self { oauth, oidc_configuration }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LoginWithQrCodeHandler {
|
||||
/// This method allows you to log in with a scanned QR code.
|
||||
///
|
||||
/// The existing device needs to display the QR code which this device can
|
||||
/// scan, call this method and handle its progress updates to log in.
|
||||
///
|
||||
/// For the login to succeed, the [`Client`] associated with the
|
||||
/// [`LoginWithQrCodeHandler`] must have been built with
|
||||
/// [`QrCodeData::server_name`] as the server name.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where this device generates the QR code for the
|
||||
/// existing device to scan, use [`LoginWithQrCodeHandler::generate`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// transfer the [`CheckCode`] to the existing device.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn scan(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = self
|
||||
.oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let login =
|
||||
self.oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data.inner);
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This method allows you to log in by generating a QR code.
|
||||
///
|
||||
/// This device needs to call this method and handle its progress updates to
|
||||
/// generate a QR code which the existing device can scan and grant the
|
||||
/// log in.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where the existing device generates the QR code
|
||||
/// for this device to scan, use [`LoginWithQrCodeHandler::scan`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn generate(
|
||||
self: Arc<Self>,
|
||||
progress_listener: Box<dyn GeneratedQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = self
|
||||
.oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let login = self.oauth.login_with_qr_code(Some(®istration_data)).generate();
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for granting login in with a QR code.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct GrantLoginWithQrCodeHandler {
|
||||
oauth: OAuth,
|
||||
}
|
||||
|
||||
impl GrantLoginWithQrCodeHandler {
|
||||
pub(crate) fn new(oauth: OAuth) -> Self {
|
||||
Self { oauth }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl GrantLoginWithQrCodeHandler {
|
||||
/// This method allows you to grant login with a scanned QR code.
|
||||
///
|
||||
/// The new device needs to display the QR code which this device can
|
||||
/// scan, call this method and handle its progress updates to grant the
|
||||
/// login.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where this device generates the QR code for the
|
||||
/// existing device to scan, use [`GrantLoginWithQrCodeHandler::generate`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// transfer the [`CheckCode`] to the new device.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn scan(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
progress_listener: Box<dyn GrantQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrGrantLoginError> {
|
||||
let grant = self.oauth.grant_login_with_qr_code().scan(&qr_code_data.inner);
|
||||
|
||||
let mut progress = grant.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
grant.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This method allows you to grant login by generating a QR code.
|
||||
///
|
||||
/// This device needs to call this method and handle its progress updates to
|
||||
/// generate a QR code which the new device can scan to log in.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where the existing device generates the QR code
|
||||
/// for this device to scan, use [`GrantLoginWithQrCodeHandler::scan`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn generate(
|
||||
self: Arc<Self>,
|
||||
progress_listener: Box<dyn GrantGeneratedQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrGrantLoginError> {
|
||||
let grant = self.oauth.grant_login_with_qr_code().generate();
|
||||
|
||||
let mut progress = grant.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
grant.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for the QR code login mechanism.
|
||||
///
|
||||
@@ -33,8 +243,8 @@ impl QrCodeData {
|
||||
/// will return `None`.
|
||||
pub fn server_name(&self) -> Option<String> {
|
||||
match &self.inner.mode_data {
|
||||
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
|
||||
QrCodeModeData::Login => None,
|
||||
qrcode::QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
|
||||
qrcode::QrCodeModeData::Login => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +256,7 @@ pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: LoginQrCodeDecodeError,
|
||||
error: qrcode::LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,6 +280,12 @@ pub enum HumanQrLoginError {
|
||||
OidcMetadataInvalid,
|
||||
#[error("The other device is not signed in and as such can't sign in other devices.")]
|
||||
OtherDeviceNotSignedIn,
|
||||
#[error("The check code was already sent.")]
|
||||
CheckCodeAlreadySent,
|
||||
#[error("The check code could not be sent.")]
|
||||
CheckCodeCannotBeSent,
|
||||
#[error("The rendezvous session was not found and might have expired")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
@@ -103,7 +319,10 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
| SecureChannelError::CannotReceiveCheckCode => {
|
||||
HumanQrLoginError::ConnectionInsecure
|
||||
}
|
||||
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
|
||||
},
|
||||
|
||||
@@ -112,12 +331,77 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
| QRCodeLoginError::DeviceKeyUpload(_)
|
||||
| QRCodeLoginError::SessionTokens(_)
|
||||
| QRCodeLoginError::UserIdDiscovery(_)
|
||||
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
|
||||
| QRCodeLoginError::SecretImport(_)
|
||||
| QRCodeLoginError::ServerReset(_) => HumanQrLoginError::Unknown,
|
||||
|
||||
QRCodeLoginError::NotFound => HumanQrLoginError::NotFound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of the QR-code login.
|
||||
impl From<CheckCodeSenderError> for HumanQrLoginError {
|
||||
fn from(value: CheckCodeSenderError) -> Self {
|
||||
match value {
|
||||
CheckCodeSenderError::AlreadySent => HumanQrLoginError::CheckCodeAlreadySent,
|
||||
CheckCodeSenderError::CannotSend => HumanQrLoginError::CheckCodeCannotBeSent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum HumanQrGrantLoginError {
|
||||
/// The requested device ID is already in use.
|
||||
#[error("The requested device ID is already in use.")]
|
||||
DeviceIDAlreadyInUse,
|
||||
|
||||
/// The check code was incorrect.
|
||||
#[error("The check code was incorrect.")]
|
||||
InvalidCheckCode,
|
||||
|
||||
/// The other client proposed an unsupported protocol.
|
||||
#[error("Unsupported protocol: {0}")]
|
||||
UnsupportedProtocol(String),
|
||||
|
||||
/// Secrets backup not set up properly.
|
||||
#[error("Secrets backup not set up: {0}")]
|
||||
MissingSecretsBackup(String),
|
||||
|
||||
/// The rendezvous session was not found and might have expired.
|
||||
#[error("The rendezvous session was not found and might have expired")]
|
||||
NotFound,
|
||||
|
||||
/// The device could not be created.
|
||||
#[error("The device could not be created.")]
|
||||
UnableToCreateDevice,
|
||||
|
||||
/// An unknown error has happened.
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
|
||||
fn from(value: qrcode::QRCodeGrantLoginError) -> Self {
|
||||
use qrcode::QRCodeGrantLoginError;
|
||||
|
||||
match value {
|
||||
QRCodeGrantLoginError::DeviceIDAlreadyInUse => Self::DeviceIDAlreadyInUse,
|
||||
QRCodeGrantLoginError::InvalidCheckCode => Self::InvalidCheckCode,
|
||||
QRCodeGrantLoginError::UnableToCreateDevice => Self::UnableToCreateDevice,
|
||||
QRCodeGrantLoginError::UnsupportedProtocol(protocol) => {
|
||||
Self::UnsupportedProtocol(protocol.to_string())
|
||||
}
|
||||
QRCodeGrantLoginError::MissingSecretsBackup(error) => {
|
||||
Self::MissingSecretsBackup(error.map_or("other".to_owned(), |e| e.to_string()))
|
||||
}
|
||||
QRCodeGrantLoginError::NotFound => Self::NotFound,
|
||||
QRCodeGrantLoginError::Unknown(string) => Self::Unknown(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of logging in by scanning a QR code that was
|
||||
/// generated on an existing device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum QrLoginProgress {
|
||||
/// The login process is starting.
|
||||
@@ -136,6 +420,8 @@ pub enum QrLoginProgress {
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
@@ -145,13 +431,13 @@ pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress) -> Self {
|
||||
impl From<qrcode::LoginProgress<QrProgress>> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress<QrProgress>) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel { check_code } => {
|
||||
LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
@@ -160,7 +446,185 @@ impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of logging in by generating a QR code and
|
||||
/// having an existing device scan it.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GeneratedQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We have established the secure channel and now need to display the
|
||||
/// QR code so that the existing device can scan it.
|
||||
QrReady { qr_code: Arc<QrCodeData> },
|
||||
/// The existing device has scanned the QR code and is displaying the
|
||||
/// checkcode. We now need to ask the user to enter the checkcode so that
|
||||
/// we can verify that the channel is indeed secure.
|
||||
QrScanned { check_code_sender: Arc<CheckCodeSender> },
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GeneratedQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress<GeneratedQrProgress>> for GeneratedQrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress<GeneratedQrProgress>) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
|
||||
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
|
||||
}
|
||||
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(inner)) => {
|
||||
Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) }
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of granting login in by scanning a QR code that
|
||||
/// was generated on a new device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GrantQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// The secure channel has been confirmed using the [`CheckCode`] and this
|
||||
/// device is waiting for the authorization to complete.
|
||||
WaitingForAuth {
|
||||
/// A URI to open in a (secure) system browser to verify the new login.
|
||||
verification_uri: String,
|
||||
},
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GrantQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GrantQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::GrantLoginProgress<QrProgress>> for GrantQrLoginProgress {
|
||||
fn from(value: qrcode::GrantLoginProgress<QrProgress>) -> Self {
|
||||
use qrcode::GrantLoginProgress;
|
||||
|
||||
match value {
|
||||
GrantLoginProgress::Starting => Self::Starting,
|
||||
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
GrantLoginProgress::WaitingForAuth { verification_uri } => {
|
||||
Self::WaitingForAuth { verification_uri: verification_uri.into() }
|
||||
}
|
||||
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
GrantLoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of granting login by generating a QR code to
|
||||
/// be scanned on the new device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GrantGeneratedQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We have established the secure channel and now need to display the
|
||||
/// QR code so that the existing device can scan it.
|
||||
QrReady { qr_code: Arc<QrCodeData> },
|
||||
/// The existing device has scanned the QR code and is displaying the
|
||||
/// checkcode. We now need to ask the user to enter the checkcode so that
|
||||
/// we can verify that the channel is indeed secure.
|
||||
QrScanned { check_code_sender: Arc<CheckCodeSender> },
|
||||
/// The secure channel has been confirmed using the [`CheckCode`] and this
|
||||
/// device is waiting for the authorization to complete.
|
||||
WaitingForAuth {
|
||||
/// A URI to open in a (secure) system browser to verify the new login.
|
||||
verification_uri: String,
|
||||
},
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GrantGeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GrantGeneratedQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::GrantLoginProgress<GeneratedQrProgress>> for GrantGeneratedQrLoginProgress {
|
||||
fn from(value: qrcode::GrantLoginProgress<GeneratedQrProgress>) -> Self {
|
||||
use qrcode::GrantLoginProgress;
|
||||
|
||||
match value {
|
||||
GrantLoginProgress::Starting => Self::Starting,
|
||||
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
|
||||
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
|
||||
}
|
||||
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(
|
||||
inner,
|
||||
)) => Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) },
|
||||
GrantLoginProgress::WaitingForAuth { verification_uri } => {
|
||||
Self::WaitingForAuth { verification_uri: verification_uri.into() }
|
||||
}
|
||||
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
GrantLoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
/// Used to pass back the [`CheckCode`] entered by the user to verify that the
|
||||
/// secure channel is indeed secure.
|
||||
pub struct CheckCodeSender {
|
||||
inner: SdkCheckCodeSender,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl CheckCodeSender {
|
||||
/// Send the [`CheckCode`].
|
||||
///
|
||||
/// Calling this method more than once will result in an error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `check_code` - The check code in digits representation.
|
||||
pub async fn send(&self, code: u8) -> Result<(), HumanQrLoginError> {
|
||||
self.inner.send(code).await.map_err(HumanQrLoginError::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
|
||||
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
|
||||
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
SuccessorRoom as SdkSuccessorRoom,
|
||||
};
|
||||
@@ -21,12 +23,12 @@ use mime::Mime;
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
call::notify,
|
||||
receipt::ReceiptThread,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
MediaSource,
|
||||
MediaSource as RumaMediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
@@ -39,16 +41,20 @@ use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
|
||||
event::TimelineEvent,
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{ImageInfo, LocationContent},
|
||||
ruma::{
|
||||
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
|
||||
AbstractProgress, EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
|
||||
UploadSource,
|
||||
},
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
@@ -227,11 +233,8 @@ impl Room {
|
||||
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
if configuration.track_read_receipts {
|
||||
builder = builder.track_read_marker_and_receipts();
|
||||
}
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into())
|
||||
.track_read_marker_and_receipts(configuration.track_read_receipts);
|
||||
|
||||
match configuration.filter {
|
||||
TimelineFilter::All => {
|
||||
@@ -670,6 +673,25 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a room as fully read, by attaching a read receipt to the provided
|
||||
/// `event_id`.
|
||||
///
|
||||
/// **Warning:** using this method is **NOT** recommended, as providing the
|
||||
/// latest event id can cause incorrect read receipts. This method won't
|
||||
/// check if sending the read receipt is necessary or valid. It should
|
||||
/// *only* be used when some constraint prevents you from instantiating a
|
||||
/// [`Timeline`]. For any other case use [`Timeline::mark_as_read`]
|
||||
/// instead.
|
||||
pub async fn mark_as_fully_read_unchecked(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
self.inner
|
||||
.send_single_receipt(ReceiptType::FullyRead.into(), ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
|
||||
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
|
||||
Ok(Arc::new(RoomPowerLevels::new(power_levels, self.inner.own_user_id().to_owned())))
|
||||
@@ -735,6 +757,37 @@ impl Room {
|
||||
self.inner.send_queue().set_enabled(enable);
|
||||
}
|
||||
|
||||
/// Subscribe to all send queue updates in this room.
|
||||
///
|
||||
/// The given listener will be immediately called with
|
||||
/// `RoomSendQueueUpdate::NewLocalEvent` for each local echo existing in
|
||||
/// the queue.
|
||||
pub async fn subscribe_to_send_queue_updates(
|
||||
&self,
|
||||
listener: Box<dyn SendQueueListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let q = self.inner.send_queue();
|
||||
let (local_echoes, mut subscriber) = q.subscribe().await?;
|
||||
|
||||
for local_echo in local_echoes {
|
||||
listener.on_update(RoomSendQueueUpdate::NewLocalEvent {
|
||||
transaction_id: local_echo.transaction_id.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(update) => match update.try_into() {
|
||||
Ok(update) => listener.on_update(update),
|
||||
Err(err) => error!("error when converting send queue update: {err}"),
|
||||
},
|
||||
Err(err) => error!("error when listening for send queue updates: {err}"),
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Store the given `ComposerDraft` in the state store using the current
|
||||
/// room id, as identifier.
|
||||
pub async fn save_composer_draft(
|
||||
@@ -1010,6 +1063,44 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Declines a call (and stop ringing).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `rtc_notification_event_id` - the event id of the m.rtc.notification
|
||||
/// event.
|
||||
pub async fn decline_call(&self, rtc_notification_event_id: String) -> Result<(), ClientError> {
|
||||
let parsed_id = EventId::parse(rtc_notification_event_id.as_str())?;
|
||||
|
||||
let content = self.inner.make_decline_call_event(&parsed_id).await?;
|
||||
|
||||
self.inner.send_queue().send(content.into()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to call decline for a currently ringing call, using a
|
||||
/// `listener` to be notified when someone declines.
|
||||
///
|
||||
/// Will error if `rtc_notification_event_id` is not a valid event id.
|
||||
/// Use the [`TaskHandle`] to cancel the subscription.
|
||||
pub fn subscribe_to_call_decline_events(
|
||||
self: Arc<Self>,
|
||||
rtc_notification_event_id: String,
|
||||
listener: Box<dyn CallDeclineListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let parsed_id = EventId::parse(rtc_notification_event_id.as_str())?;
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let (_event_handler_drop_guard, mut subscriber) =
|
||||
self.inner.subscribe_to_call_decline_events(&parsed_id);
|
||||
|
||||
while let Ok(user_id) = subscriber.recv().await {
|
||||
listener.call(user_id.to_string());
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Subscribes to live location shares in this room, using a `listener` to
|
||||
/// be notified of the changes.
|
||||
///
|
||||
@@ -1141,6 +1232,22 @@ impl Room {
|
||||
.await?
|
||||
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
|
||||
}
|
||||
|
||||
/// Either loads the event associated with the `event_id` from the event
|
||||
/// cache or fetches it from the homeserver.
|
||||
pub async fn load_or_fetch_event(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<TimelineEvent, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let timeline_event = self.inner.load_or_fetch_event(&event_id, None).await?;
|
||||
Ok(timeline_event
|
||||
.kind
|
||||
.into_raw()
|
||||
.deserialize()?
|
||||
.into_full_event(self.inner.room_id().to_owned())
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread subscription (MSC4306).
|
||||
@@ -1157,6 +1264,12 @@ pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
/// A listener for receiving call decline events in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait CallDeclineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, decliner_user_id: String);
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
|
||||
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
|
||||
Self {
|
||||
@@ -1321,8 +1434,8 @@ 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().media_source {
|
||||
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
RumaMediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
RumaMediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -1340,18 +1453,6 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RtcApplicationType {
|
||||
Call,
|
||||
}
|
||||
impl From<RtcApplicationType> for notify::ApplicationType {
|
||||
fn from(value: RtcApplicationType) -> Self {
|
||||
match value {
|
||||
RtcApplicationType::Call => notify::ApplicationType::Call,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current draft of the composer for the room.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ComposerDraft {
|
||||
@@ -1362,21 +1463,257 @@ pub struct ComposerDraft {
|
||||
pub html_text: Option<String>,
|
||||
/// The type of draft.
|
||||
pub draft_type: ComposerDraftType,
|
||||
/// Attachments associated with this draft.
|
||||
pub attachments: Vec<DraftAttachment>,
|
||||
}
|
||||
|
||||
impl From<SdkComposerDraft> for ComposerDraft {
|
||||
fn from(value: SdkComposerDraft) -> Self {
|
||||
let SdkComposerDraft { plain_text, html_text, draft_type } = value;
|
||||
Self { plain_text, html_text, draft_type: draft_type.into() }
|
||||
let SdkComposerDraft { plain_text, html_text, draft_type, attachments } = value;
|
||||
Self {
|
||||
plain_text,
|
||||
html_text,
|
||||
draft_type: draft_type.into(),
|
||||
attachments: attachments.into_iter().map(|a| a.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ComposerDraft> for SdkComposerDraft {
|
||||
type Error = ruma::IdParseError;
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: ComposerDraft) -> std::result::Result<Self, Self::Error> {
|
||||
let ComposerDraft { plain_text, html_text, draft_type } = value;
|
||||
Ok(Self { plain_text, html_text, draft_type: draft_type.try_into()? })
|
||||
let ComposerDraft { plain_text, html_text, draft_type, attachments } = value;
|
||||
Ok(Self {
|
||||
plain_text,
|
||||
html_text,
|
||||
draft_type: draft_type.try_into()?,
|
||||
attachments: attachments
|
||||
.into_iter()
|
||||
.map(|a| a.try_into())
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An attachment stored with a composer draft.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DraftAttachment {
|
||||
Audio { audio_info: AudioInfo, source: UploadSource },
|
||||
File { file_info: FileInfo, source: UploadSource },
|
||||
Image { image_info: ImageInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
|
||||
Video { video_info: VideoInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
|
||||
}
|
||||
|
||||
impl From<SdkDraftAttachment> for DraftAttachment {
|
||||
fn from(value: SdkDraftAttachment) -> Self {
|
||||
match value.content {
|
||||
DraftAttachmentContent::Image {
|
||||
data,
|
||||
mimetype,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
blurhash,
|
||||
thumbnail,
|
||||
} => {
|
||||
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
|
||||
bytes: t.data.clone(),
|
||||
filename: t.filename.clone(),
|
||||
});
|
||||
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
|
||||
width: t.width,
|
||||
height: t.height,
|
||||
mimetype: t.mimetype,
|
||||
size: t.size,
|
||||
});
|
||||
DraftAttachment::Image {
|
||||
image_info: ImageInfo {
|
||||
height,
|
||||
width,
|
||||
mimetype,
|
||||
size,
|
||||
thumbnail_info,
|
||||
thumbnail_source: None,
|
||||
blurhash,
|
||||
is_animated: None,
|
||||
},
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
thumbnail_source,
|
||||
}
|
||||
}
|
||||
DraftAttachmentContent::Video {
|
||||
data,
|
||||
mimetype,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
duration,
|
||||
blurhash,
|
||||
thumbnail,
|
||||
} => {
|
||||
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
|
||||
bytes: t.data.clone(),
|
||||
filename: t.filename.clone(),
|
||||
});
|
||||
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
|
||||
width: t.width,
|
||||
height: t.height,
|
||||
mimetype: t.mimetype,
|
||||
size: t.size,
|
||||
});
|
||||
DraftAttachment::Video {
|
||||
video_info: VideoInfo {
|
||||
duration,
|
||||
height,
|
||||
width,
|
||||
mimetype,
|
||||
size,
|
||||
thumbnail_info,
|
||||
thumbnail_source: None,
|
||||
blurhash,
|
||||
},
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
thumbnail_source,
|
||||
}
|
||||
}
|
||||
DraftAttachmentContent::Audio { data, mimetype, size, duration } => {
|
||||
DraftAttachment::Audio {
|
||||
audio_info: AudioInfo { duration, size, mimetype },
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
}
|
||||
}
|
||||
DraftAttachmentContent::File { data, mimetype, size } => DraftAttachment::File {
|
||||
file_info: FileInfo {
|
||||
mimetype,
|
||||
size,
|
||||
thumbnail_info: None,
|
||||
thumbnail_source: None,
|
||||
},
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the bytes and filename from an `UploadSource`, reading the file
|
||||
/// contents if needed.
|
||||
fn read_upload_source(source: UploadSource) -> Result<(Vec<u8>, String), ClientError> {
|
||||
match source {
|
||||
UploadSource::Data { bytes, filename } => Ok((bytes, filename)),
|
||||
UploadSource::File { filename } => {
|
||||
let path: PathBuf = filename.into();
|
||||
let filename = path
|
||||
.file_name()
|
||||
.ok_or(ClientError::Generic {
|
||||
msg: "Invalid attachment path".to_owned(),
|
||||
details: None,
|
||||
})?
|
||||
.to_str()
|
||||
.ok_or(ClientError::Generic {
|
||||
msg: "Invalid attachment path".to_owned(),
|
||||
details: None,
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
let bytes = fs::read(&path).map_err(|_| ClientError::Generic {
|
||||
msg: "Could not load file".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
|
||||
Ok((bytes, filename))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
DraftAttachment::Image { image_info, source, thumbnail_source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
let thumbnail = match (image_info.thumbnail_info, thumbnail_source) {
|
||||
(Some(info), Some(source)) => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Image {
|
||||
data,
|
||||
mimetype: image_info.mimetype,
|
||||
size: image_info.size,
|
||||
width: image_info.width,
|
||||
height: image_info.height,
|
||||
blurhash: image_info.blurhash,
|
||||
thumbnail,
|
||||
},
|
||||
})
|
||||
}
|
||||
DraftAttachment::Video { video_info, source, thumbnail_source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
let thumbnail = match (video_info.thumbnail_info, thumbnail_source) {
|
||||
(Some(info), Some(source)) => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Video {
|
||||
data,
|
||||
mimetype: video_info.mimetype,
|
||||
size: video_info.size,
|
||||
width: video_info.width,
|
||||
height: video_info.height,
|
||||
duration: video_info.duration,
|
||||
blurhash: video_info.blurhash,
|
||||
thumbnail,
|
||||
},
|
||||
})
|
||||
}
|
||||
DraftAttachment::Audio { audio_info, source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Audio {
|
||||
data,
|
||||
mimetype: audio_info.mimetype,
|
||||
size: audio_info.size,
|
||||
duration: audio_info.duration,
|
||||
},
|
||||
})
|
||||
}
|
||||
DraftAttachment::File { file_info, source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::File {
|
||||
data,
|
||||
mimetype: file_info.mimetype,
|
||||
size: file_info.size,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1521,3 +1858,126 @@ impl From<SdkPredecessorRoom> for PredecessorRoom {
|
||||
Self { room_id: value.room_id.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener to send queue updates in a specific room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called every time the send queue dispatches an update for the given
|
||||
/// room.
|
||||
fn on_update(&self, update: RoomSendQueueUpdate);
|
||||
}
|
||||
|
||||
/// An update to a room send queue.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomSendQueueUpdate {
|
||||
/// A new local event is being sent.
|
||||
NewLocalEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// A local event that hadn't been sent to the server yet has been cancelled
|
||||
/// before sending.
|
||||
CancelledLocalEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// A local event's content has been replaced with something else.
|
||||
ReplacedLocalEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// An error happened when an event was being sent.
|
||||
///
|
||||
/// The event has not been removed from the queue. All the send queues
|
||||
/// will be disabled after this happens, and must be manually re-enabled.
|
||||
SendError {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
/// Error received while sending the event.
|
||||
error: QueueWedgeError,
|
||||
/// Whether the error is considered recoverable or not.
|
||||
///
|
||||
/// An error that's recoverable will disable the room's send queue,
|
||||
/// while an unrecoverable error will be parked, until the user
|
||||
/// decides to cancel sending it.
|
||||
is_recoverable: bool,
|
||||
},
|
||||
|
||||
/// The event has been unwedged and sending is now being retried.
|
||||
RetryEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// The event has been sent to the server, and the query returned
|
||||
/// successfully.
|
||||
SentEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
/// Received event id from the send response.
|
||||
event_id: String,
|
||||
},
|
||||
|
||||
/// A media upload (consisting of a file and possibly a thumbnail) has made
|
||||
/// progress.
|
||||
MediaUpload {
|
||||
/// The media event this uploaded media relates to.
|
||||
related_to: String,
|
||||
|
||||
/// The final media source for the file if it has finished uploading.
|
||||
file: Option<Arc<MediaSource>>,
|
||||
|
||||
/// The index of the media within the transaction. A file and its
|
||||
/// thumbnail share the same index. Will always be 0 for non-gallery
|
||||
/// media uploads.
|
||||
index: u64,
|
||||
|
||||
/// The combined upload progress across the file and, if existing, its
|
||||
/// thumbnail. For gallery uploads, the progress is reported per indexed
|
||||
/// gallery item.
|
||||
progress: AbstractProgress,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkRoomSendQueueUpdate> for RoomSendQueueUpdate {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: SdkRoomSendQueueUpdate) -> std::result::Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkRoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
|
||||
Self::CancelledLocalEvent { transaction_id: transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::MediaUpload { related_to, file, index, progress } => {
|
||||
Self::MediaUpload {
|
||||
related_to: related_to.into(),
|
||||
file: file.map(|source| source.try_into().map(Arc::new)).transpose()?,
|
||||
index,
|
||||
progress: progress.into(),
|
||||
}
|
||||
}
|
||||
SdkRoomSendQueueUpdate::NewLocalEvent(local_echo) => {
|
||||
Self::NewLocalEvent { transaction_id: local_echo.transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::ReplacedLocalEvent { transaction_id, .. } => {
|
||||
Self::ReplacedLocalEvent { transaction_id: transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::RetryEvent { transaction_id } => {
|
||||
Self::RetryEvent { transaction_id: transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::SendError { transaction_id, error, is_recoverable } => {
|
||||
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&*error).into();
|
||||
Self::SendError {
|
||||
transaction_id: transaction_id.into(),
|
||||
error: as_queue_wedge_error.into(),
|
||||
is_recoverable,
|
||||
}
|
||||
}
|
||||
SdkRoomSendQueueUpdate::SentEvent { transaction_id, event_id } => {
|
||||
Self::SentEvent { transaction_id: transaction_id.into(), event_id: event_id.into() }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,8 @@ pub struct RoomPowerLevelsValues {
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
/// The level required to change the space's children.
|
||||
pub space_child: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
|
||||
@@ -228,6 +230,7 @@ impl From<RumaPowerLevels> for RoomPowerLevelsValues {
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
space_child: state_event_level_for(&value, &TimelineEventType::SpaceChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,15 @@ impl RoomList {
|
||||
self: Arc<Self>,
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
self.entries_with_dynamic_adapters_with(page_size, false, listener)
|
||||
}
|
||||
|
||||
fn entries_with_dynamic_adapters_with(
|
||||
self: Arc<Self>,
|
||||
page_size: u32,
|
||||
enable_latest_event_sorter: bool,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self;
|
||||
|
||||
@@ -216,7 +225,10 @@ 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());
|
||||
this.inner.entries_with_dynamic_adapters_with(
|
||||
page_size.try_into().unwrap(),
|
||||
enable_latest_event_sorter,
|
||||
);
|
||||
|
||||
// FFI dance to make those values consumable by foreign language, nothing fancy
|
||||
// here, that's the real code for this method.
|
||||
@@ -231,7 +243,12 @@ impl RoomList {
|
||||
listener.on_update(
|
||||
diffs
|
||||
.into_iter()
|
||||
.map(|room| RoomListEntriesUpdate::from(utd_hook.clone(), room))
|
||||
.map(|diff| {
|
||||
RoomListEntriesUpdate::from(
|
||||
utd_hook.clone(),
|
||||
diff.map(|room| room.into_inner()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
@@ -456,6 +473,7 @@ pub enum RoomListEntriesDynamicFilterKind {
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
NonSpace,
|
||||
Space,
|
||||
NonLeft,
|
||||
// Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
|
||||
// support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
|
||||
@@ -498,8 +516,9 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
Kind::Any { filters } => Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::NonLeft => Box::new(new_filter_non_left()),
|
||||
Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
|
||||
Kind::Space => Box::new(new_filter_space()),
|
||||
Kind::NonLeft => Box::new(new_filter_non_left()),
|
||||
Kind::Joined => Box::new(new_filter_joined()),
|
||||
Kind::Unread => Box::new(new_filter_unread()),
|
||||
Kind::Favourite => Box::new(new_filter_favourite()),
|
||||
|
||||
@@ -93,7 +93,6 @@ pub struct RoomMember {
|
||||
pub membership: MembershipState,
|
||||
pub is_name_ambiguous: bool,
|
||||
pub power_level: PowerLevel,
|
||||
pub normalized_power_level: PowerLevel,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
@@ -110,7 +109,6 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
membership: m.membership().clone().try_into()?,
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level().try_into()?,
|
||||
normalized_power_level: m.normalized_power_level().try_into()?,
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
|
||||
@@ -23,7 +23,6 @@ use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVid
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
call::notify::NotifyType as RumaNotifyType,
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
@@ -57,6 +56,7 @@ use ruma::{
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
rtc::notification::NotificationType as RumaNotificationType,
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent,
|
||||
key::{
|
||||
@@ -487,25 +487,25 @@ impl TryFrom<RumaMessageType> for MessageType {
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum NotifyType {
|
||||
pub enum RtcNotificationType {
|
||||
Ring,
|
||||
Notify,
|
||||
Notification,
|
||||
}
|
||||
|
||||
impl From<RumaNotifyType> for NotifyType {
|
||||
fn from(val: RumaNotifyType) -> Self {
|
||||
impl From<RumaNotificationType> for RtcNotificationType {
|
||||
fn from(val: RumaNotificationType) -> Self {
|
||||
match val {
|
||||
RumaNotifyType::Ring => Self::Ring,
|
||||
_ => Self::Notify,
|
||||
RumaNotificationType::Ring => Self::Ring,
|
||||
_ => Self::Notification,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotifyType> for RumaNotifyType {
|
||||
fn from(value: NotifyType) -> Self {
|
||||
impl From<RtcNotificationType> for RumaNotificationType {
|
||||
fn from(value: RtcNotificationType) -> Self {
|
||||
match value {
|
||||
NotifyType::Ring => RumaNotifyType::Ring,
|
||||
NotifyType::Notify => RumaNotifyType::Notify,
|
||||
RtcNotificationType::Ring => RumaNotificationType::Ring,
|
||||
RtcNotificationType::Notification => RumaNotificationType::Notification,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -736,7 +736,7 @@ impl TryFrom<&AudioInfo> for BaseAudioInfo {
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size) })
|
||||
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size), waveform: None })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
room_list::SpaceRoomListPaginationState, SpaceRoom as UISpaceRoom,
|
||||
SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
@@ -77,18 +78,74 @@ impl SpaceService {
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns a flattened list containing all the spaces where the user has
|
||||
/// permission to send `m.space.child` state events.
|
||||
///
|
||||
/// Note: Unlike [`Self::joined_spaces()`], this method does not recompute
|
||||
/// the space graph, nor does it notify subscribers about changes.
|
||||
pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.editable_spaces().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Returns a `SpaceRoomList` for the given space ID.
|
||||
#[allow(clippy::unused_async)]
|
||||
// This method doesn't need to be async but if its not the FFI layer panics
|
||||
// with "there is no no reactor running, must be called from the context
|
||||
// of a Tokio 1.x runtime" error because the underlying constructor spawns
|
||||
// an async task.
|
||||
pub async fn space_room_list(
|
||||
&self,
|
||||
space_id: String,
|
||||
) -> Result<Arc<SpaceRoomList>, ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id))))
|
||||
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id).await)))
|
||||
}
|
||||
|
||||
/// Returns all known direct-parents of a given space room ID.
|
||||
pub async fn joined_parents_of_child(
|
||||
&self,
|
||||
child_id: String,
|
||||
) -> Result<Vec<SpaceRoom>, ClientError> {
|
||||
let child_id = RoomId::parse(child_id)?;
|
||||
|
||||
let parents = self.inner.joined_parents_of_child(&child_id).await;
|
||||
|
||||
Ok(parents.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
pub async fn add_child_to_space(
|
||||
&self,
|
||||
child_id: String,
|
||||
space_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
let child_id = RoomId::parse(child_id)?;
|
||||
|
||||
self.inner.add_child_to_space(child_id, space_id).await.map_err(ClientError::from)
|
||||
}
|
||||
|
||||
pub async fn remove_child_from_space(
|
||||
&self,
|
||||
child_id: String,
|
||||
space_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
let child_id = RoomId::parse(child_id)?;
|
||||
|
||||
self.inner.remove_child_from_space(child_id, space_id).await.map_err(ClientError::from)
|
||||
}
|
||||
|
||||
/// Start a space leave process returning a [`LeaveSpaceHandle`] from which
|
||||
/// rooms can be retrieved in reversed BFS order starting from the requested
|
||||
/// `space_id` graph node. If the room is unknown then an error will be
|
||||
/// returned.
|
||||
///
|
||||
/// Once the rooms to be left are chosen the handle can be used to leave
|
||||
/// them.
|
||||
pub async fn leave_space(
|
||||
&self,
|
||||
space_id: String,
|
||||
) -> Result<Arc<LeaveSpaceHandle>, ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
|
||||
let handle = self.inner.leave_space(&space_id).await.map_err(ClientError::from)?;
|
||||
|
||||
Ok(Arc::new(handle.into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +172,27 @@ impl SpaceRoomList {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceRoomList {
|
||||
/// Returns the space of the room list if known.
|
||||
pub fn space(&self) -> Option<SpaceRoom> {
|
||||
self.inner.space().map(Into::into)
|
||||
}
|
||||
|
||||
/// Subscribe to space updates.
|
||||
pub fn subscribe_to_space_updates(
|
||||
&self,
|
||||
listener: Box<dyn SpaceRoomListSpaceListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let space_updates = self.inner.subscribe_to_space_updates();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(space_updates);
|
||||
|
||||
while let Some(space) = space_updates.next().await {
|
||||
listener.on_update(space.map(Into::into));
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns if the room list is currently paginating or not.
|
||||
pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
|
||||
self.inner.pagination_state()
|
||||
@@ -166,6 +244,11 @@ impl SpaceRoomList {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceRoomListSpaceListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, space: Option<SpaceRoom>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceRoomListPaginationStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, pagination_state: SpaceRoomListPaginationState);
|
||||
@@ -189,8 +272,11 @@ pub struct SpaceRoom {
|
||||
pub room_id: String,
|
||||
/// The canonical alias of the room, if any.
|
||||
pub canonical_alias: Option<String>,
|
||||
/// The name of the room, if any.
|
||||
pub name: Option<String>,
|
||||
/// The room's name from the room state event if received from sync, or one
|
||||
/// that's been computed otherwise.
|
||||
pub display_name: String,
|
||||
/// Room name as defined by the room state event only.
|
||||
pub raw_name: Option<String>,
|
||||
/// The topic of the room, if any.
|
||||
pub topic: Option<String>,
|
||||
/// The URL for the room's avatar, if one is set.
|
||||
@@ -206,12 +292,19 @@ pub struct SpaceRoom {
|
||||
/// Whether guest users may join the room and participate in it.
|
||||
pub guest_can_join: bool,
|
||||
|
||||
/// Whether this room is a direct room.
|
||||
///
|
||||
/// Only set if the room is known to the client otherwise we
|
||||
/// assume DMs shouldn't be exposed publicly in spaces.
|
||||
pub is_direct: Option<bool>,
|
||||
/// The number of children room this has, if a space.
|
||||
pub children_count: u64,
|
||||
/// Whether this room is joined, left etc.
|
||||
pub state: Option<Membership>,
|
||||
/// A list of room members considered to be heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
/// The via parameters of the room.
|
||||
pub via: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<UISpaceRoom> for SpaceRoom {
|
||||
@@ -219,7 +312,8 @@ impl From<UISpaceRoom> for SpaceRoom {
|
||||
Self {
|
||||
room_id: room.room_id.into(),
|
||||
canonical_alias: room.canonical_alias.map(|alias| alias.into()),
|
||||
name: room.name,
|
||||
display_name: room.display_name,
|
||||
raw_name: room.name,
|
||||
topic: room.topic,
|
||||
avatar_url: room.avatar_url.map(|url| url.into()),
|
||||
room_type: room.room_type.into(),
|
||||
@@ -227,9 +321,11 @@ impl From<UISpaceRoom> for SpaceRoom {
|
||||
join_rule: room.join_rule.map(Into::into),
|
||||
world_readable: room.world_readable,
|
||||
guest_can_join: room.guest_can_join,
|
||||
is_direct: room.is_direct,
|
||||
children_count: room.children_count,
|
||||
state: room.state.map(Into::into),
|
||||
heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
|
||||
via: room.via.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,3 +370,57 @@ impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `LeaveSpaceHandle` processes rooms to be left in the order they were
|
||||
/// provided by the [`SpaceService`] and annotates them with extra data to
|
||||
/// inform the leave process e.g. if the current user is the last room admin.
|
||||
///
|
||||
/// Once the upstream client decides what rooms should actually be left, the
|
||||
/// handle provides a method to execute that too.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LeaveSpaceHandle {
|
||||
inner: UILeaveSpaceHandle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LeaveSpaceHandle {
|
||||
/// A list of rooms to be left which next to normal [`SpaceRoom`] data also
|
||||
/// include leave specific information.
|
||||
pub fn rooms(&self) -> Vec<LeaveSpaceRoom> {
|
||||
let rooms = self.inner.rooms();
|
||||
rooms.iter().map(|room| room.clone().into()).collect()
|
||||
}
|
||||
|
||||
/// Bulk leave the given rooms. Stops when encountering an error.
|
||||
pub async fn leave(&self, room_ids: Vec<String>) -> Result<(), ClientError> {
|
||||
let room_ids = room_ids.iter().map(RoomId::parse).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner
|
||||
.leave(|room| room_ids.contains(&room.space_room.room_id))
|
||||
.await
|
||||
.map_err(ClientError::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UILeaveSpaceHandle> for LeaveSpaceHandle {
|
||||
fn from(handle: UILeaveSpaceHandle) -> Self {
|
||||
LeaveSpaceHandle { inner: handle }
|
||||
}
|
||||
}
|
||||
|
||||
/// Space leaving specific room that groups normal [`SpaceRoom`] details with
|
||||
/// information about the leaving user's role.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LeaveSpaceRoom {
|
||||
/// The underlying [`SpaceRoom`]
|
||||
space_room: SpaceRoom,
|
||||
/// Whether the user is the last admin in the room. This helps clients
|
||||
/// better inform the user about the consequences of leaving the room.
|
||||
is_last_admin: bool,
|
||||
}
|
||||
|
||||
impl From<UILeaveSpaceRoom> for LeaveSpaceRoom {
|
||||
fn from(room: UILeaveSpaceRoom) -> Self {
|
||||
LeaveSpaceRoom { space_room: room.space_room.into(), is_last_admin: room.is_last_admin }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#[cfg(feature = "sqlite")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use matrix_sdk::SqliteStoreConfig;
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::client_builder::ClientBuilder;
|
||||
|
||||
/// The outcome of building a [`StoreBuilder`], with data that can be passed
|
||||
/// directly to a [`ClientBuilder`].
|
||||
pub enum StoreBuilderOutcome {
|
||||
/// An SQLite store configuration successfully built.
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite { config: SqliteStoreConfig, cache_path: PathBuf, store_path: PathBuf },
|
||||
|
||||
/// An IndexedDB store configuration successfully built.
|
||||
#[cfg(feature = "indexeddb")]
|
||||
IndexedDb { name: String, passphrase: Option<String> },
|
||||
|
||||
/// An in-memory store configuration successfully built.
|
||||
InMemory,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
mod sqlite {
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
|
||||
use matrix_sdk::SqliteStoreConfig;
|
||||
use tracing::debug;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::StoreBuilderOutcome;
|
||||
use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc};
|
||||
|
||||
/// The store paths the client will use when built.
|
||||
#[derive(Clone)]
|
||||
struct StorePaths {
|
||||
/// The path that the client will use to store its data.
|
||||
data_path: String,
|
||||
|
||||
/// The path that the client will use to store its caches. This path can
|
||||
/// be the same as the data path if you prefer to keep
|
||||
/// everything in one place.
|
||||
cache_path: String,
|
||||
}
|
||||
|
||||
/// A builder for configuring a Sqlite session store.
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct SqliteStoreBuilder {
|
||||
paths: StorePaths,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
pool_max_size: Option<usize>,
|
||||
cache_size: Option<u32>,
|
||||
journal_size_limit: Option<u32>,
|
||||
system_is_memory_constrained: bool,
|
||||
}
|
||||
|
||||
impl SqliteStoreBuilder {
|
||||
pub(crate) fn raw_new(data_path: String, cache_path: String) -> Self {
|
||||
Self {
|
||||
paths: StorePaths { data_path, cache_path },
|
||||
passphrase: Zeroizing::new(None),
|
||||
pool_max_size: None,
|
||||
cache_size: None,
|
||||
journal_size_limit: None,
|
||||
system_is_memory_constrained: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SqliteStoreBuilder {
|
||||
/// Construct a [`SqliteStoreBuilder`] and set the paths that the client
|
||||
/// will use to store its data and caches.
|
||||
///
|
||||
/// Both paths **must** be unique per session as the SDK stores aren't
|
||||
/// capable of handling multiple users, however it is valid to use the
|
||||
/// same path for both stores on a single session.
|
||||
#[uniffi::constructor]
|
||||
pub fn new(data_path: String, cache_path: String) -> Arc<Self> {
|
||||
Arc::new(Self::raw_new(data_path, cache_path))
|
||||
}
|
||||
|
||||
/// Set the passphrase for the stores.
|
||||
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.passphrase = Zeroizing::new(passphrase);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the pool max size for the stores.
|
||||
///
|
||||
/// Each store exposes an async pool of connections. This method
|
||||
/// controls the size of the pool. The larger the pool is, the more
|
||||
/// memory is consumed, but also the more the app is reactive because it
|
||||
/// doesn't need to wait on a pool to be available to run queries.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
|
||||
pub fn pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.pool_max_size = pool_max_size.map(|size| {
|
||||
size.try_into().expect("`pool_max_size` is too large to fit in `usize`")
|
||||
});
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the cache size for the stores.
|
||||
///
|
||||
/// Each store exposes a SQLite connection. This method controls the
|
||||
/// cache size, in **bytes (!)**.
|
||||
///
|
||||
/// The cache represents data SQLite holds in memory at once per open
|
||||
/// database file. The default cache implementation does not allocate
|
||||
/// the full amount of cache memory all at once. Cache memory is
|
||||
/// allocated in smaller chunks on an as-needed basis.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::cache_size`] to learn more.
|
||||
pub fn cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.cache_size = cache_size;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the size limit for the SQLite WAL files of stores.
|
||||
///
|
||||
/// Each store uses the WAL journal mode. This method controls the size
|
||||
/// limit of the WAL files, in **bytes (!)**.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
|
||||
pub fn journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.journal_size_limit = limit;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Tell the client that the system is memory constrained, like in a
|
||||
/// push notification process for example.
|
||||
///
|
||||
/// So far, at the time of writing (2025-04-07), it changes
|
||||
/// the defaults of [`SqliteStoreConfig`]. Please check
|
||||
/// [`SqliteStoreConfig::with_low_memory_config`].
|
||||
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.system_is_memory_constrained = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl SqliteStoreBuilder {
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
|
||||
let data_path = Path::new(&self.paths.data_path);
|
||||
let cache_path = Path::new(&self.paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(data_path)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
|
||||
let mut sqlite_store_config = if self.system_is_memory_constrained {
|
||||
SqliteStoreConfig::with_low_memory_config(data_path)
|
||||
} else {
|
||||
SqliteStoreConfig::new(data_path)
|
||||
};
|
||||
|
||||
sqlite_store_config = sqlite_store_config.passphrase(self.passphrase.as_deref());
|
||||
|
||||
if let Some(size) = self.pool_max_size {
|
||||
sqlite_store_config = sqlite_store_config.pool_max_size(size);
|
||||
}
|
||||
|
||||
if let Some(size) = self.cache_size {
|
||||
sqlite_store_config = sqlite_store_config.cache_size(size);
|
||||
}
|
||||
|
||||
if let Some(limit) = self.journal_size_limit {
|
||||
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
|
||||
}
|
||||
|
||||
Ok(StoreBuilderOutcome::Sqlite {
|
||||
config: sqlite_store_config,
|
||||
store_path: data_path.to_owned(),
|
||||
cache_path: cache_path.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
mod indexeddb {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::StoreBuilderOutcome;
|
||||
use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct IndexedDbStoreBuilder {
|
||||
name: String,
|
||||
passphrase: Option<String>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl IndexedDbStoreBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(name: String) -> Arc<Self> {
|
||||
Arc::new(Self { name, passphrase: None })
|
||||
}
|
||||
|
||||
/// Set the passphrase for the stores.
|
||||
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.passphrase = passphrase;
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexedDbStoreBuilder {
|
||||
pub fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
|
||||
Ok(StoreBuilderOutcome::IndexedDb {
|
||||
name: self.name.clone(),
|
||||
passphrase: self.passphrase.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
pub use indexeddb::*;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use sqlite::*;
|
||||
|
||||
use crate::client_builder::ClientBuildError;
|
||||
|
||||
/// Represent the kind of store the client will configure.
|
||||
#[derive(Clone)]
|
||||
pub enum StoreBuilder {
|
||||
/// Represents the builder for the SQLite store.
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite(SqliteStoreBuilder),
|
||||
|
||||
/// Represents the builder for the IndexedDB store.
|
||||
#[cfg(feature = "indexeddb")]
|
||||
IndexedDb(IndexedDbStoreBuilder),
|
||||
|
||||
/// Represents the builder for in-memory store.
|
||||
InMemory,
|
||||
}
|
||||
|
||||
impl StoreBuilder {
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::Sqlite(config) => config.build(),
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
Self::IndexedDb(config) => config.build(),
|
||||
|
||||
Self::InMemory => Ok(StoreBuilderOutcome::InMemory),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
MatrixSyncServiceState::Idle => Self::Idle,
|
||||
MatrixSyncServiceState::Running => Self::Running,
|
||||
MatrixSyncServiceState::Terminated => Self::Terminated,
|
||||
MatrixSyncServiceState::Error => Self::Error,
|
||||
MatrixSyncServiceState::Error(_error) => Self::Error,
|
||||
MatrixSyncServiceState::Offline => Self::Offline,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
|
||||
use matrix_sdk_ui::timeline::{
|
||||
event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter,
|
||||
TimelineReadReceiptTracking,
|
||||
};
|
||||
use ruma::{
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
EventId,
|
||||
@@ -173,11 +176,11 @@ pub struct TimelineConfiguration {
|
||||
pub date_divider_mode: DateDividerMode,
|
||||
|
||||
/// Should the read receipts and read markers be tracked for the timeline
|
||||
/// items in this instance?
|
||||
/// items in this instance and on which event types?
|
||||
///
|
||||
/// As this has a non negligible performance impact, make sure to enable it
|
||||
/// only when you need it.
|
||||
pub track_read_receipts: bool,
|
||||
pub track_read_receipts: TimelineReadReceiptTracking,
|
||||
|
||||
/// Whether this timeline instance should report UTDs through the client's
|
||||
/// delegate.
|
||||
|
||||
@@ -16,9 +16,11 @@ use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
|
||||
use ruma::events::FullStateEventContent;
|
||||
use ruma::events::{
|
||||
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, FullStateEventContent,
|
||||
};
|
||||
|
||||
use crate::{timeline::msg_like::MsgLikeContent, utils::Timestamp};
|
||||
use crate::{client::JoinRule, timeline::msg_like::MsgLikeContent, utils::Timestamp};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
@@ -35,7 +37,7 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::CallNotify => TimelineItemContent::CallNotify,
|
||||
Content::RtcNotification => TimelineItemContent::RtcNotification,
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
@@ -95,6 +97,51 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum HistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member' 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' 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 history visibility, up for interpretation by the consumer.
|
||||
Custom {
|
||||
/// The string representation for this custom history visibility.
|
||||
repr: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: RumaHistoryVisibility) -> Self {
|
||||
match value {
|
||||
RumaHistoryVisibility::Invited => Self::Invited,
|
||||
RumaHistoryVisibility::Joined => Self::Joined,
|
||||
RumaHistoryVisibility::Shared => Self::Shared,
|
||||
RumaHistoryVisibility::WorldReadable => Self::WorldReadable,
|
||||
_ => Self::Custom { repr: value.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `TimelineItemContent`, we would need to
|
||||
@@ -109,7 +156,7 @@ pub enum TimelineItemContent {
|
||||
content: MsgLikeContent,
|
||||
},
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
RtcNotification,
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
@@ -203,11 +250,11 @@ pub enum OtherState {
|
||||
RoomAliases,
|
||||
RoomAvatar { url: Option<String> },
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomCreate { federate: Option<bool> },
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomHistoryVisibility { history_visibility: Option<HistoryVisibility> },
|
||||
RoomJoinRules { join_rule: Option<JoinRule> },
|
||||
RoomName { name: Option<String> },
|
||||
RoomPinnedEvents { change: RoomPinnedEventsChange },
|
||||
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
|
||||
@@ -240,11 +287,39 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Self::RoomAvatar { url }
|
||||
}
|
||||
Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
|
||||
Content::RoomCreate(_) => Self::RoomCreate,
|
||||
Content::RoomCreate(c) => {
|
||||
let federate = match c {
|
||||
FullContent::Original { content, .. } => Some(content.federate),
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomCreate { federate }
|
||||
}
|
||||
Content::RoomEncryption(_) => Self::RoomEncryption,
|
||||
Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
|
||||
Content::RoomHistoryVisibility(_) => Self::RoomHistoryVisibility,
|
||||
Content::RoomJoinRules(_) => Self::RoomJoinRules,
|
||||
Content::RoomHistoryVisibility(c) => {
|
||||
let history_visibility = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
Some(content.history_visibility.clone().into())
|
||||
}
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomHistoryVisibility { history_visibility }
|
||||
}
|
||||
Content::RoomJoinRules(c) => {
|
||||
let join_rule = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
match content.join_rule.clone().try_into() {
|
||||
Ok(jr) => Some(jr),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to convert join rule: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomJoinRules { join_rule }
|
||||
}
|
||||
Content::RoomName(c) => {
|
||||
let name = match c {
|
||||
FullContent::Original { content, .. } => Some(content.name.clone()),
|
||||
|
||||
@@ -37,6 +37,7 @@ use matrix_sdk_ui::timeline::{
|
||||
use mime::Mime;
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
poll::{
|
||||
@@ -49,6 +50,7 @@ use ruma::{
|
||||
},
|
||||
room::message::{
|
||||
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
|
||||
TextMessageEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
@@ -111,16 +113,16 @@ impl Timeline {
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
let caption = params.caption.map(|caption| {
|
||||
let formatted =
|
||||
formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
|
||||
assign!(TextMessageEventContent::plain(caption), { formatted })
|
||||
});
|
||||
|
||||
let attachment_config = AttachmentConfig {
|
||||
info: Some(attachment_info),
|
||||
thumbnail,
|
||||
caption: params.caption,
|
||||
formatted_caption,
|
||||
caption,
|
||||
mentions: params.mentions.map(Into::into),
|
||||
in_reply_to: in_reply_to_event_id,
|
||||
..Default::default()
|
||||
@@ -352,17 +354,31 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark the room as read by trying to attach an *unthreaded* read receipt
|
||||
/// to the latest room event.
|
||||
/// Mark the timeline as read by attempting to send a read receipt on the
|
||||
/// latest visible event.
|
||||
///
|
||||
/// This works even if the latest event belongs to a thread, as a threaded
|
||||
/// reply also belongs to the unthreaded timeline. No threaded receipt
|
||||
/// will be sent here (see also #3123).
|
||||
/// The latest visible event is determined from the timeline's focus kind
|
||||
/// and whether or not it hides threaded events. If no latest event can
|
||||
/// be determined and the timeline is live, the room's unread marker is
|
||||
/// unset instead.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `receipt_type` - The type of receipt to send. When using
|
||||
/// [`ReceiptType::FullyRead`], an unthreaded receipt will be sent. This
|
||||
/// works even if the latest event belongs to a thread, as a threaded
|
||||
/// reply also belongs to the unthreaded timeline. Otherwise the receipt
|
||||
/// thread will be determined based on the timeline's focus kind.
|
||||
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
|
||||
self.inner.mark_as_read(receipt_type.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the latest [`EventId`] in the timeline.
|
||||
pub async fn latest_event_id(&self) -> Option<String> {
|
||||
self.inner.latest_event_id().await.as_deref().map(ToString::to_string)
|
||||
}
|
||||
|
||||
/// Queues an event in the room's send queue so it's processed for
|
||||
/// sending later.
|
||||
///
|
||||
@@ -422,14 +438,12 @@ impl Timeline {
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
waveform: Vec<f32>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Voice {
|
||||
audio_info: BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
waveform: Some(waveform),
|
||||
};
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
|
||||
let mut info =
|
||||
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
info.waveform = Some(waveform);
|
||||
self.send_attachment(params, AttachmentInfo::Voice(info), audio_info.mimetype, None)
|
||||
}
|
||||
|
||||
pub fn send_file(
|
||||
@@ -605,13 +619,14 @@ impl Timeline {
|
||||
///
|
||||
/// Ensures that only one reaction is sent at a time to avoid race
|
||||
/// conditions and spamming the homeserver with requests.
|
||||
///
|
||||
/// Returns `true` if the reaction was added, `false` if it was removed.
|
||||
pub async fn toggle_reaction(
|
||||
&self,
|
||||
item_id: EventOrTransactionId,
|
||||
key: String,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.toggle_reaction(&item_id.try_into()?, &key).await?;
|
||||
Ok(())
|
||||
) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.toggle_reaction(&item_id.try_into()?, &key).await?)
|
||||
}
|
||||
|
||||
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
@@ -1298,6 +1313,8 @@ pub enum LatestEventValue {
|
||||
},
|
||||
Local {
|
||||
timestamp: Timestamp,
|
||||
sender: String,
|
||||
profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
is_sending: bool,
|
||||
},
|
||||
@@ -1316,8 +1333,14 @@ impl From<UiLatestEventValue> for LatestEventValue {
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
UiLatestEventValue::Local { timestamp, content, is_sending } => {
|
||||
Self::Local { timestamp: timestamp.into(), content: content.into(), is_sending }
|
||||
UiLatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
|
||||
Self::Local {
|
||||
timestamp: timestamp.into(),
|
||||
sender: sender.to_string(),
|
||||
profile: profile.into(),
|
||||
content: content.into(),
|
||||
is_sending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1336,7 +1359,7 @@ mod galleries {
|
||||
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use ruma::EventId;
|
||||
use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
@@ -1475,15 +1498,18 @@ mod galleries {
|
||||
let mime_str = self.mimetype().as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
let caption = self.caption().as_ref().map(|caption| {
|
||||
let formatted = formatted_body_from(
|
||||
Some(caption),
|
||||
self.formatted_caption().clone().map(Into::into),
|
||||
);
|
||||
assign!(TextMessageEventContent::plain(caption), { formatted })
|
||||
});
|
||||
Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
|
||||
source: self.source().clone().into(),
|
||||
content_type: mime_type,
|
||||
attachment_info: self.attachment_info()?,
|
||||
caption: self.caption().clone(),
|
||||
formatted_caption: self
|
||||
.formatted_caption()
|
||||
.clone()
|
||||
.map(ruma::events::room::message::FormattedBody::from),
|
||||
caption,
|
||||
thumbnail: self.thumbnail()?,
|
||||
})
|
||||
}
|
||||
@@ -1542,10 +1568,11 @@ mod galleries {
|
||||
params: GalleryUploadParameters,
|
||||
item_infos: Vec<GalleryItemInfo>,
|
||||
) -> Result<Arc<SendGalleryJoinHandle>, RoomError> {
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
let caption = params.caption.map(|caption| {
|
||||
let formatted =
|
||||
formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
|
||||
assign!(TextMessageEventContent::plain(caption), { formatted })
|
||||
});
|
||||
|
||||
let in_reply_to = params
|
||||
.in_reply_to
|
||||
@@ -1555,8 +1582,7 @@ mod galleries {
|
||||
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
|
||||
let mut gallery_config = GalleryConfig::new()
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.caption(caption)
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.in_reply_to(in_reply_to);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
|
||||
use super::{
|
||||
@@ -23,6 +23,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
event::MessageLikeEventType,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
timeline::content::ReactionSenderData,
|
||||
utils::Timestamp,
|
||||
@@ -50,6 +51,9 @@ pub enum MsgLikeKind {
|
||||
|
||||
/// An `m.room.encrypted` event that could not be decrypted.
|
||||
UnableToDecrypt { msg: EncryptedMessage },
|
||||
|
||||
/// A custom message like event.
|
||||
Other { event_type: MessageLikeEventType },
|
||||
}
|
||||
|
||||
/// A special kind of [`super::TimelineItemContent`] that groups together
|
||||
@@ -182,6 +186,15 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
Kind::Other(other) => Self {
|
||||
kind: MsgLikeKind::Other {
|
||||
event_type: MessageLikeEventType::Other(other.event_type().to_string()),
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::borrow::ToOwned;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use tracing::{callsite::DefaultCallsite, field::FieldSet, Callsite};
|
||||
use tracing::{callsite::DefaultCallsite, debug, error, field::FieldSet, Callsite};
|
||||
use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
|
||||
/// Log an event.
|
||||
@@ -96,6 +98,8 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Span(tracing::Span);
|
||||
|
||||
pub(crate) const BRIDGE_SPAN_NAME: &str = "<sdk_bridge_span>";
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Span {
|
||||
/// Create a span originating at the given callsite (file, line and column).
|
||||
@@ -129,18 +133,41 @@ impl Span {
|
||||
level: LogLevel,
|
||||
target: String,
|
||||
name: String,
|
||||
bridge_trace_id: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
|
||||
let loc = MetadataId { file, line, level, target, name: Some(name) };
|
||||
let callsite = get_or_init_metadata(&CALLSITES, loc, &[], MetadataKind::SPAN);
|
||||
|
||||
// If sentry isn't enabled, ignore bridge_trace_id's contents
|
||||
let bridge_trace_id = if cfg!(feature = "sentry") { bridge_trace_id } else { None };
|
||||
|
||||
let callsite = if cfg!(feature = "sentry") {
|
||||
get_or_init_metadata(&CALLSITES, loc, &["sentry", "sentry.trace"], MetadataKind::SPAN)
|
||||
} else {
|
||||
get_or_init_metadata(&CALLSITES, loc, &[], MetadataKind::SPAN)
|
||||
};
|
||||
|
||||
let metadata = callsite.metadata();
|
||||
|
||||
let span = if span_or_event_enabled(callsite) {
|
||||
// This function is hidden from docs, but we have to use it (see above).
|
||||
let values = metadata.fields().value_set(&[]);
|
||||
tracing::Span::new(metadata, &values)
|
||||
let fields = metadata.fields();
|
||||
|
||||
if let Some(parent_trace_id) = bridge_trace_id {
|
||||
debug!("Adding fields | sentry:true, sentry.trace={parent_trace_id}");
|
||||
let sentry_field = fields.field("sentry").unwrap();
|
||||
let sentry_trace_field = fields.field("sentry.trace").unwrap();
|
||||
#[allow(trivial_casts)] // The compiler is lying, it can't infer this cast
|
||||
let values = [
|
||||
(&sentry_field, Some(&true as &dyn tracing::Value)),
|
||||
(&sentry_trace_field, Some(&parent_trace_id as &dyn tracing::Value)),
|
||||
];
|
||||
tracing::Span::new(metadata, &fields.value_set(&values))
|
||||
} else {
|
||||
tracing::Span::new(metadata, &fields.value_set(&[]))
|
||||
}
|
||||
} else {
|
||||
tracing::Span::none()
|
||||
};
|
||||
@@ -164,6 +191,27 @@ impl Span {
|
||||
fn is_none(&self) -> bool {
|
||||
self.0.is_none()
|
||||
}
|
||||
|
||||
/// Creates a [`Span`] that acts as a bridge between the client spans and
|
||||
/// the SDK ones, allowing them to be joined in Sentry. This function
|
||||
/// will only return a valid span if the `sentry` feature is enabled,
|
||||
/// otherwise it will return a noop span.
|
||||
#[uniffi::constructor]
|
||||
pub fn new_bridge_span(target: String, parent_trace_id: Option<String>) -> Arc<Self> {
|
||||
if cfg!(feature = "sentry") {
|
||||
Self::new(
|
||||
"Bridge".to_owned(),
|
||||
None,
|
||||
LogLevel::Info,
|
||||
target,
|
||||
BRIDGE_SPAN_NAME.to_owned(),
|
||||
parent_trace_id,
|
||||
)
|
||||
} else {
|
||||
error!("Sentry is not enabled!");
|
||||
Arc::new(Self(tracing::Span::none()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, uniffi::Enum)]
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo,
|
||||
|
||||
@@ -125,9 +125,10 @@ pub async fn generate_webview_url(
|
||||
/// call widget.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn new_virtual_element_call_widget(
|
||||
props: matrix_sdk::widget::VirtualElementCallWidgetOptions,
|
||||
props: matrix_sdk::widget::VirtualElementCallWidgetProperties,
|
||||
config: matrix_sdk::widget::VirtualElementCallWidgetConfig,
|
||||
) -> Result<WidgetSettings, ParseError> {
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props)
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props, config)
|
||||
.map(|w| w.into())?)
|
||||
}
|
||||
|
||||
@@ -175,6 +176,10 @@ pub fn get_element_call_required_permissions(
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::RoomRedaction.to_string(),
|
||||
},
|
||||
// This allows declining an incoming call and detect if someone declines a call.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::RtcDecline.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
WidgetCapabilities {
|
||||
@@ -199,10 +204,12 @@ pub fn get_element_call_required_permissions(
|
||||
send: vec![
|
||||
// To notify other users that a call has started.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
|
||||
event_type: MessageLikeEventType::RtcNotification.to_string(),
|
||||
},
|
||||
// Also for call notifications, except this is the deprecated fallback type which
|
||||
// Element Call still sends.
|
||||
// Deprecated for now, kept for backward compatibility as widgets will send both
|
||||
// CallNotify and RtcNotification.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::CallNotify.to_string(),
|
||||
},
|
||||
@@ -528,5 +535,9 @@ mod tests {
|
||||
);
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
|
||||
|
||||
// RTC decline
|
||||
cap_assert("org.matrix.msc2762.receive.event:org.matrix.msc4310.rtc.decline");
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.msc4310.rtc.decline");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[bindings.kotlin]
|
||||
package_name = "org.matrix.rustcomponents.sdk"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
android_cleaner = true
|
||||
android_cleaner = true
|
||||
|
||||
@@ -6,6 +6,46 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.16.0] - 2025-12-04
|
||||
|
||||
### Security Fixes
|
||||
|
||||
- Skip the serialization of custom join rules in the `RoomInfo` which prevented
|
||||
the processing of sync responses containing events with custom join rules.
|
||||
([#5924](https://github.com/matrix-org/matrix-rust-sdk/pull/5924))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `ServerInfo` has been renamed to `SupportedVersionsResponse`,
|
||||
and its `well_known` field has been removed. It is also wrapped in a
|
||||
`TtlStoreValue` that handles the expiration of the data, rather than calling
|
||||
`maybe_decode()`. Its constructor has been removed since all its fields are
|
||||
now public.
|
||||
([#5910](https://github.com/matrix-org/matrix-rust-sdk/pull/5910))
|
||||
- `StateStoreData(Key/Value)::ServerInfo` has been split into the
|
||||
`SupportedVersions` and `WellKnown` variants.
|
||||
- [**breaking**] Upgrade Ruma to version 0.14.0.
|
||||
([#5882](https://github.com/matrix-org/matrix-rust-sdk/pull/5882))
|
||||
- `Client::sync_lock` has been renamed `Client::state_store_lock`.
|
||||
([#5707](https://github.com/matrix-org/matrix-rust-sdk/pull/5707))
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] The `EventCacheStore::get_room_events()` method has received
|
||||
two new arguments. This allows users to load only events of a certain event
|
||||
type and events that were encrypted using a certain room key identified by its
|
||||
session ID.
|
||||
([#5817](https://github.com/matrix-org/matrix-rust-sdk/pull/5817))
|
||||
- `ComposerDraft` can now store attachments alongside text messages.
|
||||
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
|
||||
|
||||
## [0.14.1] - 2025-09-10
|
||||
|
||||
### Security Fixes
|
||||
|
||||
- Fix a panic in the `RoomMember::normalized_power_level` method.
|
||||
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635)) (Low, [CVE-2025-59047](https://www.cve.org/CVERecord?id=CVE-2025-59047), [GHSA-qhj8-q5r6-8q6j](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-qhj8-q5r6-8q6j)).
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features
|
||||
|
||||
@@ -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.14.0"
|
||||
version = "0.16.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -36,7 +36,7 @@ experimental-send-custom-to-device = [
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
|
||||
experimental-encrypted-state-events = [
|
||||
"e2e-encryption",
|
||||
"ruma/unstable-msc3414",
|
||||
"ruma/unstable-msc4362",
|
||||
"matrix-sdk-crypto?/experimental-encrypted-state-events"
|
||||
]
|
||||
|
||||
@@ -65,6 +65,8 @@ testing = [
|
||||
# Add support for inline media galleries via msgtypes
|
||||
unstable-msc4274 = []
|
||||
|
||||
experimental-element-recent-emojis = []
|
||||
|
||||
[dependencies]
|
||||
as_variant.workspace = true
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
@@ -82,7 +84,7 @@ matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption.workspace = true
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell.workspace = true
|
||||
regex = "1.11.2"
|
||||
regex.workspace = true
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
@@ -107,6 +109,7 @@ futures-executor.workspace = true
|
||||
http.workspace = true
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-test-utils.workspace = true
|
||||
proptest.workspace = true
|
||||
similar-asserts.workspace = true
|
||||
stream_assert.workspace = true
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ use crate::{
|
||||
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
|
||||
deserialized_responses::DisplayName,
|
||||
error::{Error, Result},
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
|
||||
media::store::MediaStoreLock,
|
||||
response_processors::{self as processors, Context},
|
||||
room::{
|
||||
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
|
||||
@@ -92,6 +93,9 @@ pub struct BaseClient {
|
||||
/// The store used by the event cache.
|
||||
event_cache_store: EventCacheStoreLock,
|
||||
|
||||
/// The store used by the media cache.
|
||||
media_store: MediaStoreLock,
|
||||
|
||||
/// The store used for encryption.
|
||||
///
|
||||
/// This field is only meant to be used for `OlmMachine` initialization.
|
||||
@@ -190,6 +194,7 @@ impl BaseClient {
|
||||
BaseClient {
|
||||
state_store: store,
|
||||
event_cache_store: config.event_cache_store,
|
||||
media_store: config.media_store,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
crypto_store: config.crypto_store,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -223,6 +228,7 @@ impl BaseClient {
|
||||
let copy = Self {
|
||||
state_store: BaseStateStore::new(config.state_store),
|
||||
event_cache_store: config.event_cache_store,
|
||||
media_store: config.media_store,
|
||||
// We copy the crypto store as well as the `OlmMachine` for two reasons:
|
||||
// 1. The `self.crypto_store` is the same as the one used inside the `OlmMachine`.
|
||||
// 2. We need to ensure that the parent and child use the same data and caches inside
|
||||
@@ -307,6 +313,11 @@ impl BaseClient {
|
||||
&self.event_cache_store
|
||||
}
|
||||
|
||||
/// Get a reference to the media store.
|
||||
pub fn media_store(&self) -> &MediaStoreLock {
|
||||
&self.media_store
|
||||
}
|
||||
|
||||
/// Check whether the client has been activated.
|
||||
///
|
||||
/// See [`BaseClient::activate`] to know what it means.
|
||||
@@ -414,7 +425,7 @@ impl BaseClient {
|
||||
);
|
||||
|
||||
if room.state() != RoomState::Knocked {
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
@@ -481,7 +492,7 @@ impl BaseClient {
|
||||
// If the state isn't `RoomState::Joined` then this means that we knew about
|
||||
// this room before. Let's modify the existing state now.
|
||||
if room.state() != RoomState::Joined {
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
let previous_state = room.state();
|
||||
@@ -532,7 +543,7 @@ impl BaseClient {
|
||||
);
|
||||
|
||||
if room.state() != RoomState::Left {
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_left();
|
||||
@@ -547,9 +558,12 @@ impl BaseClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get access to the store's sync lock.
|
||||
pub fn sync_lock(&self) -> &Mutex<()> {
|
||||
self.state_store.sync_lock()
|
||||
/// Get a lock to the state store, with an exclusive access.
|
||||
///
|
||||
/// It doesn't give an access to the state store itself. It's rather a lock
|
||||
/// to synchronise all accesses to the state store.
|
||||
pub fn state_store_lock(&self) -> &Mutex<()> {
|
||||
self.state_store.lock()
|
||||
}
|
||||
|
||||
/// Receive a response from a sync call.
|
||||
@@ -772,7 +786,7 @@ impl BaseClient {
|
||||
context.state_changes.ambiguity_maps = ambiguity_cache.cache;
|
||||
|
||||
{
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
@@ -795,7 +809,11 @@ impl BaseClient {
|
||||
.await;
|
||||
|
||||
// Save the new display name updates if any.
|
||||
processors::changes::save_only(context, &self.state_store).await?;
|
||||
{
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
processors::changes::save_only(context, &self.state_store).await?;
|
||||
}
|
||||
|
||||
for (room_id, member_ids) in updated_members_in_room {
|
||||
if let Some(room) = self.get_room(&room_id) {
|
||||
@@ -916,18 +934,21 @@ impl BaseClient {
|
||||
|
||||
context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
|
||||
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_members_synced();
|
||||
context.state_changes.add_room(room_info);
|
||||
{
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&self.state_store,
|
||||
&self.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_members_synced();
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&self.state_store,
|
||||
&self.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
|
||||
|
||||
@@ -1041,7 +1062,15 @@ impl BaseClient {
|
||||
self.state_store.forget_room(room_id).await?;
|
||||
|
||||
// Remove the room in the event cache store too.
|
||||
self.event_cache_store().lock().await?.remove_room(room_id).await?;
|
||||
match self.event_cache_store().lock().await? {
|
||||
// If the lock is clear, we can do the operation as expected.
|
||||
// If the lock is dirty, we can ignore to refresh the state, we just need to remove a
|
||||
// room. Also, we must not mark the lock as non-dirty because other operations may be
|
||||
// critical and may need to refresh the `EventCache`' state.
|
||||
EventCacheStoreLockState::Clean(guard) | EventCacheStoreLockState::Dirty(guard) => {
|
||||
guard.remove_room(room_id).await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use matrix_sdk_common::store_locks::LockStoreError;
|
||||
use matrix_sdk_common::cross_process_lock::CrossProcessLockError;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
|
||||
use thiserror::Error;
|
||||
@@ -51,7 +51,7 @@ pub enum Error {
|
||||
|
||||
/// An error happened while attempting to lock the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheLock(#[from] LockStoreError),
|
||||
EventCacheLock(#[from] CrossProcessLockError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStore` implementations.
|
||||
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
UnableToDecryptInfo, UnableToDecryptReason, VerificationState,
|
||||
},
|
||||
linked_chunk::{
|
||||
ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update, lazy_loader,
|
||||
@@ -29,24 +32,17 @@ use matrix_sdk_common::{
|
||||
};
|
||||
use matrix_sdk_test::{ALICE, DEFAULT_TEST_ROOM_ID, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventId, RoomId,
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
event_id,
|
||||
EventId, RoomId, event_id,
|
||||
events::{
|
||||
AnyMessageLikeEvent, AnyTimelineEvent,
|
||||
relation::RelationType,
|
||||
room::{MediaSource, message::RoomMessageEventContentWithoutRelation},
|
||||
AnyMessageLikeEvent, AnyTimelineEvent, relation::RelationType,
|
||||
room::message::RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
mxc_uri,
|
||||
push::Action,
|
||||
room_id, uint,
|
||||
room_id,
|
||||
};
|
||||
|
||||
use super::{DynEventCacheStore, media::IgnoreMediaRetentionPolicy};
|
||||
use crate::{
|
||||
event_cache::{Gap, store::DEFAULT_CHUNK_CAPACITY},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
use super::DynEventCacheStore;
|
||||
use crate::event_cache::{Gap, store::DEFAULT_CHUNK_CAPACITY};
|
||||
|
||||
/// Create a test event with all data filled, for testing that linked chunk
|
||||
/// correctly stores event data.
|
||||
@@ -56,6 +52,24 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
|
||||
make_test_event_with_event_id(room_id, content, None)
|
||||
}
|
||||
|
||||
/// Create a `m.room.encrypted` test event with all data filled, for testing
|
||||
/// that linked chunk correctly stores event data for encrypted events.
|
||||
pub fn make_encrypted_test_event(room_id: &RoomId, session_id: &str) -> TimelineEvent {
|
||||
let device_id = "DEVICEID";
|
||||
let builder = EventFactory::new()
|
||||
.encrypted("", "curve_key", device_id, session_id)
|
||||
.room(room_id)
|
||||
.sender(*ALICE);
|
||||
|
||||
let event = builder.into_raw();
|
||||
let utd_info = UnableToDecryptInfo {
|
||||
session_id: Some(session_id.to_owned()),
|
||||
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
|
||||
};
|
||||
|
||||
TimelineEvent::from_utd(event, utd_info)
|
||||
}
|
||||
|
||||
/// Same as [`make_test_event`], with an extra event id.
|
||||
pub fn make_test_event_with_event_id(
|
||||
room_id: &RoomId,
|
||||
@@ -118,12 +132,6 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
/// `event_cache_store_integration_tests!` macro.
|
||||
#[allow(async_fn_in_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);
|
||||
@@ -154,6 +162,12 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test that finding event relations works as expected.
|
||||
async fn test_find_event_relations(&self);
|
||||
|
||||
/// Test that getting all events in a room works as expected.
|
||||
async fn test_get_room_events(&self);
|
||||
|
||||
/// Test that getting events in a room of a certain type works as expected.
|
||||
async fn test_get_room_events_filtered(&self);
|
||||
|
||||
/// Test that saving an event works as expected.
|
||||
async fn test_save_event(&self);
|
||||
|
||||
@@ -163,190 +177,6 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
}
|
||||
|
||||
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");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
@@ -1116,6 +946,134 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_get_room_events(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let another_linked_chunk_id = LinkedChunkId::Room(another_room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let event_comte = event("comté");
|
||||
let event_gruyere = event("gruyère");
|
||||
let event_stilton = event("stilton");
|
||||
|
||||
// Add one event in one room.
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_comte.clone(), event_gruyere.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add an event in a different room.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_stilton.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now let's find the events.
|
||||
let events = self
|
||||
.get_room_events(room_id, None, None)
|
||||
.await
|
||||
.expect("failed to query for room events");
|
||||
|
||||
assert_eq!(events.len(), 2);
|
||||
|
||||
let got_ids: Vec<_> = events.into_iter().map(|ev| ev.event_id()).collect();
|
||||
let expected_ids = vec![event_comte.event_id(), event_gruyere.event_id()];
|
||||
|
||||
for expected in expected_ids {
|
||||
assert!(
|
||||
got_ids.contains(&expected),
|
||||
"Expected event {expected:?} not in got events: {got_ids:?}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_get_room_events_filtered(&self) {
|
||||
macro_rules! assert_expected_events {
|
||||
($events:expr, [$($item:expr),* $(,)?]) => {{
|
||||
let got_ids: BTreeSet<_> = $events.into_iter().map(|ev| ev.event_id().unwrap()).collect();
|
||||
let expected_ids = BTreeSet::from([$($item.event_id().unwrap()),*]);
|
||||
|
||||
assert_eq!(got_ids, expected_ids);
|
||||
}};
|
||||
}
|
||||
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
let another_linked_chunk_id = LinkedChunkId::Room(another_room_id);
|
||||
|
||||
let event = |session_id: &str| make_encrypted_test_event(room_id, session_id);
|
||||
|
||||
let first_event = event("session_1");
|
||||
let second_event = event("session_2");
|
||||
let third_event = event("session_3");
|
||||
let fourth_event = make_test_event(room_id, "It's a secret to everybody");
|
||||
|
||||
// Add one event in one room.
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![first_event.clone(), second_event.clone(), fourth_event.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add an event in a different room.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![third_event.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now let's find all the encrypted events of the first room.
|
||||
let events = self
|
||||
.get_room_events(room_id, Some("m.room.encrypted"), None)
|
||||
.await
|
||||
.expect("failed to query for room events");
|
||||
|
||||
assert_eq!(events.len(), 2);
|
||||
assert_expected_events!(events, [first_event, second_event]);
|
||||
|
||||
// Now let's find all the encrypted events which were encrypted using the first
|
||||
// session ID.
|
||||
let events = self
|
||||
.get_room_events(room_id, Some("m.room.encrypted"), Some("session_1"))
|
||||
.await
|
||||
.expect("failed to query for room events");
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_expected_events!(events, [first_event]);
|
||||
}
|
||||
|
||||
async fn test_save_event(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
@@ -1333,20 +1291,6 @@ macro_rules! event_cache_store_integration_tests {
|
||||
|
||||
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 =
|
||||
@@ -1410,6 +1354,20 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_find_event_relations().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room_events() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_get_room_events().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room_events_filtered() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_get_room_events_filtered().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_save_event() {
|
||||
let event_cache_store =
|
||||
@@ -1450,57 +1408,57 @@ macro_rules! event_cache_store_integration_tests_time {
|
||||
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);
|
||||
assert_eq!(acquired0, Some(1)); // first lock generation
|
||||
|
||||
// Should extend the lease automatically (same holder).
|
||||
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired2);
|
||||
assert_eq!(acquired2, Some(1)); // same lock generation
|
||||
|
||||
// Should extend the lease automatically (same holder + time is ok).
|
||||
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired3);
|
||||
assert_eq!(acquired3, Some(1)); // same lock generation
|
||||
|
||||
// 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);
|
||||
assert!(acquired4.is_none()); // not acquired
|
||||
|
||||
// Even if we insist.
|
||||
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired5);
|
||||
assert!(acquired5.is_none()); // not acquired
|
||||
|
||||
// That's a nice test we got here, go take a little nap.
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired55);
|
||||
assert!(acquired55.is_none()); // not acquired
|
||||
|
||||
// Ok you can take another nap then.
|
||||
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);
|
||||
assert_eq!(acquired6, Some(2)); // new lock generation!
|
||||
|
||||
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);
|
||||
assert_eq!(acquired7, Some(3)); // new lock generation!
|
||||
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// But when we take a longer lease...
|
||||
// But when we take a longer lease…
|
||||
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired8);
|
||||
assert_eq!(acquired8, Some(4)); // new lock generation!
|
||||
|
||||
// It blocks the other user.
|
||||
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(!acquired9);
|
||||
assert!(acquired9.is_none()); // not acquired
|
||||
|
||||
// We can hold onto our lease.
|
||||
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired10);
|
||||
assert_eq!(acquired10, Some(4)); // same lock generation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,34 +14,25 @@
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
num::NonZeroUsize,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
cross_process_lock::{
|
||||
CrossProcessLockGeneration,
|
||||
memory_store_helper::{Lease, try_take_leased_lock},
|
||||
},
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update, relational::RelationalLinkedChunk,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
EventCacheStore, EventCacheStoreError, Result, compute_filters_string, extract_event_relation,
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaRequestParameters, UniqueKey as _},
|
||||
};
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result, extract_event_relation};
|
||||
use crate::event_cache::{Event, Gap};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
@@ -49,55 +40,21 @@ use crate::{
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStore {
|
||||
inner: Arc<StdRwLock<MemoryStoreInner>>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
leases: HashMap<String, Lease>,
|
||||
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: SystemTime,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
// Given that the store is empty, we won't need to clean it up right away.
|
||||
let last_media_cleanup_time = SystemTime::now();
|
||||
let media_service = MediaService::new();
|
||||
media_service.restore(None, Some(last_media_cleanup_time));
|
||||
|
||||
Self {
|
||||
inner: Arc::new(StdRwLock::new(MemoryStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
media_retention_policy: None,
|
||||
last_media_cleanup_time,
|
||||
})),
|
||||
media_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +76,7 @@ impl EventCacheStore for MemoryStore {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
@@ -238,14 +195,13 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.filter_map(|(event, pos)| {
|
||||
// Must have a relation.
|
||||
let (related_to, rel_type) = extract_event_relation(event.raw())?;
|
||||
let rel_type = RelationType::from(rel_type.as_str());
|
||||
|
||||
// Must relate to the target item.
|
||||
if related_to != event_id {
|
||||
@@ -264,6 +220,28 @@ impl EventCacheStore for MemoryStore {
|
||||
Ok(related_events)
|
||||
}
|
||||
|
||||
async fn get_room_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event: Vec<_> = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.map(|(event, _pos)| event.clone())
|
||||
.filter(|e| {
|
||||
event_type
|
||||
.is_none_or(|event_type| Some(event_type) == e.kind.event_type().as_deref())
|
||||
})
|
||||
.filter(|e| session_id.is_none_or(|s| Some(s) == e.kind.session_id()))
|
||||
.collect();
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
|
||||
if event.event_id().is_none() {
|
||||
error!(%room_id, "Trying to save an event with no ID");
|
||||
@@ -273,312 +251,20 @@ impl EventCacheStore for MemoryStore {
|
||||
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();
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
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_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), 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() as u64) {
|
||||
// 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() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
// 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(
|
||||
(0u64, 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() as u64) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
inner.last_media_cleanup_time = current_time;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unused_imports)] // There seems to be a false positive when importing the test macros.
|
||||
mod tests {
|
||||
use super::{MemoryStore, Result};
|
||||
use crate::event_cache_store_media_integration_tests;
|
||||
use crate::{event_cache_store_integration_tests, event_cache_store_integration_tests_time};
|
||||
|
||||
async fn get_event_cache_store() -> Result<MemoryStore> {
|
||||
Ok(MemoryStore::new())
|
||||
@@ -586,5 +272,4 @@ mod tests {
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
event_cache_store_integration_tests_time!();
|
||||
event_cache_store_media_integration_tests!(with_media_size_tests);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// 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
|
||||
//! The event cache stores holds events 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
|
||||
@@ -24,19 +24,15 @@ 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,
|
||||
use matrix_sdk_common::cross_process_lock::{
|
||||
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
|
||||
MappedCrossProcessLockState, TryLock,
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
use ruma::{
|
||||
OwnedEventId,
|
||||
events::{AnySyncTimelineEvent, relation::RelationType},
|
||||
serde::Raw,
|
||||
};
|
||||
use ruma::{OwnedEventId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
@@ -50,7 +46,7 @@ pub use self::{
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `EventCacheStore`.
|
||||
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
|
||||
cross_process_lock: Arc<CrossProcessLock<LockableEventCacheStore>>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
@@ -69,7 +65,7 @@ impl EventCacheStoreLock {
|
||||
/// Create a new lock around the [`EventCacheStore`].
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessStoreLock::new`].
|
||||
/// [`CrossProcessLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
where
|
||||
S: IntoEventCacheStore,
|
||||
@@ -77,7 +73,7 @@ impl EventCacheStoreLock {
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
|
||||
cross_process_lock: Arc::new(CrossProcessLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
@@ -86,38 +82,62 @@ impl EventCacheStoreLock {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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?;
|
||||
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
|
||||
pub async fn lock(&self) -> Result<EventCacheStoreLockState, CrossProcessLockError> {
|
||||
let lock_state =
|
||||
self.cross_process_lock.spin_lock(None).await??.map(|cross_process_lock_guard| {
|
||||
EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.clone() }
|
||||
});
|
||||
|
||||
Ok(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
|
||||
Ok(lock_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// The equivalent of [`CrossProcessLockState`] but for the [`EventCacheStore`].
|
||||
///
|
||||
/// [`CrossProcessLockState`]: matrix_sdk_common::cross_process_lock::CrossProcessLockState
|
||||
pub type EventCacheStoreLockState = MappedCrossProcessLockState<EventCacheStoreLockGuard>;
|
||||
|
||||
/// 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> {
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLockGuard {
|
||||
/// The cross process lock guard.
|
||||
#[allow(unused)]
|
||||
cross_process_lock_guard: CrossProcessStoreLockGuard,
|
||||
cross_process_lock_guard: CrossProcessLockGuard,
|
||||
|
||||
/// A reference to the store.
|
||||
store: &'a DynEventCacheStore,
|
||||
store: Arc<DynEventCacheStore>,
|
||||
}
|
||||
|
||||
impl EventCacheStoreLockGuard {
|
||||
/// Forward to [`CrossProcessLockGuard::clear_dirty`].
|
||||
///
|
||||
/// This is an associated method to avoid colliding with the [`Deref`]
|
||||
/// implementation.
|
||||
pub fn clear_dirty(this: &Self) {
|
||||
this.cross_process_lock_guard.clear_dirty();
|
||||
}
|
||||
|
||||
/// Force to [`CrossProcessLockGuard::is_dirty`].
|
||||
pub fn is_dirty(this: &Self) -> bool {
|
||||
this.cross_process_lock_guard.is_dirty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
|
||||
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<'_> {
|
||||
impl Deref for EventCacheStoreLockGuard {
|
||||
type Target = DynEventCacheStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.store
|
||||
self.store.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,15 +197,21 @@ impl EventCacheStoreError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheStoreError> for CrossProcessLockError {
|
||||
fn from(value: EventCacheStoreError) -> Self {
|
||||
Self::TryLock(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// A type that wraps the [`EventCacheStore`] but implements [`TryLock`] to
|
||||
/// make it usable inside the cross process lock.
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
|
||||
|
||||
impl BackingStore for LockableEventCacheStore {
|
||||
impl TryLock for LockableEventCacheStore {
|
||||
type LockError = EventCacheStoreError;
|
||||
|
||||
async fn try_lock(
|
||||
@@ -193,7 +219,7 @@ impl BackingStore for LockableEventCacheStore {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> std::result::Result<bool, Self::LockError> {
|
||||
) -> std::result::Result<Option<CrossProcessLockGeneration>, Self::LockError> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
@@ -226,18 +252,3 @@ pub fn extract_event_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(Owne
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the list of string filters to be applied when looking for an event's
|
||||
/// relations.
|
||||
// TODO: get Ruma fix from https://github.com/ruma/ruma/pull/2052, and get rid of this function
|
||||
// then.
|
||||
pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<String>> {
|
||||
filters.map(|filter| {
|
||||
filter
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if *f == RelationType::Replacement { "m.replace".to_owned() } else { f.to_string() }
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,21 +17,16 @@ use std::{fmt, sync::Arc};
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
AsyncTraitDeps,
|
||||
cross_process_lock::CrossProcessLockGeneration,
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update,
|
||||
},
|
||||
};
|
||||
use ruma::{EventId, MxcUri, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
|
||||
use super::{
|
||||
EventCacheStoreError,
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
};
|
||||
use super::EventCacheStoreError;
|
||||
use crate::event_cache::{Event, Gap};
|
||||
|
||||
/// A default capacity for linked chunks, when manipulating in conjunction with
|
||||
/// an `EventCacheStore` implementation.
|
||||
@@ -52,7 +47,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error>;
|
||||
) -> Result<Option<CrossProcessLockGeneration>, 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
|
||||
@@ -160,6 +155,17 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error>;
|
||||
|
||||
/// Get all events in this room.
|
||||
///
|
||||
/// This method must return events saved either in any linked chunks, *or*
|
||||
/// events saved "out-of-band" with the [`Self::save_event`] method.
|
||||
async fn get_room_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<Vec<Event>, Self::Error>;
|
||||
|
||||
/// Save an event, that might or might not be part of an existing linked
|
||||
/// chunk.
|
||||
///
|
||||
@@ -170,128 +176,16 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// without causing an error.
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
/// Perform database optimizations if any are available, i.e. vacuuming in
|
||||
/// SQLite.
|
||||
///
|
||||
/// # 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>;
|
||||
/// **Warning:** this was added to check if SQLite fragmentation was the
|
||||
/// source of performance issues, **DO NOT use in production**.
|
||||
#[doc(hidden)]
|
||||
async fn optimize(&self) -> 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>;
|
||||
/// Returns the size of the store in bytes, if known.
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
@@ -314,7 +208,7 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -387,73 +281,26 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_room_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
self.0.get_room_events(room_id, event_type, session_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
|
||||
self.0.save_event(room_id, event).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 optimize(&self) -> Result<(), Self::Error> {
|
||||
self.0.optimize().await.map_err(Into::into)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
Ok(self.0.get_size().await.map_err(Into::into)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use ruma::{
|
||||
UserId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
call::invite::SyncCallInviteEvent,
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::{
|
||||
@@ -16,6 +16,7 @@ use ruma::{
|
||||
message::{MessageType, SyncRoomMessageEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
rtc::notification::SyncRtcNotificationEvent,
|
||||
sticker::SyncStickerEvent,
|
||||
},
|
||||
};
|
||||
@@ -41,6 +42,40 @@ pub enum LatestEventValue {
|
||||
LocalCannotBeSent(LocalLatestEventValue),
|
||||
}
|
||||
|
||||
impl LatestEventValue {
|
||||
/// Get the timestamp of the [`LatestEventValue`].
|
||||
///
|
||||
/// If it's [`None`], it returns `None`. If it's [`Remote`], it returns the
|
||||
/// [`TimelineEvent::timestamp`]. If it's [`LocalIsSending`] or
|
||||
/// [`LocalCannotBeSent`], it returns the
|
||||
/// [`LocalLatestEventValue::timestamp`] value.
|
||||
///
|
||||
/// [`None`]: LatestEventValue::None
|
||||
/// [`Remote`]: LatestEventValue::Remote
|
||||
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
|
||||
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
|
||||
pub fn timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::Remote(remote_latest_event_value) => remote_latest_event_value.timestamp(),
|
||||
Self::LocalIsSending(LocalLatestEventValue { timestamp, .. })
|
||||
| Self::LocalCannotBeSent(LocalLatestEventValue { timestamp, .. }) => Some(*timestamp),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the [`LatestEventValue`] represents a local value or not,
|
||||
/// i.e. it is [`LocalIsSending`] or [`LocalCannotBeSent`].
|
||||
///
|
||||
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
|
||||
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
|
||||
pub fn is_local(&self) -> bool {
|
||||
match self {
|
||||
Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) => true,
|
||||
Self::None | Self::Remote(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the value for [`LatestEventValue::Remote`].
|
||||
pub type RemoteLatestEventValue = TimelineEvent;
|
||||
|
||||
@@ -55,6 +90,73 @@ pub struct LocalLatestEventValue {
|
||||
pub content: SerializableEventContent,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_latest_event_value {
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
|
||||
serde::Raw,
|
||||
uint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{LatestEventValue, LocalLatestEventValue, RemoteLatestEventValue};
|
||||
use crate::store::SerializableEventContent;
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_none() {
|
||||
let value = LatestEventValue::None;
|
||||
|
||||
assert_eq!(value.timestamp(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_remote() {
|
||||
let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
|
||||
Raw::from_json_string(
|
||||
json!({
|
||||
"content": RoomMessageEventContent::text_plain("raclette"),
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ev0",
|
||||
"room_id": "!r0",
|
||||
"origin_server_ts": 42,
|
||||
"sender": "@mnt_io:matrix.org",
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_local_is_sending() {
|
||||
let value = LatestEventValue::LocalIsSending(LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_local_cannot_be_sent() {
|
||||
let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a decision about whether an event could be stored as the latest
|
||||
/// event in a room. Variants starting with Yes indicate that this message could
|
||||
/// be stored, and provide the inner event information, and those starting with
|
||||
@@ -73,7 +175,7 @@ pub enum PossibleLatestEvent<'a> {
|
||||
YesCallInvite(&'a SyncCallInviteEvent),
|
||||
|
||||
/// This message is suitable - it's a call notification
|
||||
YesCallNotify(&'a SyncCallNotifyEvent),
|
||||
YesRtcNotification(&'a SyncRtcNotificationEvent),
|
||||
|
||||
/// This state event is suitable - it's a knock membership change
|
||||
/// that can be handled by the current user.
|
||||
@@ -133,8 +235,8 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
PossibleLatestEvent::YesCallInvite(invite)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(notify)) => {
|
||||
PossibleLatestEvent::YesCallNotify(notify)
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RtcNotification(notify)) => {
|
||||
PossibleLatestEvent::YesRtcNotification(notify)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
|
||||
@@ -334,34 +436,26 @@ impl LatestEvent {
|
||||
mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches::assert_matches;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::serde::Raw;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
|
||||
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
call::{
|
||||
SessionDescription,
|
||||
invite::{CallInviteEventContent, SyncCallInviteEvent},
|
||||
notify::{
|
||||
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
|
||||
},
|
||||
},
|
||||
SyncMessageLikeEvent,
|
||||
call::{SessionDescription, invite::CallInviteEventContent},
|
||||
poll::{
|
||||
unstable_response::{
|
||||
SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
|
||||
},
|
||||
unstable_response::UnstablePollResponseEventContent,
|
||||
unstable_start::{
|
||||
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
|
||||
UnstablePollAnswer, UnstablePollStartContentBlock,
|
||||
NewUnstablePollStartEventContent, UnstablePollAnswer,
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
relation::Replacement,
|
||||
@@ -369,17 +463,20 @@ mod tests {
|
||||
ImageInfo, MediaSource,
|
||||
encrypted::{
|
||||
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
|
||||
SyncRoomEncryptedEvent,
|
||||
},
|
||||
message::{
|
||||
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
|
||||
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
Relation, RoomMessageEventContent,
|
||||
},
|
||||
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
sticker::{StickerEventContent, SyncStickerEvent},
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
owned_event_id, owned_mxc_uri, user_id,
|
||||
};
|
||||
use ruma::{
|
||||
events::rtc::notification::{NotificationType, RtcNotificationEventContent},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -390,20 +487,13 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomMessageEventContent::new(MessageType::Image(
|
||||
ImageMessageEventContent::new(
|
||||
"".to_owned(),
|
||||
MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
|
||||
),
|
||||
)),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::new(
|
||||
"".to_owned(),
|
||||
MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
|
||||
))))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
@@ -415,19 +505,13 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_polls_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
|
||||
SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
|
||||
"do you like rust?",
|
||||
vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
|
||||
))
|
||||
.into(),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
|
||||
"do you like rust?",
|
||||
vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
|
||||
)))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
@@ -439,20 +523,15 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
|
||||
SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: CallInviteEventContent::new(
|
||||
"call_id".into(),
|
||||
UInt::new(123).unwrap(),
|
||||
SessionDescription::new("".into(), "".into()),
|
||||
VoipVersionId::V1,
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(CallInviteEventContent::new(
|
||||
"call_id".into(),
|
||||
UInt::new(123).unwrap(),
|
||||
SessionDescription::new("".into(), "".into()),
|
||||
VoipVersionId::V1,
|
||||
))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
@@ -462,22 +541,16 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
|
||||
SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: CallNotifyEventContent::new(
|
||||
"call_id".into(),
|
||||
ApplicationType::Call,
|
||||
NotifyType::Ring,
|
||||
Mentions::new(),
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RtcNotificationEventContent::new(
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
Duration::new(30, 0),
|
||||
NotificationType::Ring,
|
||||
))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
|
||||
PossibleLatestEvent::YesRtcNotification(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
@@ -485,19 +558,14 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: StickerEventContent::new(
|
||||
"sticker!".to_owned(),
|
||||
ImageInfo::new(),
|
||||
owned_mxc_uri!("mxc://example.com/1"),
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(StickerEventContent::new(
|
||||
"sticker!".to_owned(),
|
||||
ImageInfo::new(),
|
||||
owned_mxc_uri!("mxc://example.com/1"),
|
||||
))
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
@@ -508,19 +576,13 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event =
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
|
||||
SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: UnstablePollResponseEventContent::new(
|
||||
vec![String::from("option1")],
|
||||
owned_event_id!("$1"),
|
||||
),
|
||||
event_id: owned_event_id!("$2"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(UnstablePollResponseEventContent::new(
|
||||
vec![String::from("option1")],
|
||||
owned_event_id!("$1"),
|
||||
))
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
@@ -531,25 +593,10 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
let room_redaction_event = serde_json::from_value(json!({
|
||||
"content": {},
|
||||
"event_id": "$redaction",
|
||||
"sender": "@x:y.za",
|
||||
"origin_server_ts": 223543,
|
||||
"unsigned": { "reason": "foo" }
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent {
|
||||
content: RedactedRoomMessageEventContent::new(),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: RedactedUnsigned::new(room_redaction_event),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.redacted(user_id!("@x:y.za"), RedactedRoomMessageEventContent::new())
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
@@ -560,20 +607,16 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomEncryptedEventContent::new(
|
||||
EncryptedEventScheme::OlmV1Curve25519AesSha2(
|
||||
OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()),
|
||||
),
|
||||
None,
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomEncryptedEventContent::new(
|
||||
EncryptedEventScheme::OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content::new(
|
||||
BTreeMap::new(),
|
||||
"".to_owned(),
|
||||
)),
|
||||
None,
|
||||
))
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
@@ -584,16 +627,11 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
|
||||
content: RoomTopicEventContent::new("".to_owned()),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: StateUnsigned::new(),
|
||||
state_key: EmptyStateKey,
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomTopicEventContent::new("".to_owned()))
|
||||
.state_key("")
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
@@ -610,15 +648,7 @@ mod tests {
|
||||
RoomMessageEventContent::text_plain("Hello, world!").into(),
|
||||
)));
|
||||
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: event_content,
|
||||
event_id: owned_event_id!("$2"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new().sender(user_id!("@a:b.c")).event(event_content).into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
@@ -631,22 +661,17 @@ mod tests {
|
||||
fn test_verification_requests_are_unsuitable() {
|
||||
use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
|
||||
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomMessageEventContent::new(MessageType::VerificationRequest(
|
||||
KeyVerificationRequestEventContent::new(
|
||||
"body".to_owned(),
|
||||
vec![],
|
||||
device_id!("device_id").to_owned(),
|
||||
user_id!("@user_id:example.com").to_owned(),
|
||||
),
|
||||
)),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomMessageEventContent::new(MessageType::VerificationRequest(
|
||||
KeyVerificationRequestEventContent::new(
|
||||
"body".to_owned(),
|
||||
vec![],
|
||||
device_id!("device_id").to_owned(),
|
||||
user_id!("@user_id:example.com").to_owned(),
|
||||
),
|
||||
)))
|
||||
.into();
|
||||
|
||||
assert_let!(
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType =
|
||||
@@ -688,6 +713,7 @@ mod tests {
|
||||
}
|
||||
},
|
||||
"thread_summary": "None",
|
||||
"timestamp": null,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(target_family = "wasm", allow(clippy::arc_with_non_send_sync))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
@@ -45,6 +45,9 @@ pub mod sync;
|
||||
mod test_utils;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
pub mod recent_emojis;
|
||||
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
@@ -57,12 +60,13 @@ pub use once_cell;
|
||||
pub use room::{
|
||||
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
|
||||
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomState,
|
||||
RoomStateFilter, SuccessorRoom, apply_redaction,
|
||||
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp,
|
||||
RoomState, RoomStateFilter, SuccessorRoom, apply_redaction,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
StateStoreDataValue, StoreError, ThreadSubscriptionCatchupToken,
|
||||
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
|
||||
QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
|
||||
ThreadSubscriptionCatchupToken,
|
||||
};
|
||||
pub use utils::{
|
||||
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
|
||||
// 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.
|
||||
|
||||
//! Media store and common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
|
||||
|
||||
pub mod store;
|
||||
|
||||
use ruma::{
|
||||
MxcUri, UInt,
|
||||
+390
-55
@@ -12,26 +12,28 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStoreMedia`
|
||||
//! Trait and macro of integration tests for `MediaStoreInner`
|
||||
//! implementations.
|
||||
|
||||
use ruma::{
|
||||
events::room::MediaSource,
|
||||
media::Method,
|
||||
mxc_uri, owned_mxc_uri,
|
||||
time::{Duration, SystemTime},
|
||||
uint,
|
||||
};
|
||||
|
||||
use super::{
|
||||
EventCacheStoreMedia, MediaRetentionPolicy, media_service::IgnoreMediaRetentionPolicy,
|
||||
use super::{MediaRetentionPolicy, MediaStoreInner, media_service::IgnoreMediaRetentionPolicy};
|
||||
use crate::media::{
|
||||
MediaFormat, MediaRequestParameters, MediaThumbnailSettings, store::MediaStore,
|
||||
};
|
||||
use crate::media::{MediaFormat, MediaRequestParameters};
|
||||
|
||||
/// [`EventCacheStoreMedia`] integration tests.
|
||||
/// [`MediaStoreInner`] integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `event_cache_store_media_integration_tests!` macro.
|
||||
/// `media_store_inner_integration_tests!` macro.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreMediaIntegrationTests {
|
||||
pub trait MediaStoreInnerIntegrationTests {
|
||||
/// Test media retention policy storage.
|
||||
async fn test_store_media_retention_policy(&self);
|
||||
|
||||
@@ -56,9 +58,9 @@ pub trait EventCacheStoreMediaIntegrationTests {
|
||||
async fn test_store_last_media_cleanup_time(&self);
|
||||
}
|
||||
|
||||
impl<Store> EventCacheStoreMediaIntegrationTests for Store
|
||||
impl<Store> MediaStoreInnerIntegrationTests for Store
|
||||
where
|
||||
Store: EventCacheStoreMedia + std::fmt::Debug,
|
||||
Store: MediaStoreInner + std::fmt::Debug,
|
||||
{
|
||||
async fn test_store_media_retention_policy(&self) {
|
||||
let stored = self.media_retention_policy_inner().await.unwrap();
|
||||
@@ -138,7 +140,7 @@ where
|
||||
assert!(stored.is_some());
|
||||
|
||||
// A cleanup doesn't have any effect.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
|
||||
assert!(stored.is_some());
|
||||
@@ -149,7 +151,7 @@ where
|
||||
let policy = MediaRetentionPolicy::empty().with_max_file_size(Some(100));
|
||||
|
||||
// The cleanup removes the average media.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
|
||||
assert!(stored.is_none());
|
||||
@@ -217,7 +219,7 @@ where
|
||||
.with_max_file_size(Some(1000));
|
||||
|
||||
// The cleanup removes the average media.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
|
||||
assert!(stored.is_none());
|
||||
@@ -395,7 +397,7 @@ where
|
||||
|
||||
// Cleanup removes the oldest content first.
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small_1, time).await.unwrap();
|
||||
@@ -481,7 +483,7 @@ where
|
||||
// before.
|
||||
time += Duration::from_secs(1);
|
||||
tracing::info!(?self, "before");
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
tracing::info!(?self, "after");
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small_1, time).await.unwrap();
|
||||
@@ -602,7 +604,7 @@ where
|
||||
assert_eq!(time, SystemTime::UNIX_EPOCH + Duration::from_secs(10));
|
||||
|
||||
// Cleanup has no effect, nothing has expired.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -629,7 +631,7 @@ where
|
||||
time += Duration::from_secs(26);
|
||||
|
||||
// Cleanup removes the two oldest media contents.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -745,7 +747,7 @@ where
|
||||
|
||||
// Because the big and average contents are ignored, cleanup has no effect.
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
|
||||
@@ -763,7 +765,7 @@ where
|
||||
.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
|
||||
@@ -782,7 +784,7 @@ where
|
||||
.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
|
||||
@@ -892,7 +894,7 @@ where
|
||||
time += Duration::from_secs(120);
|
||||
|
||||
// Cleanup removes all the media contents that are not ignored.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -922,7 +924,7 @@ where
|
||||
time += Duration::from_secs(120);
|
||||
|
||||
// Cleanup removes the remaining media contents.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -947,21 +949,21 @@ where
|
||||
|
||||
// With an empty policy.
|
||||
let policy = MediaRetentionPolicy::empty();
|
||||
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
|
||||
self.clean_inner(policy, new_time).await.unwrap();
|
||||
|
||||
let stored = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
assert_eq!(stored, initial);
|
||||
|
||||
// With the default policy.
|
||||
let policy = MediaRetentionPolicy::default();
|
||||
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
|
||||
self.clean_inner(policy, new_time).await.unwrap();
|
||||
|
||||
let stored = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
assert_eq!(stored, Some(new_time));
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your [`EventCacheStoreMedia`] implementation to run
|
||||
/// Macro building to allow your [`MediaStoreInner`] implementation to run
|
||||
/// the entire tests suite locally.
|
||||
///
|
||||
/// Can be run with the `with_media_size_tests` argument to include more tests
|
||||
@@ -969,91 +971,424 @@ where
|
||||
/// recommended to run those in encrypted stores because the size of the
|
||||
/// encrypted content may vary compared to what the tests expect.
|
||||
///
|
||||
/// You need to provide an `async fn get_event_cache_store() ->
|
||||
/// event_cache::store::Result<Store>` that provides a fresh event cache store
|
||||
/// that implements `EventCacheStoreMedia` on the same level you invoke the
|
||||
/// You need to provide an `async fn get_media_store() ->
|
||||
/// media::store::Result<Store>` that provides a fresh media store
|
||||
/// that implements `MediaStoreInner` 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,
|
||||
/// # use matrix_sdk_base::media::store::{
|
||||
/// # MediaStore,
|
||||
/// # MemoryMediaStore as MyStore,
|
||||
/// # Result as MediaStoreResult,
|
||||
/// # };
|
||||
///
|
||||
/// #[cfg(test)]
|
||||
/// mod tests {
|
||||
/// use super::{EventCacheStoreResult, MyStore};
|
||||
/// use super::{MediaStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_event_cache_store() -> EventCacheStoreResult<MyStore> {
|
||||
/// async fn get_media_store() -> MediaStoreResult<MyStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
/// event_cache_store_media_integration_tests!();
|
||||
/// media_store_inner_integration_tests!();
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_media_integration_tests {
|
||||
macro_rules! media_store_inner_integration_tests {
|
||||
(with_media_size_tests) => {
|
||||
mod event_cache_store_media_integration_tests {
|
||||
$crate::event_cache_store_media_integration_tests!(@inner);
|
||||
mod media_store_inner_integration_tests {
|
||||
$crate::media_store_inner_integration_tests!(@inner);
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_max_file_size() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_max_file_size().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_max_file_size().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_max_cache_size() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_max_cache_size().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_max_cache_size().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_ignore_max_size() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_ignore_max_size().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_ignore_max_size().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
() => {
|
||||
mod event_cache_store_media_integration_tests {
|
||||
$crate::event_cache_store_media_integration_tests!(@inner);
|
||||
mod media_store_inner_integration_tests {
|
||||
$crate::media_store_inner_integration_tests!(@inner);
|
||||
}
|
||||
};
|
||||
|
||||
(@inner) => {
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache::store::media::EventCacheStoreMediaIntegrationTests;
|
||||
use $crate::media::store::MediaStoreInnerIntegrationTests;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
use super::get_media_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_store_media_retention_policy() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_store_media_retention_policy().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_store_media_retention_policy().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_expiry() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_expiry().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_expiry().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_ignore_expiry() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_ignore_expiry().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_ignore_expiry().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_store_last_media_cleanup_time() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_store_last_media_cleanup_time().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_store_last_media_cleanup_time().await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// [`MediaStore`] integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `media_store_inner_integration_tests!` macro.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait MediaStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
|
||||
/// Test replacing a MXID.
|
||||
async fn test_replace_media_key(&self);
|
||||
}
|
||||
|
||||
impl<Store> MediaStoreIntegrationTests for Store
|
||||
where
|
||||
Store: MediaStore + std::fmt::Debug,
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your [`MediaStore`] implementation to run
|
||||
/// the entire tests suite locally.
|
||||
///
|
||||
/// You need to provide an `async fn get_media_store() ->
|
||||
/// media::store::Result<Store>` that provides a fresh media store
|
||||
/// that implements `MediaStoreInner` on the same level you invoke the
|
||||
/// macro.
|
||||
///
|
||||
/// ## Usage Example:
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_base::media::store::{
|
||||
/// # MediaStore,
|
||||
/// # MemoryMediaStore as MyStore,
|
||||
/// # Result as MediaStoreResult,
|
||||
/// # };
|
||||
///
|
||||
/// #[cfg(test)]
|
||||
/// mod tests {
|
||||
/// use super::{MediaStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_media_store() -> MediaStoreResult<MyStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
/// media_store_integration_tests!();
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! media_store_integration_tests {
|
||||
() => {
|
||||
mod media_store_integration_tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::media::store::integration_tests::MediaStoreIntegrationTests;
|
||||
|
||||
use super::get_media_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_content() {
|
||||
let media_store = get_media_store().await.unwrap();
|
||||
media_store.test_media_content().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_replace_media_key() {
|
||||
let media_store = get_media_store().await.unwrap();
|
||||
media_store.test_replace_media_key().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro generating tests for the media store, related to time (mostly
|
||||
/// for the cross-process lock).
|
||||
#[allow(unused_macros)]
|
||||
#[macro_export]
|
||||
macro_rules! media_store_integration_tests_time {
|
||||
() => {
|
||||
mod media_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
||||
use gloo_timers::future::sleep;
|
||||
use matrix_sdk_test::async_test;
|
||||
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
|
||||
use tokio::time::sleep;
|
||||
use $crate::media::store::MediaStore;
|
||||
|
||||
use super::get_media_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_lease_locks() {
|
||||
let store = get_media_store().await.unwrap();
|
||||
|
||||
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert_eq!(acquired0, Some(1)); // first lock generation
|
||||
|
||||
// Should extend the lease automatically (same holder).
|
||||
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert_eq!(acquired2, Some(1)); // same lock generation
|
||||
|
||||
// Should extend the lease automatically (same holder + time is ok).
|
||||
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert_eq!(acquired3, Some(1)); // same lock generation
|
||||
|
||||
// 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.is_none()); // not acquired
|
||||
|
||||
// Even if we insist.
|
||||
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired5.is_none()); // not acquired
|
||||
|
||||
// That's a nice test we got here, go take a little nap.
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired55.is_none()); // not acquired
|
||||
|
||||
// Ok you can take another nap then.
|
||||
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_eq!(acquired6, Some(2)); // new lock generation!
|
||||
|
||||
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_eq!(acquired7, Some(3)); // new lock generation!
|
||||
|
||||
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_eq!(acquired8, Some(4)); // new lock generation!
|
||||
|
||||
// It blocks the other user.
|
||||
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired9.is_none()); // not acquired
|
||||
|
||||
// We can hold onto our lease.
|
||||
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert_eq!(acquired10, Some(4)); // same lock generation
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+8
-5
@@ -17,19 +17,22 @@
|
||||
//! 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()`].
|
||||
//! [`MediaStore::set_media_retention_policy()`]. Then call
|
||||
//! [`MediaStore::clean()`].
|
||||
//!
|
||||
//! 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
|
||||
//! [`MediaStore::set_media_retention_policy()`]: crate::media::store::MediaStore::set_media_retention_policy
|
||||
//! [`MediaStore::clean()`]: crate::media::store::MediaStore::clean
|
||||
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The retention policy for media content used by the [`EventCacheStore`].
|
||||
#[cfg(doc)]
|
||||
use crate::media::store::MediaStore;
|
||||
|
||||
/// The retention policy for media content used by the [`MediaStore`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
+47
-177
@@ -12,11 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
|
||||
SendOutsideWasm, SyncOutsideWasm,
|
||||
executor::{JoinHandle, spawn},
|
||||
locks::Mutex,
|
||||
};
|
||||
@@ -24,13 +23,13 @@ use ruma::{MxcUri, time::SystemTime};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::error;
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
|
||||
use super::{MediaRetentionPolicy, MediaStoreInner};
|
||||
use crate::media::MediaRequestParameters;
|
||||
|
||||
/// API for implementors of [`EventCacheStore`] to manage their media through
|
||||
/// their implementation of [`EventCacheStoreMedia`].
|
||||
/// API for implementors of [`MediaStore`] to manage their media through
|
||||
/// their implementation of [`MediaStoreInner`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
/// [`MediaStore`]: crate::media::store::MediaStore
|
||||
#[derive(Debug)]
|
||||
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
inner: Arc<MediaServiceInner<Time>>,
|
||||
@@ -122,10 +121,10 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn set_media_retention_policy<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
policy: MediaRetentionPolicy,
|
||||
@@ -148,7 +147,7 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
@@ -156,7 +155,7 @@ where
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn add_media_content<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -189,13 +188,13 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `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>(
|
||||
pub async fn set_ignore_media_retention_policy<Store: MediaStoreInner>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -208,10 +207,10 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn get_media_content<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -229,10 +228,10 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn get_media_content_for_uri<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
uri: &MxcUri,
|
||||
@@ -251,15 +250,12 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
pub async fn clean_up_media_cache<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
) -> Result<(), Store::Error> {
|
||||
self.clean_up_media_cache_inner(store, self.now()).await
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
pub async fn clean<Store: MediaStoreInner>(&self, store: &Store) -> Result<(), Store::Error> {
|
||||
self.clean_inner(store, self.now()).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner<Store: EventCacheStoreMedia>(
|
||||
async fn clean_inner<Store: MediaStoreInner>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
@@ -276,7 +272,7 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store.clean_up_media_cache_inner(policy, current_time).await?;
|
||||
store.clean_inner(policy, current_time).await?;
|
||||
|
||||
*self.inner.last_media_cleanup_time.lock() = Some(current_time);
|
||||
|
||||
@@ -290,7 +286,7 @@ where
|
||||
/// * The media retention policy's `cleanup_frequency` is set and enough
|
||||
/// time has passed since the last cleanup.
|
||||
/// * No other cleanup is running,
|
||||
fn maybe_spawn_automatic_media_cache_cleanup<Store: EventCacheStoreMedia + 'static>(
|
||||
fn maybe_spawn_automatic_media_cache_cleanup<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
@@ -320,7 +316,7 @@ where
|
||||
let store = store.clone();
|
||||
|
||||
let handle = spawn(async move {
|
||||
if let Err(error) = this.clean_up_media_cache_inner(&store, current_time).await {
|
||||
if let Err(error) = this.clean_inner(&store, current_time).await {
|
||||
error!("Failed to run automatic media cache cleanup: {error}");
|
||||
}
|
||||
});
|
||||
@@ -349,132 +345,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
|
||||
|
||||
/// 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 and to be stored as the time of the last media cache cleanup.
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// The time of the last media cache cleanup.
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
|
||||
}
|
||||
|
||||
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
|
||||
/// content.
|
||||
///
|
||||
@@ -544,18 +414,18 @@ mod tests {
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
|
||||
use crate::{
|
||||
event_cache::store::{EventCacheStoreError, media::MediaRetentionPolicy},
|
||||
media::{MediaFormat, MediaRequestParameters, UniqueKey},
|
||||
use super::{
|
||||
IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStoreInner,
|
||||
TimeProvider,
|
||||
};
|
||||
use crate::media::{MediaFormat, MediaRequestParameters, UniqueKey, store::MediaStoreError};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct MockEventCacheStoreMedia {
|
||||
inner: Arc<Mutex<MockEventCacheStoreMediaInner>>,
|
||||
struct MockMediaStoreInner {
|
||||
inner: Arc<Mutex<MockMediaStoreInnerInner>>,
|
||||
}
|
||||
|
||||
impl MockEventCacheStoreMedia {
|
||||
impl MockMediaStoreInner {
|
||||
/// Whether the store was accessed.
|
||||
fn accessed(&self) -> bool {
|
||||
self.inner.lock().accessed
|
||||
@@ -570,7 +440,7 @@ mod tests {
|
||||
///
|
||||
/// Should be called for every access to the inner store as it also sets
|
||||
/// the `accessed` boolean.
|
||||
fn inner(&self) -> MutexGuard<'_, MockEventCacheStoreMediaInner> {
|
||||
fn inner(&self) -> MutexGuard<'_, MockMediaStoreInnerInner> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.accessed = true;
|
||||
inner
|
||||
@@ -578,7 +448,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockEventCacheStoreMediaInner {
|
||||
struct MockMediaStoreInnerInner {
|
||||
/// Whether this store was accessed.
|
||||
///
|
||||
/// Must be set to `true` for any operation that unlocks the store.
|
||||
@@ -614,26 +484,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockEventCacheStoreMediaError;
|
||||
struct MockMediaStoreInnerError;
|
||||
|
||||
impl fmt::Display for MockEventCacheStoreMediaError {
|
||||
impl fmt::Display for MockMediaStoreInnerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "MockEventCacheStoreMediaError")
|
||||
write!(f, "MockMediaStoreInnerError")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MockEventCacheStoreMediaError {}
|
||||
impl std::error::Error for MockMediaStoreInnerError {}
|
||||
|
||||
impl From<MockEventCacheStoreMediaError> for EventCacheStoreError {
|
||||
fn from(value: MockEventCacheStoreMediaError) -> Self {
|
||||
impl From<MockMediaStoreInnerError> for MediaStoreError {
|
||||
fn from(value: MockMediaStoreInnerError) -> Self {
|
||||
Self::backend(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
|
||||
type Error = MockEventCacheStoreMediaError;
|
||||
impl MediaStoreInner for MockMediaStoreInner {
|
||||
type Error = MockMediaStoreInnerError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
@@ -736,7 +606,7 @@ mod tests {
|
||||
Ok(Some(media_content.content.clone()))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
async fn clean_inner(
|
||||
&self,
|
||||
_policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
@@ -787,7 +657,7 @@ mod tests {
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let store = MockMediaStoreInner::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// By default an empty policy is used.
|
||||
@@ -849,7 +719,7 @@ mod tests {
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
service.clean(&store).await.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
}
|
||||
@@ -877,7 +747,7 @@ mod tests {
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let store = MockMediaStoreInner::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Check that restoring the policy works.
|
||||
@@ -1011,7 +881,7 @@ mod tests {
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
service.clean(&store).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
}
|
||||
@@ -1034,7 +904,7 @@ mod tests {
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let store = MockMediaStoreInner::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Set an empty policy.
|
||||
@@ -0,0 +1,451 @@
|
||||
// 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::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
cross_process_lock::{
|
||||
CrossProcessLockGeneration,
|
||||
memory_store_helper::{Lease, try_take_leased_lock},
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri, time::SystemTime};
|
||||
|
||||
use super::Result;
|
||||
use crate::media::{
|
||||
MediaRequestParameters, UniqueKey as _,
|
||||
store::{
|
||||
IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore,
|
||||
MediaStoreError, MediaStoreInner,
|
||||
},
|
||||
};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `MediaStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryMediaStore {
|
||||
inner: Arc<StdRwLock<MemoryMediaStoreInner>>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryMediaStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, Lease>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: SystemTime,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
|
||||
|
||||
impl Default for MemoryMediaStore {
|
||||
fn default() -> Self {
|
||||
// Given that the store is empty, we won't need to clean it up right away.
|
||||
let last_media_cleanup_time = SystemTime::now();
|
||||
let media_service = MediaService::new();
|
||||
media_service.restore(None, Some(last_media_cleanup_time));
|
||||
|
||||
Self {
|
||||
inner: Arc::new(StdRwLock::new(MemoryMediaStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
media_retention_policy: None,
|
||||
last_media_cleanup_time,
|
||||
})),
|
||||
media_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryMediaStore {
|
||||
/// Create a new empty MemoryMediaStore
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl MediaStore for MemoryMediaStore {
|
||||
type Error = MediaStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
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::Error> {
|
||||
self.media_service.get_media_content(self, request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
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<(), Self::Error> {
|
||||
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(&self) -> Result<(), Self::Error> {
|
||||
self.media_service.clean(self).await
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl MediaStoreInner for MemoryMediaStore {
|
||||
type Error = MediaStoreError;
|
||||
|
||||
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() as u64) {
|
||||
// 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_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() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
// 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(
|
||||
(0u64, 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() as u64) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
inner.last_media_cleanup_time = current_time;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MemoryMediaStore, Result};
|
||||
use crate::{
|
||||
media_store_inner_integration_tests, media_store_integration_tests,
|
||||
media_store_integration_tests_time,
|
||||
};
|
||||
|
||||
async fn get_media_store() -> Result<MemoryMediaStore> {
|
||||
Ok(MemoryMediaStore::new())
|
||||
}
|
||||
|
||||
media_store_inner_integration_tests!();
|
||||
media_store_integration_tests!();
|
||||
media_store_integration_tests_time!();
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// 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.
|
||||
|
||||
//! The media store holds downloaded media when the cache was
|
||||
//! activated to save bandwidth at the cost of increased storage space usage.
|
||||
//!
|
||||
//! Implementing the `MediaStore` trait, you can plug any storage backend
|
||||
//! into the media store for the actual storage. By default this brings an
|
||||
//! in-memory store.
|
||||
|
||||
mod media_retention_policy;
|
||||
mod media_service;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
use std::fmt;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::cross_process_lock::{
|
||||
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
|
||||
CrossProcessLockState, TryLock,
|
||||
};
|
||||
use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner};
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::{MediaStoreInnerIntegrationTests, MediaStoreIntegrationTests};
|
||||
pub use self::{
|
||||
media_retention_policy::MediaRetentionPolicy,
|
||||
media_service::{IgnoreMediaRetentionPolicy, MediaService},
|
||||
memory_store::MemoryMediaStore,
|
||||
};
|
||||
|
||||
/// Media store specific error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MediaStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the media store: {0}")]
|
||||
Encryption(#[from] StoreEncryptionError),
|
||||
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// The store failed to serialize or deserialize some data.
|
||||
#[error("Error serializing or deserializing data from the media store: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl MediaStoreError {
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `MediaStoreError::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))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaStoreError> for CrossProcessLockError {
|
||||
fn from(value: MediaStoreError) -> Self {
|
||||
Self::TryLock(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `MediaStore` specific result type.
|
||||
pub type Result<T, E = MediaStoreError> = std::result::Result<T, E>;
|
||||
|
||||
/// The high-level public type to represent an `MediaStore` lock.
|
||||
#[derive(Clone)]
|
||||
pub struct MediaStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `MediaStore`.
|
||||
cross_process_lock: Arc<CrossProcessLock<LockableMediaStore>>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
/// That's the only place where the store exists.
|
||||
store: Arc<DynMediaStore>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for MediaStoreLock {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("MediaStoreLock").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaStoreLock {
|
||||
/// Create a new lock around the [`MediaStore`].
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
where
|
||||
S: IntoMediaStore,
|
||||
{
|
||||
let store = store.into_media_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: Arc::new(CrossProcessLock::new(
|
||||
LockableMediaStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
)),
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
|
||||
pub async fn lock(&self) -> Result<MediaStoreLockGuard<'_>, CrossProcessLockError> {
|
||||
let cross_process_lock_guard = match self.cross_process_lock.spin_lock(None).await?? {
|
||||
// The lock is clean: no other hold acquired it, all good!
|
||||
CrossProcessLockState::Clean(guard) => guard,
|
||||
|
||||
// The lock is dirty: another holder acquired it since the last time we acquired it.
|
||||
// It's not a problem in the case of the `MediaStore` because this API is “stateless” at
|
||||
// the time of writing (2025-11-11). There is nothing that can be out-of-sync: all the
|
||||
// state is in the database, nothing in memory.
|
||||
CrossProcessLockState::Dirty(guard) => {
|
||||
guard.clear_dirty();
|
||||
|
||||
guard
|
||||
}
|
||||
};
|
||||
|
||||
Ok(MediaStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
|
||||
}
|
||||
}
|
||||
|
||||
/// An RAII implementation of a “scoped lock” of an [`MediaStoreLock`].
|
||||
/// When this structure is dropped (falls out of scope), the lock will be
|
||||
/// unlocked.
|
||||
pub struct MediaStoreLockGuard<'a> {
|
||||
/// The cross process lock guard.
|
||||
#[allow(unused)]
|
||||
cross_process_lock_guard: CrossProcessLockGuard,
|
||||
|
||||
/// A reference to the store.
|
||||
store: &'a DynMediaStore,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for MediaStoreLockGuard<'_> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("MediaStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MediaStoreLockGuard<'_> {
|
||||
type Target = DynMediaStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.store
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that wraps the [`MediaStore`] but implements [`TryLock`] to
|
||||
/// make it usable inside the cross process lock.
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableMediaStore(Arc<DynMediaStore>);
|
||||
|
||||
impl TryLock for LockableMediaStore {
|
||||
type LockError = MediaStoreError;
|
||||
|
||||
async fn try_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> std::result::Result<Option<CrossProcessLockGeneration>, Self::LockError> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
// 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 media store.
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{AsyncTraitDeps, cross_process_lock::CrossProcessLockGeneration};
|
||||
use ruma::{MxcUri, time::SystemTime};
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::media::store::MediaService;
|
||||
use crate::media::{
|
||||
MediaRequestParameters,
|
||||
store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaStoreError},
|
||||
};
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the media of the SDK.
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait MediaStore: AsyncTraitDeps {
|
||||
/// The error type used by this media store.
|
||||
type Error: fmt::Debug + Into<MediaStoreError>;
|
||||
|
||||
/// 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<Option<CrossProcessLockGeneration>, 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(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Perform database optimizations if any are available, i.e. vacuuming in
|
||||
/// SQLite.
|
||||
///
|
||||
/// **Warning:** this was added to check if SQLite fragmentation was the
|
||||
/// source of performance issues, **DO NOT use in production**.
|
||||
#[doc(hidden)]
|
||||
async fn optimize(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Returns the size of the store in bytes, if known.
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
|
||||
}
|
||||
|
||||
/// 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_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait MediaStoreInner: AsyncTraitDeps + Clone {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + fmt::Display + Into<MediaStoreError>;
|
||||
|
||||
/// 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 and to be stored as the time of the last media cache cleanup.
|
||||
async fn clean_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// The time of the last media cache cleanup.
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
struct EraseMediaStoreError<T>(T);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<T: fmt::Debug> fmt::Debug for EraseMediaStoreError<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl<T: MediaStore> MediaStore for EraseMediaStoreError<T> {
|
||||
type Error = MediaStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
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(&self) -> Result<(), Self::Error> {
|
||||
self.0.clean().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
self.0.optimize().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
self.0.get_size().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased [`MediaStore`].
|
||||
pub type DynMediaStore = dyn MediaStore<Error = MediaStoreError>;
|
||||
|
||||
/// A type that can be type-erased into `Arc<dyn MediaStore>`.
|
||||
///
|
||||
/// This trait is not meant to be implemented directly outside
|
||||
/// `matrix-sdk-base`, but it is automatically implemented for everything that
|
||||
/// implements `MediaStore`.
|
||||
pub trait IntoMediaStore {
|
||||
#[doc(hidden)]
|
||||
fn into_media_store(self) -> Arc<DynMediaStore>;
|
||||
}
|
||||
|
||||
impl IntoMediaStore for Arc<DynMediaStore> {
|
||||
fn into_media_store(self) -> Arc<DynMediaStore> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoMediaStore for T
|
||||
where
|
||||
T: MediaStore + Sized + 'static,
|
||||
{
|
||||
fn into_media_store(self) -> Arc<DynMediaStore> {
|
||||
Arc::new(EraseMediaStoreError(self))
|
||||
}
|
||||
}
|
||||
|
||||
// Turns a given `Arc<T>` into `Arc<DynMediaStore>` by attaching the
|
||||
// `MediaStore` impl vtable of `EraseMediaStoreError<T>`.
|
||||
impl<T> IntoMediaStore for Arc<T>
|
||||
where
|
||||
T: MediaStore + 'static,
|
||||
{
|
||||
fn into_media_store(self) -> Arc<DynMediaStore> {
|
||||
let ptr: *const T = Arc::into_raw(self);
|
||||
let ptr_erased = ptr as *const EraseMediaStoreError<T>;
|
||||
// SAFETY: EraseMediaStoreError is repr(transparent) so T and
|
||||
// EraseMediaStoreError<T> have the same layout and ABI
|
||||
unsafe { Arc::from_raw(ptr_erased) }
|
||||
}
|
||||
}
|
||||
@@ -570,7 +570,7 @@ fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool
|
||||
match event {
|
||||
AnySyncMessageLikeEvent::CallAnswer(_)
|
||||
| AnySyncMessageLikeEvent::CallInvite(_)
|
||||
| AnySyncMessageLikeEvent::CallNotify(_)
|
||||
| AnySyncMessageLikeEvent::RtcNotification(_)
|
||||
| AnySyncMessageLikeEvent::CallHangup(_)
|
||||
| AnySyncMessageLikeEvent::CallCandidates(_)
|
||||
| AnySyncMessageLikeEvent::CallNegotiate(_)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Data types used for handling the recently used emojis.
|
||||
//!
|
||||
//! There is no formal spec for this, only the implementation in Element Web:
|
||||
//! <https://github.com/element-hq/element-web/commit/a7f92f35f5a27a53a5a030ea7c471be97751a67a>
|
||||
|
||||
use ruma::{UInt, events::macros::EventContent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An event type containing a list of recently used emojis for reactions.
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
|
||||
#[ruma_event(type = "io.element.recent_emoji", kind = GlobalAccountData)]
|
||||
pub struct RecentEmojisContent {
|
||||
/// The list of recently used emojis, ordered by recency. The tuple of
|
||||
/// `String`, `UInt` values represent the actual emoji and the number of
|
||||
/// times it's been used in total, for those clients that might be
|
||||
/// interested.
|
||||
pub recent_emoji: Vec<(String, UInt)>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
impl RecentEmojisContent {
|
||||
/// Creates a new recent emojis event content given the provided recent
|
||||
/// emojis.
|
||||
pub fn new(recent_emoji: Vec<(String, UInt)>) -> Self {
|
||||
Self { recent_emoji }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::uint;
|
||||
use serde_json::{from_value, json, to_value};
|
||||
|
||||
use crate::recent_emojis::RecentEmojisContent;
|
||||
|
||||
#[test]
|
||||
fn serialization() {
|
||||
let content = RecentEmojisContent::new(vec![
|
||||
("😁".to_owned(), uint!(2)),
|
||||
("🎉".to_owned(), uint!(10)),
|
||||
]);
|
||||
let json = to_value(&content).expect("recent emoji serialization failed");
|
||||
let expected = json!({
|
||||
"recent_emoji": [
|
||||
["😁", 2],
|
||||
["🎉", 10],
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(json, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization() {
|
||||
let json = json!({
|
||||
"recent_emoji": [
|
||||
["😁", 2],
|
||||
["🎉", 10],
|
||||
]
|
||||
});
|
||||
let content =
|
||||
from_value::<RecentEmojisContent>(json).expect("recent emoji deserialization failed");
|
||||
let expected = RecentEmojisContent::new(vec![
|
||||
("😁".to_owned(), uint!(2)),
|
||||
("🎉".to_owned(), uint!(10)),
|
||||
]);
|
||||
|
||||
assert_eq!(content.recent_emoji, expected.recent_emoji);
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ fn map_info<F: FnOnce(&mut RoomInfo)>(
|
||||
let mut info = room.clone_info();
|
||||
f(&mut info);
|
||||
changes.add_room(info);
|
||||
} else {
|
||||
} else if store.already_logged_missing_room.lock().insert(room_id.to_owned()) {
|
||||
debug!(room = %room_id, "couldn't find room in state changes or store");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
use ruma::RoomId;
|
||||
|
||||
use super::{super::verification, E2EE};
|
||||
use crate::Result;
|
||||
@@ -26,21 +26,28 @@ use crate::Result;
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
///
|
||||
/// The returned [`TimelineEvent`] has no push actions set up. It's the
|
||||
/// responsibility of the caller to set them.
|
||||
pub async fn sync_timeline_event(
|
||||
e2ee: E2EE<'_>,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
event: &TimelineEvent,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
|
||||
|
||||
Ok(Some(
|
||||
match olm
|
||||
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
|
||||
.try_decrypt_room_event(
|
||||
event.raw().cast_ref_unchecked(),
|
||||
room_id,
|
||||
e2ee.decryption_settings,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
// Note: the push actions are set by the caller.
|
||||
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
let timeline_event = event.to_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
|
||||
verification::process_if_relevant(&sync_timeline_event, e2ee, room_id).await?;
|
||||
@@ -48,9 +55,7 @@ pub async fn sync_timeline_event(
|
||||
|
||||
timeline_event
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => event.to_utd(utd_info),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ async fn find_suitable_and_decrypt(
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesCallNotify(_)
|
||||
| PossibleLatestEvent::YesRtcNotification(_)
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
return Some((Box::new(LatestEvent::new(decrypted)), i));
|
||||
@@ -116,6 +116,10 @@ async fn decrypt_sync_room_event(
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
// We're fine not setting the push actions for the latest event.
|
||||
|
||||
// TODO: we should use `TimelineEvent::to_decrypted`
|
||||
// but this whole code is about to get soon removed by
|
||||
// https://github.com/matrix-org/matrix-rust-sdk/pull/5624.
|
||||
let event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = event.raw().deserialize() {
|
||||
@@ -127,6 +131,9 @@ async fn decrypt_sync_room_event(
|
||||
}
|
||||
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
// TODO: we should use `TimelineEvent::to_utd`
|
||||
// but this whole code is about to get soon removed by
|
||||
// https://github.com/matrix-org/matrix-rust-sdk/pull/5624.
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -399,7 +399,7 @@ fn properties(
|
||||
}
|
||||
|
||||
if let Some(recency_stamp) = &room_response.bump_stamp {
|
||||
let recency_stamp: u64 = (*recency_stamp).into();
|
||||
let recency_stamp = u64::from(*recency_stamp).into();
|
||||
|
||||
if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
|
||||
room_info.update_recency_stamp(recency_stamp);
|
||||
@@ -461,7 +461,7 @@ pub(crate) async fn cache_latest_events(
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesCallNotify(_)
|
||||
| PossibleLatestEvent::YesRtcNotification(_)
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
// We found a suitable latest event. Store it.
|
||||
@@ -548,8 +548,8 @@ pub(crate) async fn cache_latest_events(
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to deserialize event as AnySyncTimelineEvent. ID={}",
|
||||
event.event_id().expect("Event has no ID!")
|
||||
event_id = ?event.event_id(),
|
||||
"Failed to deserialize event as `AnySyncTimelineEvent`",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use matrix_sdk_common::{deserialized_responses::TimelineEvent, timer};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::SyncMessageLikeEvent;
|
||||
use ruma::{
|
||||
UInt, UserId, assign,
|
||||
MilliSecondsSinceUnixEpoch, UInt, UserId, assign,
|
||||
events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent},
|
||||
push::{Action, PushConditionRoomCtx},
|
||||
};
|
||||
@@ -31,6 +31,7 @@ use crate::{Result, Room, RoomInfo, sync::Timeline};
|
||||
///
|
||||
/// For each event:
|
||||
/// - will try to decrypt it,
|
||||
/// - will fix the `origin_server_ts` if considered invalid,
|
||||
/// - will process verification,
|
||||
/// - will process redaction,
|
||||
/// - will process notification.
|
||||
@@ -46,6 +47,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
) -> Result<Timeline> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "build a timeline from sync");
|
||||
|
||||
let now = MilliSecondsSinceUnixEpoch::now();
|
||||
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
|
||||
let mut push_condition_room_ctx = get_push_room_context(context, room, room_info).await?;
|
||||
let room_id = room.room_id();
|
||||
@@ -53,7 +55,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
for raw_event in timeline_inputs.raw_events {
|
||||
// Start by assuming we have a plaintext event. We'll replace it with a
|
||||
// decrypted or UTD event below if necessary.
|
||||
let mut timeline_event = TimelineEvent::from_plaintext(raw_event);
|
||||
let mut timeline_event = TimelineEvent::from_plaintext_with_max_timestamp(raw_event, now);
|
||||
|
||||
// Do some special stuff on the `timeline_event` before collecting it.
|
||||
match timeline_event.raw().deserialize() {
|
||||
@@ -94,7 +96,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
if let Some(decrypted_timeline_event) =
|
||||
Box::pin(e2ee::decrypt::sync_timeline_event(
|
||||
e2ee.clone(),
|
||||
timeline_event.raw(),
|
||||
&timeline_event,
|
||||
room_id,
|
||||
))
|
||||
.await?
|
||||
|
||||
@@ -25,6 +25,15 @@ use crate::Result;
|
||||
|
||||
/// Process the given event as a verification event if it is a candidate. The
|
||||
/// event must be decrypted.
|
||||
///
|
||||
/// **Note**: If the supplied event is an `m.room.message` event with
|
||||
/// `msgtype: m.key.verification.request`, then the device information for
|
||||
/// the sending user must be up-to-date before calling this method
|
||||
/// (otherwise, the request will be ignored). It is hard to guarantee this
|
||||
/// is the case, but you can maximize your chances by explicitly making a
|
||||
/// request for this user's device info by calling
|
||||
/// [`OlmMachine::query_keys_for_users`], sending the request, and
|
||||
/// processing the response with [`OlmMachine::mark_request_as_sent`].
|
||||
pub async fn process_if_relevant(
|
||||
event: &AnySyncTimelineEvent,
|
||||
e2ee: E2EE<'_>,
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Room {
|
||||
/// Is there a non expired membership with application `m.call` and scope
|
||||
/// `m.room` in this room.
|
||||
pub fn has_active_room_call(&self) -> bool {
|
||||
self.inner.read().has_active_room_call()
|
||||
self.info.read().has_active_room_call()
|
||||
}
|
||||
|
||||
/// Returns a `Vec` of `OwnedUserId`'s that participate in the room call.
|
||||
@@ -32,7 +32,7 @@ impl Room {
|
||||
///
|
||||
/// The vector is ordered by oldest membership user to newest.
|
||||
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
|
||||
self.inner.read().active_room_call_participants()
|
||||
self.info.read().active_room_call_participants()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,15 +41,15 @@ mod tests {
|
||||
use std::{ops::Sub, sync::Arc, time::Duration};
|
||||
|
||||
use assign::assign;
|
||||
use matrix_sdk_test::{ALICE, BOB, CAROL};
|
||||
use matrix_sdk_test::{ALICE, BOB, CAROL, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId, device_id, event_id,
|
||||
events::{
|
||||
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
|
||||
AnySyncStateEvent,
|
||||
call::member::{
|
||||
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
|
||||
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
|
||||
LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
|
||||
LegacyMembershipDataInit, LivekitFocus,
|
||||
},
|
||||
},
|
||||
room_id,
|
||||
@@ -101,17 +101,15 @@ mod tests {
|
||||
user_id: &UserId,
|
||||
) -> AnySyncStateEvent {
|
||||
let content = CallMemberEventContent::new_legacy(memberships);
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
EventFactory::new()
|
||||
.sender(user_id)
|
||||
.event(content)
|
||||
.state_key(CallMemberStateKey::new(user_id.to_owned(), None, false).as_ref())
|
||||
.event_id(ev_id)
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
.server_ts(timestamp(0))
|
||||
.into()
|
||||
}
|
||||
|
||||
struct InitData<'a> {
|
||||
@@ -156,16 +154,15 @@ mod tests {
|
||||
),
|
||||
};
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
EventFactory::new()
|
||||
.sender(user_id)
|
||||
.event(content)
|
||||
.state_key(state_key.as_ref())
|
||||
.event_id(ev_id)
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key,
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
.server_ts(timestamp(0))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn foci_and_application() -> (Application, Vec<Focus>) {
|
||||
@@ -182,7 +179,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
room.inner.update_if(|info| {
|
||||
room.info.update_if(|info| {
|
||||
let mut res = false;
|
||||
for ev in events {
|
||||
res |= info.handle_state_event(ev);
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::fmt;
|
||||
use as_variant::as_variant;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
OwnedMxcUri, OwnedUserId, UserId,
|
||||
OwnedMxcUri, OwnedUserId, RoomAliasId, UserId,
|
||||
events::{SyncStateEvent, member_hints::MemberHintsEventContent},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -57,7 +57,36 @@ impl Room {
|
||||
///
|
||||
/// This cache is refilled every time we call [`Self::display_name`].
|
||||
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
|
||||
self.inner.read().cached_display_name.clone()
|
||||
self.info.read().cached_display_name.clone()
|
||||
}
|
||||
|
||||
/// Computes the display name for a room using the provided fields.
|
||||
///
|
||||
/// This function is useful for reusing the same display name computation
|
||||
/// logic where full Rooms aren't available e.g. space summary rooms.
|
||||
pub fn compute_display_name_with_fields(
|
||||
name: Option<String>,
|
||||
canonical_alias: Option<&RoomAliasId>,
|
||||
heroes: Vec<RoomHero>,
|
||||
num_joined_members: u64,
|
||||
) -> RoomDisplayName {
|
||||
// Handle empty string names. The `Room` level implementation relies
|
||||
// on `RoomInfo` doing the same thing.
|
||||
let name = name.and_then(|name| (!name.is_empty()).then_some(name));
|
||||
|
||||
match (name, canonical_alias) {
|
||||
(Some(name), _) => RoomDisplayName::Named(name.trim().to_owned()),
|
||||
(None, Some(alias)) => RoomDisplayName::Aliased(alias.alias().trim().to_owned()),
|
||||
(None, None) => {
|
||||
let hero_display_names =
|
||||
heroes.into_iter().filter_map(|hero| hero.display_name).collect::<Vec<_>>();
|
||||
|
||||
compute_display_name_from_heroes(
|
||||
num_joined_members,
|
||||
hero_display_names.iter().map(|name| name.as_str()).collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Force recalculating a room's display name, taking into account its name,
|
||||
@@ -78,7 +107,7 @@ impl Room {
|
||||
}
|
||||
|
||||
let display_name_or_summary = {
|
||||
let inner = self.inner.read();
|
||||
let inner = self.info.read();
|
||||
|
||||
match (inner.name(), inner.canonical_alias()) {
|
||||
(Some(name), _) => {
|
||||
@@ -107,7 +136,7 @@ impl Room {
|
||||
// Update the cached display name before we return the newly computed value.
|
||||
let mut updated = false;
|
||||
|
||||
self.inner.update_if(|info| {
|
||||
self.info.update_if(|info| {
|
||||
if info.cached_display_name.as_ref() != Some(&display_name) {
|
||||
info.cached_display_name = Some(display_name.clone());
|
||||
updated = true;
|
||||
@@ -528,8 +557,8 @@ mod tests {
|
||||
|
||||
use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
|
||||
use crate::{
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomState, StateChanges, StateStore,
|
||||
store::MemoryStore,
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomHero, RoomState, StateChanges,
|
||||
StateStore, store::MemoryStore,
|
||||
};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
@@ -563,23 +592,51 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
|
||||
fn make_name_event_with(name: &str) -> MinimalStateEvent<RoomNameEventContent> {
|
||||
MinimalStateEvent::Original(OriginalMinimalStateEvent {
|
||||
content: RoomNameEventContent::new("Test Room".to_owned()),
|
||||
content: RoomNameEventContent::new(name.to_owned()),
|
||||
event_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
|
||||
make_name_event_with("Test Room")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_is_empty_if_no_info() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_empty() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(None, None, vec![], 0),
|
||||
RoomDisplayName::Empty
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_is_empty_if_name_empty() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.info.update(|info| info.base_info.name = Some(make_name_event_with("")));
|
||||
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_empty_if_name_empty() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(Some("".to_owned()), None, vec![], 0),
|
||||
RoomDisplayName::Empty
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -587,16 +644,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_alias() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(
|
||||
None,
|
||||
Some(room_alias_id!("#test:example.com")),
|
||||
vec![],
|
||||
0,
|
||||
),
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_prefers_name_over_alias() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
room.info.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
// Display name wasn't cached when we asked for it above, and name overrides
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -604,6 +674,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_name_over_alias() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(
|
||||
Some("Test Room".to_owned()),
|
||||
Some(room_alias_id!("#test:example.com")),
|
||||
vec![],
|
||||
0
|
||||
),
|
||||
RoomDisplayName::Named("Test Room".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
@@ -618,7 +701,7 @@ mod tests {
|
||||
content: RoomNameEventContent::new(String::new()),
|
||||
event_id: None,
|
||||
});
|
||||
room.inner.update(|info| info.base_info.name = Some(room_name));
|
||||
room.info.update(|info| info.base_info.name = Some(room_name));
|
||||
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
}
|
||||
@@ -626,7 +709,7 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -637,13 +720,13 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_display_name_for_invited_room_prefers_name_over_alias() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
room.info.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
// Display name wasn't cached when we asked for it above, and name overrides
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -670,7 +753,7 @@ mod tests {
|
||||
changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
@@ -720,12 +803,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
@@ -755,12 +838,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
|
||||
|
||||
let member_hints_content =
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
|
||||
changes
|
||||
.state
|
||||
.entry(room_id.to_owned())
|
||||
@@ -771,7 +854,7 @@ mod tests {
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
// Bot should not contribute to the display name.
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -801,11 +884,11 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
|
||||
|
||||
let member_hints_content =
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
|
||||
changes
|
||||
.state
|
||||
.entry(room_id.to_owned())
|
||||
@@ -816,7 +899,7 @@ mod tests {
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
// Bot should not contribute to the display name.
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
}
|
||||
@@ -837,8 +920,8 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
@@ -867,12 +950,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
|
||||
|
||||
let member_hints_content =
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
|
||||
changes
|
||||
.state
|
||||
.entry(room_id.to_owned())
|
||||
@@ -914,10 +997,10 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -928,9 +1011,9 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into());
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -938,7 +1021,7 @@ mod tests {
|
||||
joined_member_count: Some(7u32.into()),
|
||||
heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
|
||||
});
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -946,6 +1029,47 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_name_deterministic() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(
|
||||
None,
|
||||
None,
|
||||
vec![
|
||||
RoomHero {
|
||||
user_id: user_id!("@alice:example.org").to_owned(),
|
||||
display_name: Some("Alice".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@bob:example.org").to_owned(),
|
||||
display_name: Some("Bob".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@carol:example.org").to_owned(),
|
||||
display_name: Some("Carol".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@denis:example.org").to_owned(),
|
||||
display_name: Some("Denis".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@erica:example.org").to_owned(),
|
||||
display_name: Some("Erica".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
],
|
||||
1234,
|
||||
),
|
||||
RoomDisplayName::Calculated(
|
||||
"Alice, Bob, Carol, Denis, Erica, and 1229 others".to_owned()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_deterministic_no_heroes() {
|
||||
let (store, room) = make_room_test_helper(RoomState::Joined);
|
||||
@@ -971,10 +1095,10 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
@@ -986,9 +1110,9 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into());
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -1018,12 +1142,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::EmptyWas("Matthew".to_owned())
|
||||
|
||||
@@ -19,13 +19,13 @@ use super::Room;
|
||||
impl Room {
|
||||
/// Get the encryption state of this room.
|
||||
pub fn encryption_state(&self) -> EncryptionState {
|
||||
self.inner.read().encryption_state()
|
||||
self.info.read().encryption_state()
|
||||
}
|
||||
|
||||
/// Get the `m.room.encryption` content that enabled end to end encryption
|
||||
/// in the room.
|
||||
pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
|
||||
self.inner.read().base_info.encryption.clone()
|
||||
self.info.read().base_info.encryption.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,19 +80,15 @@ impl EncryptionState {
|
||||
mod tests {
|
||||
use std::{
|
||||
ops::{Not, Sub},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::ALICE;
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||
events::{
|
||||
AnySyncStateEvent, EmptyStateKey, StateUnsigned, SyncStateEvent,
|
||||
room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
|
||||
},
|
||||
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, event_id,
|
||||
events::{AnySyncStateEvent, room::encryption::RoomEncryptionEventContent},
|
||||
room_id,
|
||||
time::SystemTime,
|
||||
user_id,
|
||||
@@ -118,7 +114,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
room.inner.update_if(|info| {
|
||||
room.info.update_if(|info| {
|
||||
let mut res = false;
|
||||
for ev in events {
|
||||
res |= info.handle_state_event(ev);
|
||||
@@ -135,18 +131,15 @@ mod tests {
|
||||
|
||||
let encryption_content =
|
||||
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
|
||||
OriginalSyncRoomEncryptionEvent {
|
||||
content: encryption_content,
|
||||
event_id: OwnedEventId::from_str("$1234_1").unwrap(),
|
||||
sender: ALICE.to_owned(),
|
||||
// we can simply use now here since this will be dropped when using a
|
||||
// MinimalStateEvent in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key: EmptyStateKey,
|
||||
unsigned: StateUnsigned::new(),
|
||||
},
|
||||
));
|
||||
let encryption_event = EventFactory::new()
|
||||
.sender(*ALICE)
|
||||
.event(encryption_content)
|
||||
.state_key("")
|
||||
.event_id(event_id!("$1234_1"))
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
.server_ts(timestamp(0))
|
||||
.into();
|
||||
receive_state_events(&room, vec![&encryption_event]);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
|
||||
@@ -157,7 +150,7 @@ mod tests {
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
|
||||
room.inner.update_if(|info| {
|
||||
room.info.update_if(|info| {
|
||||
info.mark_encryption_state_synced();
|
||||
|
||||
false
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::{collections::BTreeMap, num::NonZeroUsize};
|
||||
|
||||
use ruma::MilliSecondsSinceUnixEpoch;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
|
||||
@@ -31,12 +32,25 @@ impl Room {
|
||||
/// Return the last event in this room, if one has been cached during
|
||||
/// sliding sync.
|
||||
pub fn latest_event(&self) -> Option<LatestEvent> {
|
||||
self.inner.read().latest_event.as_deref().cloned()
|
||||
self.info.read().latest_event.as_deref().cloned()
|
||||
}
|
||||
|
||||
/// Return the [`LatestEventValue`] of this room.
|
||||
///
|
||||
/// Note that it clones the [`LatestEventValue`]! This can be add pressure
|
||||
/// on the memory if used in a hot path.
|
||||
pub fn new_latest_event(&self) -> LatestEventValue {
|
||||
self.inner.read().new_latest_event.clone()
|
||||
self.info.read().new_latest_event.clone()
|
||||
}
|
||||
|
||||
/// Return the value of [`LatestEventValue::timestamp`].
|
||||
pub fn new_latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
self.info.read().new_latest_event.timestamp()
|
||||
}
|
||||
|
||||
/// Return the value of [`LatestEventValue::is_local`].
|
||||
pub fn new_latest_event_is_local(&self) -> bool {
|
||||
self.info.read().new_latest_event.is_local()
|
||||
}
|
||||
|
||||
/// Return the most recent few encrypted events. When the keys come through
|
||||
@@ -215,7 +229,7 @@ mod tests_with_e2e_encryption {
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
|
||||
room.info.update(|info| info.latest_event = Some(make_latest_event("$A")));
|
||||
add_encrypted_event(&room, "$0");
|
||||
add_encrypted_event(&room, "$1");
|
||||
add_encrypted_event(&room, "$2");
|
||||
|
||||
@@ -19,8 +19,9 @@ use std::{
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use futures_util::future;
|
||||
use ruma::{
|
||||
MxcUri, OwnedUserId, UserId,
|
||||
Int, MxcUri, OwnedUserId, UserId,
|
||||
events::{
|
||||
MessageLikeEventType, StateEventType,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
@@ -35,7 +36,7 @@ use tracing::debug;
|
||||
|
||||
use super::Room;
|
||||
use crate::{
|
||||
MinimalRoomMemberEvent,
|
||||
MinimalRoomMemberEvent, StoreError,
|
||||
deserialized_responses::{DisplayName, MemberEvent},
|
||||
store::{Result as StoreResult, StateStoreExt, ambiguity_map::is_display_name_ambiguous},
|
||||
};
|
||||
@@ -48,7 +49,7 @@ impl Room {
|
||||
///
|
||||
/// Returns true if no members are missing, false otherwise.
|
||||
pub fn are_members_synced(&self) -> bool {
|
||||
self.inner.read().members_synced
|
||||
self.info.read().members_synced
|
||||
}
|
||||
|
||||
/// Mark this Room as holding all member information.
|
||||
@@ -57,14 +58,14 @@ impl Room {
|
||||
/// about its members.
|
||||
#[cfg(feature = "testing")]
|
||||
pub fn mark_members_synced(&self) {
|
||||
self.inner.update(|info| {
|
||||
self.info.update(|info| {
|
||||
info.members_synced = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Mark this Room as still missing member information.
|
||||
pub fn mark_members_missing(&self) {
|
||||
self.inner.update_if(|info| {
|
||||
self.info.update_if(|info| {
|
||||
// notify observable subscribers only if the previous value was false
|
||||
mem::replace(&mut info.members_synced, false)
|
||||
})
|
||||
@@ -119,17 +120,17 @@ impl Room {
|
||||
/// Returns the number of members who have joined or been invited to the
|
||||
/// room.
|
||||
pub fn active_members_count(&self) -> u64 {
|
||||
self.inner.read().active_members_count()
|
||||
self.info.read().active_members_count()
|
||||
}
|
||||
|
||||
/// Returns the number of members who have been invited to the room.
|
||||
pub fn invited_members_count(&self) -> u64 {
|
||||
self.inner.read().invited_members_count()
|
||||
self.info.read().invited_members_count()
|
||||
}
|
||||
|
||||
/// Returns the number of members who have joined the room.
|
||||
pub fn joined_members_count(&self) -> u64 {
|
||||
self.inner.read().joined_members_count()
|
||||
self.info.read().joined_members_count()
|
||||
}
|
||||
|
||||
/// Get the `RoomMember` with the given `user_id`.
|
||||
@@ -140,17 +141,26 @@ impl Room {
|
||||
///
|
||||
/// Async because it can read from storage.
|
||||
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
|
||||
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
|
||||
debug!(%user_id, "Member event not found in state store");
|
||||
return Ok(None);
|
||||
let event = async {
|
||||
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await?
|
||||
else {
|
||||
debug!(%user_id, "Member event not found in state store");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(raw_event.deserialize()?))
|
||||
};
|
||||
let presence = async {
|
||||
let raw_event = self.store.get_presence_event(user_id).await?;
|
||||
Ok::<Option<PresenceEvent>, StoreError>(raw_event.and_then(|e| e.deserialize().ok()))
|
||||
};
|
||||
|
||||
let event = raw_event.deserialize()?;
|
||||
let profile = async { self.store.get_profile(self.room_id(), user_id).await };
|
||||
|
||||
let presence =
|
||||
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
|
||||
|
||||
let profile = self.store.get_profile(self.room_id(), user_id).await?;
|
||||
let (Some(event), presence, profile) = future::try_join3(event, presence, profile).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let display_names = [event.display_name()];
|
||||
let room_info = self.member_room_info(&display_names).await?;
|
||||
@@ -166,18 +176,23 @@ impl Room {
|
||||
display_names: &'a [DisplayName],
|
||||
) -> StoreResult<MemberRoomInfo<'a>> {
|
||||
let max_power_level = self.max_power_level();
|
||||
let power_levels = self.power_levels_or_default().await;
|
||||
let power_levels = async { Ok(self.power_levels_or_default().await) };
|
||||
|
||||
let users_display_names =
|
||||
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
|
||||
self.store.get_users_with_display_names(self.room_id(), display_names);
|
||||
|
||||
let ignored_users = self
|
||||
.store
|
||||
.get_account_data_event_static::<IgnoredUserListEventContent>()
|
||||
.await?
|
||||
.map(|c| c.deserialize())
|
||||
.transpose()?
|
||||
.map(|e| e.content.ignored_users.into_keys().collect());
|
||||
let ignored_users = async {
|
||||
Ok(self
|
||||
.store
|
||||
.get_account_data_event_static::<IgnoredUserListEventContent>()
|
||||
.await?
|
||||
.map(|c| c.deserialize())
|
||||
.transpose()?
|
||||
.map(|e| e.content.ignored_users.into_keys().collect()))
|
||||
};
|
||||
|
||||
let (power_levels, users_display_names, ignored_users) =
|
||||
future::try_join3(power_levels, users_display_names, ignored_users).await?;
|
||||
|
||||
Ok(MemberRoomInfo {
|
||||
power_levels: power_levels.into(),
|
||||
@@ -277,15 +292,13 @@ impl RoomMember {
|
||||
return UserPowerLevel::Infinite;
|
||||
};
|
||||
|
||||
let mut power_level = i64::from(power_level);
|
||||
let normalized_power_level = if self.max_power_level > 0 {
|
||||
normalize_power_level(power_level, self.max_power_level)
|
||||
} else {
|
||||
power_level
|
||||
};
|
||||
|
||||
if self.max_power_level > 0 {
|
||||
power_level = (power_level * 100) / self.max_power_level;
|
||||
}
|
||||
|
||||
UserPowerLevel::Int(
|
||||
power_level.try_into().expect("normalized power level should fit in Int"),
|
||||
)
|
||||
UserPowerLevel::Int(normalized_power_level)
|
||||
}
|
||||
|
||||
/// Get the power level of this member.
|
||||
@@ -468,3 +481,64 @@ impl RoomMemberships {
|
||||
memberships
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale the given `power_level` to a range between 0-100.
|
||||
pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int {
|
||||
let mut power_level = i64::from(power_level);
|
||||
power_level = (power_level * 100) / max_power_level;
|
||||
|
||||
Int::try_from(power_level.clamp(0, 100))
|
||||
.expect("We clamped the normalized power level so they must fit into the Int")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
prop_compose! {
|
||||
fn arb_int()(id in any::<i64>()) -> Int {
|
||||
id.try_into().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||
#[test]
|
||||
fn test_power_level_normalization_with_min_max_level(power_level in arb_int()) {
|
||||
let normalized = normalize_power_level(power_level, 1);
|
||||
let normalized = i64::from(normalized);
|
||||
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||
#[test]
|
||||
fn test_power_level_normalization(power_level in arb_int(), max_level in 1i64..) {
|
||||
let normalized = normalize_power_level(power_level, max_level);
|
||||
let normalized = i64::from(normalized);
|
||||
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_level_normalization_limits() {
|
||||
let level = Int::MIN;
|
||||
let normalized = normalize_power_level(level, 1);
|
||||
let normalized = i64::from(normalized);
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
|
||||
let level = Int::MAX;
|
||||
let normalized = normalize_power_level(level, 1);
|
||||
let normalized = i64::from(normalized);
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
|
||||
pub(crate) use room_info::SyncInfo;
|
||||
pub use room_info::{
|
||||
BaseRoomInfo, InviteAcceptanceDetails, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, apply_redaction,
|
||||
RoomInfoNotableUpdateReasons, RoomRecencyStamp, apply_redaction,
|
||||
};
|
||||
use ruma::{
|
||||
EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
|
||||
@@ -91,7 +91,7 @@ pub struct Room {
|
||||
/// Our own user ID.
|
||||
pub(super) own_user_id: OwnedUserId,
|
||||
|
||||
pub(super) inner: SharedObservable<RoomInfo>,
|
||||
pub(super) info: SharedObservable<RoomInfo>,
|
||||
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
pub(super) store: Arc<DynStateStore>,
|
||||
|
||||
@@ -140,7 +140,7 @@ impl Room {
|
||||
own_user_id: own_user_id.into(),
|
||||
room_id: room_info.room_id.clone(),
|
||||
store,
|
||||
inner: SharedObservable::new(room_info),
|
||||
info: SharedObservable::new(room_info),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
|
||||
Self::MAX_ENCRYPTED_EVENTS,
|
||||
@@ -158,7 +158,7 @@ impl Room {
|
||||
|
||||
/// Get a copy of the room creators.
|
||||
pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
|
||||
self.inner.read().creators()
|
||||
self.info.read().creators()
|
||||
}
|
||||
|
||||
/// Get our own user id.
|
||||
@@ -168,18 +168,18 @@ impl Room {
|
||||
|
||||
/// Whether this room's [`RoomType`] is `m.space`.
|
||||
pub fn is_space(&self) -> bool {
|
||||
self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
|
||||
self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
|
||||
}
|
||||
|
||||
/// Returns the room's type as defined in its creation event
|
||||
/// (`m.room.create`).
|
||||
pub fn room_type(&self) -> Option<RoomType> {
|
||||
self.inner.read().room_type().map(ToOwned::to_owned)
|
||||
self.info.read().room_type().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the unread notification counts.
|
||||
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
|
||||
self.inner.read().notification_counts
|
||||
self.info.read().notification_counts
|
||||
}
|
||||
|
||||
/// Get the number of unread messages (computed client-side).
|
||||
@@ -187,12 +187,12 @@ impl Room {
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_messages(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_unread
|
||||
self.info.read().read_receipts.num_unread
|
||||
}
|
||||
|
||||
/// Get the detailed information about read receipts for the room.
|
||||
pub fn read_receipts(&self) -> RoomReadReceipts {
|
||||
self.inner.read().read_receipts.clone()
|
||||
self.info.read().read_receipts.clone()
|
||||
}
|
||||
|
||||
/// Get the number of unread notifications (computed client-side).
|
||||
@@ -200,7 +200,7 @@ impl Room {
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_notifications(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_notifications
|
||||
self.info.read().read_receipts.num_notifications
|
||||
}
|
||||
|
||||
/// Get the number of unread mentions (computed client-side), that is,
|
||||
@@ -209,7 +209,7 @@ impl Room {
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_mentions(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_mentions
|
||||
self.info.read().read_receipts.num_mentions
|
||||
}
|
||||
|
||||
/// Check if the room states have been synced
|
||||
@@ -220,40 +220,40 @@ impl Room {
|
||||
///
|
||||
/// Returns true if the state is fully synced, false otherwise.
|
||||
pub fn is_state_fully_synced(&self) -> bool {
|
||||
self.inner.read().sync_info == SyncInfo::FullySynced
|
||||
self.info.read().sync_info == SyncInfo::FullySynced
|
||||
}
|
||||
|
||||
/// Check if the room state has been at least partially synced.
|
||||
///
|
||||
/// See [`Room::is_state_fully_synced`] for more info.
|
||||
pub fn is_state_partially_or_fully_synced(&self) -> bool {
|
||||
self.inner.read().sync_info != SyncInfo::NoState
|
||||
self.info.read().sync_info != SyncInfo::NoState
|
||||
}
|
||||
|
||||
/// Get the `prev_batch` token that was received from the last sync. May be
|
||||
/// `None` if the last sync contained the full room history.
|
||||
pub fn last_prev_batch(&self) -> Option<String> {
|
||||
self.inner.read().last_prev_batch.clone()
|
||||
self.info.read().last_prev_batch.clone()
|
||||
}
|
||||
|
||||
/// Get the avatar url of this room.
|
||||
pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
|
||||
self.inner.read().avatar_url().map(ToOwned::to_owned)
|
||||
self.info.read().avatar_url().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get information about the avatar of this room.
|
||||
pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
|
||||
self.inner.read().avatar_info().map(ToOwned::to_owned)
|
||||
self.info.read().avatar_info().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the canonical alias of this room.
|
||||
pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
|
||||
self.inner.read().canonical_alias().map(ToOwned::to_owned)
|
||||
self.info.read().canonical_alias().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the canonical alias of this room.
|
||||
pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
|
||||
self.inner.read().alt_aliases().to_owned()
|
||||
self.info.read().alt_aliases().to_owned()
|
||||
}
|
||||
|
||||
/// Get the `m.room.create` content of this room.
|
||||
@@ -266,7 +266,7 @@ impl Room {
|
||||
/// redacted, all fields except `creator` will be set to their default
|
||||
/// value.
|
||||
pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
|
||||
match self.inner.read().base_info.create.as_ref()? {
|
||||
match self.info.read().base_info.create.as_ref()? {
|
||||
MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
|
||||
MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
|
||||
}
|
||||
@@ -279,7 +279,7 @@ impl Room {
|
||||
pub async fn is_direct(&self) -> StoreResult<bool> {
|
||||
match self.state() {
|
||||
RoomState::Joined | RoomState::Left | RoomState::Banned => {
|
||||
Ok(!self.inner.read().base_info.dm_targets.is_empty())
|
||||
Ok(!self.info.read().base_info.dm_targets.is_empty())
|
||||
}
|
||||
|
||||
RoomState::Invited => {
|
||||
@@ -316,41 +316,41 @@ impl Room {
|
||||
/// us to re-find a DM with a user even if they have left, since we may
|
||||
/// want to re-invite them.
|
||||
pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
|
||||
self.inner.read().base_info.dm_targets.clone()
|
||||
self.info.read().base_info.dm_targets.clone()
|
||||
}
|
||||
|
||||
/// If this room is a direct message, returns the number of members that
|
||||
/// we're sharing the room with.
|
||||
pub fn direct_targets_length(&self) -> usize {
|
||||
self.inner.read().base_info.dm_targets.len()
|
||||
self.info.read().base_info.dm_targets.len()
|
||||
}
|
||||
|
||||
/// Get the guest access policy of this room.
|
||||
pub fn guest_access(&self) -> GuestAccess {
|
||||
self.inner.read().guest_access().clone()
|
||||
self.info.read().guest_access().clone()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room.
|
||||
pub fn history_visibility(&self) -> Option<HistoryVisibility> {
|
||||
self.inner.read().history_visibility().cloned()
|
||||
self.info.read().history_visibility().cloned()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room, or a sensible default if
|
||||
/// the event is missing.
|
||||
pub fn history_visibility_or_default(&self) -> HistoryVisibility {
|
||||
self.inner.read().history_visibility_or_default().clone()
|
||||
self.info.read().history_visibility_or_default().clone()
|
||||
}
|
||||
|
||||
/// Is the room considered to be public.
|
||||
///
|
||||
/// May return `None` if the join rule event is not available.
|
||||
pub fn is_public(&self) -> Option<bool> {
|
||||
self.inner.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
|
||||
self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
|
||||
}
|
||||
|
||||
/// Get the join rule policy of this room, if available.
|
||||
pub fn join_rule(&self) -> Option<JoinRule> {
|
||||
self.inner.read().join_rule().cloned()
|
||||
self.info.read().join_rule().cloned()
|
||||
}
|
||||
|
||||
/// Get the maximum power level that this room contains.
|
||||
@@ -358,7 +358,7 @@ impl Room {
|
||||
/// This is useful if one wishes to normalize the power levels, e.g. from
|
||||
/// 0-100 where 100 would be the max power level.
|
||||
pub fn max_power_level(&self) -> i64 {
|
||||
self.inner.read().base_info.max_power_level
|
||||
self.info.read().base_info.max_power_level
|
||||
}
|
||||
|
||||
/// Get the current power levels of this room.
|
||||
@@ -370,7 +370,7 @@ impl Room {
|
||||
.ok_or(Error::InsufficientData)?
|
||||
.deserialize()?;
|
||||
let creators = self.creators().ok_or(Error::InsufficientData)?;
|
||||
let rules = self.inner.read().room_version_rules_or_default();
|
||||
let rules = self.info.read().room_version_rules_or_default();
|
||||
|
||||
Ok(power_levels_content.power_levels(&rules.authorization, creators))
|
||||
}
|
||||
@@ -383,7 +383,7 @@ impl Room {
|
||||
}
|
||||
|
||||
// As a fallback, create the default power levels of a room.
|
||||
let rules = self.inner.read().room_version_rules_or_default();
|
||||
let rules = self.info.read().room_version_rules_or_default();
|
||||
RoomPowerLevels::new(
|
||||
RoomPowerLevelsSource::None,
|
||||
&rules.authorization,
|
||||
@@ -396,12 +396,12 @@ impl Room {
|
||||
/// The returned string may be empty if the event has been redacted, or it's
|
||||
/// missing from storage.
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.inner.read().name().map(ToOwned::to_owned)
|
||||
self.info.read().name().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the topic of the room.
|
||||
pub fn topic(&self) -> Option<String> {
|
||||
self.inner.read().topic().map(ToOwned::to_owned)
|
||||
self.info.read().topic().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Update the cached user defined notification mode.
|
||||
@@ -410,7 +410,7 @@ impl Room {
|
||||
/// cached result can be retrieved in
|
||||
/// [`Self::cached_user_defined_notification_mode`].
|
||||
pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
|
||||
self.inner.update_if(|info| {
|
||||
self.info.update_if(|info| {
|
||||
if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
|
||||
info.cached_user_defined_notification_mode = Some(mode);
|
||||
|
||||
@@ -426,7 +426,20 @@ impl Room {
|
||||
/// This cache is refilled every time we call
|
||||
/// [`Self::update_cached_user_defined_notification_mode`].
|
||||
pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
|
||||
self.inner.read().cached_user_defined_notification_mode
|
||||
self.info.read().cached_user_defined_notification_mode
|
||||
}
|
||||
|
||||
/// Removes any existing cached value for the user defined notification
|
||||
/// mode.
|
||||
pub fn clear_user_defined_notification_mode(&self) {
|
||||
self.info.update_if(|info| {
|
||||
if info.cached_user_defined_notification_mode.is_some() {
|
||||
info.cached_user_defined_notification_mode = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the list of users ids that are considered to be joined members of
|
||||
@@ -437,7 +450,7 @@ impl Room {
|
||||
|
||||
/// Get the heroes for this room.
|
||||
pub fn heroes(&self) -> Vec<RoomHero> {
|
||||
self.inner.read().heroes().to_vec()
|
||||
self.info.read().heroes().to_vec()
|
||||
}
|
||||
|
||||
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
|
||||
@@ -468,19 +481,19 @@ impl Room {
|
||||
/// Returns a boolean indicating if this room has been manually marked as
|
||||
/// unread
|
||||
pub fn is_marked_unread(&self) -> bool {
|
||||
self.inner.read().base_info.is_marked_unread
|
||||
self.info.read().base_info.is_marked_unread
|
||||
}
|
||||
|
||||
/// Returns the [`RoomVersionId`] of the room, if known.
|
||||
pub fn version(&self) -> Option<RoomVersionId> {
|
||||
self.inner.read().room_version().cloned()
|
||||
self.info.read().room_version().cloned()
|
||||
}
|
||||
|
||||
/// Returns the recency stamp of the room.
|
||||
///
|
||||
/// Please read `RoomInfo::recency_stamp` to learn more.
|
||||
pub fn recency_stamp(&self) -> Option<u64> {
|
||||
self.inner.read().recency_stamp
|
||||
pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
|
||||
self.info.read().recency_stamp
|
||||
}
|
||||
|
||||
/// Returns the details about an invite to this room if the invite has been
|
||||
@@ -491,20 +504,20 @@ impl Room {
|
||||
/// - `None` if we didn't join this room using an invite or the invite
|
||||
/// wasn't accepted by this client.
|
||||
pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
|
||||
self.inner.read().invite_acceptance_details.clone()
|
||||
self.info.read().invite_acceptance_details.clone()
|
||||
}
|
||||
|
||||
/// Get a `Stream` of loaded pinned events for this room.
|
||||
/// If no pinned events are found a single empty `Vec` will be returned.
|
||||
pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
|
||||
self.inner
|
||||
self.info
|
||||
.subscribe()
|
||||
.map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns the current pinned event ids for this room.
|
||||
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
|
||||
self.inner.read().pinned_event_ids()
|
||||
self.info.read().pinned_event_ids()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,12 +84,12 @@ pub struct InviteAcceptanceDetails {
|
||||
impl Room {
|
||||
/// Subscribe to the inner `RoomInfo`.
|
||||
pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
|
||||
self.inner.subscribe()
|
||||
self.info.subscribe()
|
||||
}
|
||||
|
||||
/// Clone the inner `RoomInfo`.
|
||||
pub fn clone_info(&self) -> RoomInfo {
|
||||
self.inner.get()
|
||||
self.info.get()
|
||||
}
|
||||
|
||||
/// Update the summary with given RoomInfo.
|
||||
@@ -98,7 +98,7 @@ impl Room {
|
||||
room_info: RoomInfo,
|
||||
room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
|
||||
) {
|
||||
self.inner.set(room_info);
|
||||
self.info.set(room_info);
|
||||
|
||||
if !room_info_notable_update_reasons.is_empty() {
|
||||
// Ignore error if no receiver exists.
|
||||
@@ -217,9 +217,15 @@ impl BaseRoomInfo {
|
||||
AnySyncStateEvent::RoomGuestAccess(g) => {
|
||||
self.guest_access = Some(g.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomJoinRules(c) => {
|
||||
self.join_rules = Some(c.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomJoinRules(c) => match c.join_rule() {
|
||||
JoinRule::Invite
|
||||
| JoinRule::Knock
|
||||
| JoinRule::Private
|
||||
| JoinRule::Restricted(_)
|
||||
| JoinRule::KnockRestricted(_)
|
||||
| JoinRule::Public => self.join_rules = Some(c.into()),
|
||||
r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
|
||||
},
|
||||
AnySyncStateEvent::RoomCanonicalAlias(a) => {
|
||||
self.canonical_alias = Some(a.into());
|
||||
}
|
||||
@@ -294,9 +300,15 @@ impl BaseRoomInfo {
|
||||
AnyStrippedStateEvent::RoomGuestAccess(g) => {
|
||||
self.guest_access = Some(g.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomJoinRules(c) => {
|
||||
self.join_rules = Some(c.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomJoinRules(c) => match &c.content.join_rule {
|
||||
JoinRule::Invite
|
||||
| JoinRule::Knock
|
||||
| JoinRule::Private
|
||||
| JoinRule::Restricted(_)
|
||||
| JoinRule::KnockRestricted(_)
|
||||
| JoinRule::Public => self.join_rules = Some(c.into()),
|
||||
r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
|
||||
},
|
||||
AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
|
||||
self.canonical_alias = Some(a.into());
|
||||
}
|
||||
@@ -490,17 +502,27 @@ pub struct RoomInfo {
|
||||
|
||||
/// The recency stamp of this room.
|
||||
///
|
||||
/// It's not to be confused with `origin_server_ts` of the latest event.
|
||||
/// Sliding Sync might "ignore” some events when computing the recency
|
||||
/// stamp of the room. Thus, using this `recency_stamp` value is
|
||||
/// more accurate than relying on the latest event.
|
||||
/// It's not to be confused with the `origin_server_ts` value of an event.
|
||||
/// Sliding Sync might “ignore” some events when computing the recency
|
||||
/// stamp of the room. The recency stamp must be considered as an opaque
|
||||
/// unsigned integer value.
|
||||
///
|
||||
/// # Sorting rooms
|
||||
///
|
||||
/// The recency stamp is designed to _sort_ rooms between them. The room
|
||||
/// with the highest stamp should be at the top of a room list. However, in
|
||||
/// some situation, it might be inaccurate (for example if the server and
|
||||
/// the client disagree on which events should increment the recency stamp).
|
||||
/// The [`LatestEventValue`] might be a useful alternative to sort rooms
|
||||
/// between them as it's all computed client-side. In this case, the recency
|
||||
/// stamp nicely acts as a default fallback.
|
||||
#[serde(default)]
|
||||
pub(crate) recency_stamp: Option<u64>,
|
||||
pub(crate) recency_stamp: Option<RoomRecencyStamp>,
|
||||
|
||||
/// A timestamp remembering when we observed the user accepting an invite on
|
||||
/// this current device.
|
||||
///
|
||||
/// This is useful to remember if the user accepted this a join on this
|
||||
/// This is useful to remember if the user accepted this join on this
|
||||
/// specific client.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
|
||||
@@ -1050,8 +1072,8 @@ impl RoomInfo {
|
||||
|
||||
/// Updates the recency stamp of this room.
|
||||
///
|
||||
/// Please read [`Self::recency_stamp`] to learn more.
|
||||
pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
|
||||
/// Please read `Self::recency_stamp` to learn more.
|
||||
pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
|
||||
self.recency_stamp = Some(stamp);
|
||||
}
|
||||
|
||||
@@ -1136,6 +1158,24 @@ impl RoomInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Type to represent a `RoomInfo::recency_stamp`.
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(transparent)]
|
||||
pub struct RoomRecencyStamp(u64);
|
||||
|
||||
impl From<u64> for RoomRecencyStamp {
|
||||
fn from(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomRecencyStamp> for u64 {
|
||||
fn from(value: RoomRecencyStamp) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) enum SyncInfo {
|
||||
/// We only know the room exists and whether it is in invite / joined / left
|
||||
@@ -1194,7 +1234,7 @@ pub fn apply_redaction(
|
||||
/// A room info notable update is an update that can be interesting for other
|
||||
/// parts of the code. This mechanism is used in coordination with
|
||||
/// [`BaseClient::room_info_notable_update_receiver`][baseclient] (and
|
||||
/// `Room::inner` plus `Room::room_info_notable_update_sender`) where `RoomInfo`
|
||||
/// `Room::info` plus `Room::room_info_notable_update_sender`) where `RoomInfo`
|
||||
/// can be observed and some of its updates can be spread to listeners.
|
||||
///
|
||||
/// [baseclient]: crate::BaseClient::room_info_notable_update_receiver
|
||||
@@ -1313,7 +1353,7 @@ mod tests {
|
||||
warned_about_unknown_room_version_rules: Arc::new(false.into()),
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
recency_stamp: Some(42),
|
||||
recency_stamp: Some(42.into()),
|
||||
invite_acceptance_details: None,
|
||||
};
|
||||
|
||||
@@ -1341,7 +1381,8 @@ mod tests {
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
|
||||
"thread_summary": "None"
|
||||
"thread_summary": "None",
|
||||
"timestamp": null,
|
||||
},
|
||||
},
|
||||
"new_latest_event": "None",
|
||||
@@ -1562,7 +1603,7 @@ mod tests {
|
||||
info.cached_user_defined_notification_mode.as_ref(),
|
||||
Some(&RoomNotificationMode::Mute)
|
||||
);
|
||||
assert_eq!(info.recency_stamp.as_ref(), Some(&42));
|
||||
assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
|
||||
}
|
||||
|
||||
// Ensure we can still deserialize RoomInfos before we added things to its
|
||||
|
||||
@@ -21,7 +21,7 @@ use super::Room;
|
||||
impl Room {
|
||||
/// Get the state of the room.
|
||||
pub fn state(&self) -> RoomState {
|
||||
self.inner.read().room_state
|
||||
self.info.read().room_state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ impl Room {
|
||||
///
|
||||
/// A room is considered favourite if it has received the `m.favourite` tag.
|
||||
pub fn is_favourite(&self) -> bool {
|
||||
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
|
||||
self.info.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
|
||||
}
|
||||
|
||||
/// Check whether the room is marked as low priority.
|
||||
@@ -46,7 +46,7 @@ impl Room {
|
||||
/// A room is considered low priority if it has received the `m.lowpriority`
|
||||
/// tag.
|
||||
pub fn is_low_priority(&self) -> bool {
|
||||
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
|
||||
self.info.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Room {
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn is_tombstoned(&self) -> bool {
|
||||
self.inner.read().base_info.tombstone.is_some()
|
||||
self.info.read().base_info.tombstone.is_some()
|
||||
}
|
||||
|
||||
/// Get the [`m.room.tombstone`] state event's content of this room if one
|
||||
@@ -37,7 +37,7 @@ impl Room {
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn tombstone_content(&self) -> Option<RoomTombstoneEventContent> {
|
||||
self.inner.read().tombstone().cloned()
|
||||
self.info.read().tombstone().cloned()
|
||||
}
|
||||
|
||||
/// If this room is tombstoned, return the “reference” to the successor room
|
||||
|
||||
@@ -1794,7 +1794,7 @@ mod tests {
|
||||
|
||||
// Then the room in the client has the recency stamp
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42);
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1816,7 +1816,7 @@ mod tests {
|
||||
|
||||
// Then the room in the client has the recency stamp
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42);
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1832,7 +1832,7 @@ mod tests {
|
||||
|
||||
// Then the room in the client has the previous recency stamp
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42);
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1849,7 +1849,7 @@ mod tests {
|
||||
|
||||
// Then the room in the client has the recency stamp
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 153);
|
||||
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 153.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ use ruma::{
|
||||
use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
use super::{
|
||||
DependentQueuedRequestKind, DisplayName, DynStateStore, RoomLoadSettings, ServerInfo,
|
||||
WellKnownResponse, send_queue::SentRequestKey,
|
||||
DependentQueuedRequestKind, DisplayName, DynStateStore, RoomLoadSettings,
|
||||
SupportedVersionsResponse, TtlStoreValue, WellKnownResponse, send_queue::SentRequestKey,
|
||||
};
|
||||
use crate::{
|
||||
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
|
||||
@@ -101,8 +101,10 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_send_queue_dependents(&self) -> TestResult;
|
||||
/// Test an update to a send queue dependent request.
|
||||
async fn test_update_send_queue_dependent(&self) -> TestResult;
|
||||
/// Test saving/restoring server info.
|
||||
async fn test_server_info_saving(&self) -> TestResult;
|
||||
/// Test saving/restoring the supported versions of the server.
|
||||
async fn test_supported_versions_saving(&self) -> TestResult;
|
||||
/// Test saving/restoring the well-known info of the server.
|
||||
async fn test_well_known_saving(&self) -> TestResult;
|
||||
/// Test fetching room infos based on [`RoomLoadSettings`].
|
||||
async fn test_get_room_infos(&self) -> TestResult;
|
||||
/// Test loading thread subscriptions.
|
||||
@@ -129,7 +131,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
changes.add_presence_event(presence_event, presence_raw);
|
||||
|
||||
let pushrules_raw: Raw<AnyGlobalAccountDataEvent> =
|
||||
f.push_rules(Ruleset::server_default(user_id)).into_raw();
|
||||
f.push_rules(Ruleset::server_default(user_id)).into();
|
||||
let pushrules_event = pushrules_raw.deserialize()?;
|
||||
changes.account_data.insert(pushrules_event.event_type(), pushrules_raw);
|
||||
|
||||
@@ -486,41 +488,74 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_server_info_saving(&self) -> TestResult {
|
||||
async fn test_supported_versions_saving(&self) -> TestResult {
|
||||
let versions =
|
||||
BTreeSet::from([MatrixVersion::V1_1, MatrixVersion::V1_2, MatrixVersion::V1_11]);
|
||||
let server_info = ServerInfo::new(
|
||||
versions.iter().map(|version| version.as_str().unwrap().to_owned()).collect(),
|
||||
[("org.matrix.experimental".to_owned(), true)].into(),
|
||||
Some(WellKnownResponse {
|
||||
homeserver: HomeserverInfo::new("matrix.example.com".to_owned()),
|
||||
identity_server: None,
|
||||
tile_server: None,
|
||||
rtc_foci: vec![RtcFocusInfo::livekit("livekit.example.com".to_owned())],
|
||||
}),
|
||||
);
|
||||
let supported_versions = SupportedVersionsResponse {
|
||||
versions: versions.iter().map(|version| version.as_str().unwrap().to_owned()).collect(),
|
||||
unstable_features: [("org.matrix.experimental".to_owned(), true)].into(),
|
||||
};
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::ServerInfo,
|
||||
StateStoreDataValue::ServerInfo(server_info.clone()),
|
||||
StateStoreDataKey::SupportedVersions,
|
||||
StateStoreDataValue::SupportedVersions(TtlStoreValue::new(supported_versions.clone())),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_let!(
|
||||
Ok(Some(StateStoreDataValue::ServerInfo(stored_info))) =
|
||||
self.get_kv_data(StateStoreDataKey::ServerInfo).await
|
||||
Ok(Some(StateStoreDataValue::SupportedVersions(stored_supported_versions))) =
|
||||
self.get_kv_data(StateStoreDataKey::SupportedVersions).await
|
||||
);
|
||||
assert_eq!(stored_info, server_info);
|
||||
|
||||
let decoded_server_info = stored_info.maybe_decode().unwrap();
|
||||
let stored_supported = decoded_server_info.supported_versions();
|
||||
assert_let!(Some(stored_supported_versions) = stored_supported_versions.into_data());
|
||||
assert_eq!(supported_versions, stored_supported_versions);
|
||||
|
||||
let stored_supported = stored_supported_versions.supported_versions();
|
||||
assert_eq!(stored_supported.versions, versions);
|
||||
assert_eq!(stored_supported.features.len(), 1);
|
||||
assert!(stored_supported.features.contains(&FeatureFlag::from("org.matrix.experimental")));
|
||||
|
||||
self.remove_kv_data(StateStoreDataKey::ServerInfo).await?;
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::ServerInfo).await, Ok(None));
|
||||
self.remove_kv_data(StateStoreDataKey::SupportedVersions).await?;
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::SupportedVersions).await, Ok(None));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_well_known_saving(&self) -> TestResult {
|
||||
let well_known = WellKnownResponse {
|
||||
homeserver: HomeserverInfo::new("matrix.example.com".to_owned()),
|
||||
identity_server: None,
|
||||
tile_server: None,
|
||||
rtc_foci: vec![RtcFocusInfo::livekit("livekit.example.com".to_owned())],
|
||||
};
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::WellKnown,
|
||||
StateStoreDataValue::WellKnown(TtlStoreValue::new(Some(well_known.clone()))),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_let!(
|
||||
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
|
||||
self.get_kv_data(StateStoreDataKey::WellKnown).await
|
||||
);
|
||||
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
|
||||
assert_eq!(stored_well_known, Some(well_known));
|
||||
|
||||
self.remove_kv_data(StateStoreDataKey::WellKnown).await?;
|
||||
assert_matches!(self.get_kv_data(StateStoreDataKey::WellKnown).await, Ok(None));
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::WellKnown,
|
||||
StateStoreDataValue::WellKnown(TtlStoreValue::new(None)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_let!(
|
||||
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
|
||||
self.get_kv_data(StateStoreDataKey::WellKnown).await
|
||||
);
|
||||
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
|
||||
assert_eq!(stored_well_known, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1576,7 +1611,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room_id,
|
||||
txn0.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event0.into(),
|
||||
event0.clone().into(),
|
||||
0,
|
||||
)
|
||||
.await?;
|
||||
@@ -1604,12 +1639,17 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Update the event id.
|
||||
let (event, event_type) = event0.raw();
|
||||
let event_id = owned_event_id!("$1");
|
||||
let num_updated = self
|
||||
.mark_dependent_queued_requests_as_ready(
|
||||
room_id,
|
||||
&txn0,
|
||||
SentRequestKey::Event(event_id.clone()),
|
||||
SentRequestKey::Event {
|
||||
event_id: event_id.clone(),
|
||||
event: event.clone(),
|
||||
event_type: event_type.to_owned(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(num_updated, 1);
|
||||
@@ -1619,9 +1659,18 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert_eq!(dependents.len(), 1);
|
||||
assert_eq!(dependents[0].parent_transaction_id, txn0);
|
||||
assert_eq!(dependents[0].own_transaction_id, child_txn);
|
||||
assert_matches!(dependents[0].parent_key.as_ref(), Some(SentRequestKey::Event(eid)) => {
|
||||
assert_eq!(*eid, event_id);
|
||||
});
|
||||
assert_matches!(
|
||||
dependents[0].parent_key.as_ref(),
|
||||
Some(SentRequestKey::Event {
|
||||
event_id: received_event_id,
|
||||
event: received_event,
|
||||
event_type: received_event_type
|
||||
}) => {
|
||||
assert_eq!(received_event_id, &event_id);
|
||||
assert_eq!(received_event.json().to_string(), event.json().to_string());
|
||||
assert_eq!(received_event_type.as_str(), event_type);
|
||||
}
|
||||
);
|
||||
assert_matches!(dependents[0].kind, DependentQueuedRequestKind::RedactEvent);
|
||||
|
||||
// Now remove it.
|
||||
@@ -2023,9 +2072,15 @@ macro_rules! statestore_integration_tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_server_info_saving() -> TestResult {
|
||||
async fn test_supported_versions_saving() -> TestResult {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_server_info_saving().await
|
||||
store.test_supported_versions_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_well_known_saving() -> TestResult {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_well_known_saving().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
|
||||
@@ -38,9 +38,10 @@ use tracing::{debug, instrument, warn};
|
||||
|
||||
use super::{
|
||||
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
|
||||
RoomLoadSettings, StateChanges, StateStore, StoreError,
|
||||
RoomLoadSettings, StateChanges, StateStore, StoreError, SupportedVersionsResponse,
|
||||
TtlStoreValue, WellKnownResponse,
|
||||
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
|
||||
traits::{ComposerDraft, ServerInfo},
|
||||
traits::ComposerDraft,
|
||||
};
|
||||
use crate::{
|
||||
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
|
||||
@@ -58,7 +59,8 @@ struct MemoryStoreInner {
|
||||
composer_drafts: HashMap<(OwnedRoomId, Option<OwnedEventId>), ComposerDraft>,
|
||||
user_avatar_url: HashMap<OwnedUserId, OwnedMxcUri>,
|
||||
sync_token: Option<String>,
|
||||
server_info: Option<ServerInfo>,
|
||||
supported_versions: Option<TtlStoreValue<SupportedVersionsResponse>>,
|
||||
well_known: Option<TtlStoreValue<Option<WellKnownResponse>>>,
|
||||
filters: HashMap<String, String>,
|
||||
utd_hook_manager_data: Option<GrowableBloom>,
|
||||
one_time_key_uploaded_error: bool,
|
||||
@@ -156,8 +158,11 @@ impl StateStore for MemoryStore {
|
||||
StateStoreDataKey::SyncToken => {
|
||||
inner.sync_token.clone().map(StateStoreDataValue::SyncToken)
|
||||
}
|
||||
StateStoreDataKey::ServerInfo => {
|
||||
inner.server_info.clone().map(StateStoreDataValue::ServerInfo)
|
||||
StateStoreDataKey::SupportedVersions => {
|
||||
inner.supported_versions.clone().map(StateStoreDataValue::SupportedVersions)
|
||||
}
|
||||
StateStoreDataKey::WellKnown => {
|
||||
inner.well_known.clone().map(StateStoreDataValue::WellKnown)
|
||||
}
|
||||
StateStoreDataKey::Filter(filter_name) => {
|
||||
inner.filters.get(filter_name).cloned().map(StateStoreDataValue::Filter)
|
||||
@@ -239,11 +244,17 @@ impl StateStore for MemoryStore {
|
||||
value.into_composer_draft().expect("Session data not a composer draft"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::ServerInfo => {
|
||||
inner.server_info = Some(
|
||||
value.into_server_info().expect("Session data not containing server info"),
|
||||
StateStoreDataKey::SupportedVersions => {
|
||||
inner.supported_versions = Some(
|
||||
value
|
||||
.into_supported_versions()
|
||||
.expect("Session data not containing supported versions"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::WellKnown => {
|
||||
inner.well_known =
|
||||
Some(value.into_well_known().expect("Session data not containing well-known"));
|
||||
}
|
||||
StateStoreDataKey::SeenKnockRequests(room_id) => {
|
||||
inner.seen_knock_requests.insert(
|
||||
room_id.to_owned(),
|
||||
@@ -267,7 +278,8 @@ impl StateStore for MemoryStore {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
match key {
|
||||
StateStoreDataKey::SyncToken => inner.sync_token = None,
|
||||
StateStoreDataKey::ServerInfo => inner.server_info = None,
|
||||
StateStoreDataKey::SupportedVersions => inner.supported_versions = None,
|
||||
StateStoreDataKey::WellKnown => inner.well_known = None,
|
||||
StateStoreDataKey::Filter(filter_name) => {
|
||||
inner.filters.remove(filter_name);
|
||||
}
|
||||
@@ -1038,6 +1050,14 @@ impl StateStore for MemoryStore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
result::Result as StdResult,
|
||||
@@ -41,6 +41,7 @@ pub mod integration_tests;
|
||||
mod observable_map;
|
||||
mod traits;
|
||||
|
||||
use matrix_sdk_common::locks::Mutex as SyncMutex;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
@@ -72,6 +73,7 @@ use crate::{
|
||||
MinimalRoomMemberEvent, Room, RoomCreateWithCreatorEventContent, RoomStateFilter, SessionMeta,
|
||||
deserialized_responses::DisplayName,
|
||||
event_cache::store as event_cache_store,
|
||||
media::store as media_store,
|
||||
room::{RoomInfo, RoomInfoNotableUpdate, RoomState},
|
||||
};
|
||||
|
||||
@@ -92,8 +94,9 @@ pub use self::{
|
||||
SentMediaInfo, SentRequestKey, SerializableEventContent,
|
||||
},
|
||||
traits::{
|
||||
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerInfo, StateStore,
|
||||
StateStoreDataKey, StateStoreDataValue, StateStoreExt, ThreadSubscriptionCatchupToken,
|
||||
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
|
||||
DynStateStore, IntoStateStore, StateStore, StateStoreDataKey, StateStoreDataValue,
|
||||
StateStoreExt, SupportedVersionsResponse, ThreadSubscriptionCatchupToken, TtlStoreValue,
|
||||
WellKnownResponse,
|
||||
},
|
||||
};
|
||||
@@ -183,7 +186,11 @@ pub(crate) struct BaseStateStore {
|
||||
rooms: Arc<StdRwLock<ObservableMap<OwnedRoomId, Room>>>,
|
||||
/// A lock to synchronize access to the store, such that data by the sync is
|
||||
/// never overwritten.
|
||||
sync_lock: Arc<Mutex<()>>,
|
||||
lock: Arc<Mutex<()>>,
|
||||
|
||||
/// Which rooms have already logged a log line about missing room info, in
|
||||
/// the context of response processors?
|
||||
pub(crate) already_logged_missing_room: Arc<SyncMutex<HashSet<OwnedRoomId>>>,
|
||||
}
|
||||
|
||||
impl BaseStateStore {
|
||||
@@ -195,13 +202,14 @@ impl BaseStateStore {
|
||||
room_load_settings: Default::default(),
|
||||
sync_token: Default::default(),
|
||||
rooms: Arc::new(StdRwLock::new(ObservableMap::new())),
|
||||
sync_lock: Default::default(),
|
||||
lock: Default::default(),
|
||||
already_logged_missing_room: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get access to the syncing lock.
|
||||
pub fn sync_lock(&self) -> &Mutex<()> {
|
||||
&self.sync_lock
|
||||
pub fn lock(&self) -> &Mutex<()> {
|
||||
&self.lock
|
||||
}
|
||||
|
||||
/// Set the [`SessionMeta`] into [`BaseStateStore::session_meta`].
|
||||
@@ -775,6 +783,7 @@ pub struct StoreConfig {
|
||||
pub(crate) crypto_store: Arc<DynCryptoStore>,
|
||||
pub(crate) state_store: Arc<DynStateStore>,
|
||||
pub(crate) event_cache_store: event_cache_store::EventCacheStoreLock,
|
||||
pub(crate) media_store: media_store::MediaStoreLock,
|
||||
cross_process_store_locks_holder_name: String,
|
||||
}
|
||||
|
||||
@@ -789,7 +798,7 @@ impl StoreConfig {
|
||||
/// Create a new default `StoreConfig`.
|
||||
///
|
||||
/// To learn more about `cross_process_store_locks_holder_name`, please read
|
||||
/// [`CrossProcessStoreLock::new`](matrix_sdk_common::store_locks::CrossProcessStoreLock::new).
|
||||
/// [`CrossProcessLock::new`](matrix_sdk_common::cross_process_lock::CrossProcessLock::new).
|
||||
#[must_use]
|
||||
pub fn new(cross_process_store_locks_holder_name: String) -> Self {
|
||||
Self {
|
||||
@@ -800,6 +809,10 @@ impl StoreConfig {
|
||||
event_cache_store::MemoryStore::new(),
|
||||
cross_process_store_locks_holder_name.clone(),
|
||||
),
|
||||
media_store: media_store::MediaStoreLock::new(
|
||||
media_store::MemoryMediaStore::new(),
|
||||
cross_process_store_locks_holder_name.clone(),
|
||||
),
|
||||
cross_process_store_locks_holder_name,
|
||||
}
|
||||
}
|
||||
@@ -830,6 +843,18 @@ impl StoreConfig {
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a custom implementation of an `MediaStore`.
|
||||
pub fn media_store<S>(mut self, media_store: S) -> Self
|
||||
where
|
||||
S: media_store::IntoMediaStore,
|
||||
{
|
||||
self.media_store = media_store::MediaStoreLock::new(
|
||||
media_store,
|
||||
self.cross_process_store_locks_holder_name.clone(),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -412,7 +412,16 @@ impl From<AccumulatedSentMediaInfo> for SentMediaInfo {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum SentRequestKey {
|
||||
/// The parent transaction returned an event when it succeeded.
|
||||
Event(OwnedEventId),
|
||||
Event {
|
||||
/// The event ID returned by the server.
|
||||
event_id: OwnedEventId,
|
||||
|
||||
/// The sent event.
|
||||
event: Raw<AnyMessageLikeEventContent>,
|
||||
|
||||
/// The type of the sent event.
|
||||
event_type: String,
|
||||
},
|
||||
|
||||
/// The parent transaction returned an uploaded resource URL.
|
||||
Media(SentMediaInfo),
|
||||
@@ -421,7 +430,7 @@ pub enum SentRequestKey {
|
||||
impl SentRequestKey {
|
||||
/// Converts the current parent key into an event id, if possible.
|
||||
pub fn into_event_id(self) -> Option<OwnedEventId> {
|
||||
as_variant!(self, Self::Event)
|
||||
as_variant!(self, Self::Event { event_id, .. } => event_id)
|
||||
}
|
||||
|
||||
/// Converts the current parent key into information about a sent media, if
|
||||
|
||||
@@ -513,6 +513,17 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
room: &RoomId,
|
||||
thread_id: &EventId,
|
||||
) -> Result<Option<StoredThreadSubscription>, Self::Error>;
|
||||
|
||||
/// Perform database optimizations if any are available, i.e. vacuuming in
|
||||
/// SQLite.
|
||||
///
|
||||
/// /// **Warning:** this was added to check if SQLite fragmentation was the
|
||||
/// source of performance issues, **DO NOT use in production**.
|
||||
#[doc(hidden)]
|
||||
async fn optimize(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Returns the size of the store in bytes, if known.
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
@@ -832,6 +843,14 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.remove_thread_subscription(room, thread_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
self.0.optimize().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
self.0.get_size().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience functionality for state stores.
|
||||
@@ -1026,56 +1045,53 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Useful server info such as data returned by the /client/versions and
|
||||
/// .well-known/client/matrix endpoints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ServerInfo {
|
||||
/// Versions supported by the remote server.
|
||||
pub versions: Vec<String>,
|
||||
|
||||
/// List of unstable features and their enablement status.
|
||||
pub unstable_features: BTreeMap<String, bool>,
|
||||
|
||||
/// Information about the server found in the client well-known file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub well_known: Option<WellKnownResponse>,
|
||||
/// A TTL value in the store whose data can only be accessed before it expires.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TtlStoreValue<T> {
|
||||
/// The data of the item.
|
||||
#[serde(flatten)]
|
||||
data: T,
|
||||
|
||||
/// Last time we fetched this data from the server, in milliseconds since
|
||||
/// epoch.
|
||||
last_fetch_ts: f64,
|
||||
}
|
||||
|
||||
impl ServerInfo {
|
||||
impl<T> TtlStoreValue<T> {
|
||||
/// The number of milliseconds after which the data is considered stale.
|
||||
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24 * 7) as _; // seven days
|
||||
|
||||
/// Encode server info into this serializable struct.
|
||||
pub fn new(
|
||||
versions: Vec<String>,
|
||||
unstable_features: BTreeMap<String, bool>,
|
||||
well_known: Option<WellKnownResponse>,
|
||||
) -> Self {
|
||||
Self { versions, unstable_features, well_known, last_fetch_ts: now_timestamp_ms() }
|
||||
/// Construct a new `TtlStoreValue` with the given data.
|
||||
pub fn new(data: T) -> Self {
|
||||
Self { data, last_fetch_ts: now_timestamp_ms() }
|
||||
}
|
||||
|
||||
/// Decode server info from this serializable struct.
|
||||
///
|
||||
/// May return `None` if the data is considered stale, after
|
||||
/// [`Self::STALE_THRESHOLD`] milliseconds since the last time we stored
|
||||
/// it.
|
||||
pub fn maybe_decode(&self) -> Option<Self> {
|
||||
/// Get the data of this value, if it hasn't expired.
|
||||
pub fn into_data(self) -> Option<T> {
|
||||
if now_timestamp_ms() - self.last_fetch_ts >= Self::STALE_THRESHOLD {
|
||||
None
|
||||
} else {
|
||||
Some(self.clone())
|
||||
Some(self.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialisable representation of get_supported_versions::Response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SupportedVersionsResponse {
|
||||
/// Versions supported by the remote server.
|
||||
pub versions: Vec<String>,
|
||||
|
||||
/// List of unstable features and their enablement status.
|
||||
pub unstable_features: BTreeMap<String, bool>,
|
||||
}
|
||||
|
||||
impl SupportedVersionsResponse {
|
||||
/// Extracts known Matrix versions and features from the un-typed lists of
|
||||
/// strings.
|
||||
///
|
||||
/// Note: Matrix versions that Ruma cannot parse, or does not know about,
|
||||
/// are discarded.
|
||||
/// Note: Matrix versions and features that Ruma cannot parse, or does not
|
||||
/// know about, are discarded.
|
||||
pub fn supported_versions(&self) -> SupportedVersions {
|
||||
SupportedVersions::from_parts(&self.versions, &self.unstable_features)
|
||||
}
|
||||
@@ -1123,8 +1139,11 @@ pub enum StateStoreDataValue {
|
||||
/// The sync token.
|
||||
SyncToken(String),
|
||||
|
||||
/// The server info (versions, well-known etc).
|
||||
ServerInfo(ServerInfo),
|
||||
/// The supported versions of the server.
|
||||
SupportedVersions(TtlStoreValue<SupportedVersionsResponse>),
|
||||
|
||||
/// The well-known information of the server.
|
||||
WellKnown(TtlStoreValue<Option<WellKnownResponse>>),
|
||||
|
||||
/// A filter with the given ID.
|
||||
Filter(String),
|
||||
@@ -1195,6 +1214,97 @@ pub struct ComposerDraft {
|
||||
pub html_text: Option<String>,
|
||||
/// The type of draft.
|
||||
pub draft_type: ComposerDraftType,
|
||||
/// Attachments associated with this draft.
|
||||
#[serde(default)]
|
||||
pub attachments: Vec<DraftAttachment>,
|
||||
}
|
||||
|
||||
/// An attachment stored with a composer draft.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DraftAttachment {
|
||||
/// The filename of the attachment.
|
||||
pub filename: String,
|
||||
/// The attachment content with type-specific data.
|
||||
pub content: DraftAttachmentContent,
|
||||
}
|
||||
|
||||
/// The content of a draft attachment with type-specific data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum DraftAttachmentContent {
|
||||
/// Image attachment.
|
||||
Image {
|
||||
/// The image file data.
|
||||
data: Vec<u8>,
|
||||
/// MIME type.
|
||||
mimetype: Option<String>,
|
||||
/// File size in bytes.
|
||||
size: Option<u64>,
|
||||
/// Width in pixels.
|
||||
width: Option<u64>,
|
||||
/// Height in pixels.
|
||||
height: Option<u64>,
|
||||
/// BlurHash string.
|
||||
blurhash: Option<String>,
|
||||
/// Optional thumbnail.
|
||||
thumbnail: Option<DraftThumbnail>,
|
||||
},
|
||||
/// Video attachment.
|
||||
Video {
|
||||
/// The video file data.
|
||||
data: Vec<u8>,
|
||||
/// MIME type.
|
||||
mimetype: Option<String>,
|
||||
/// File size in bytes.
|
||||
size: Option<u64>,
|
||||
/// Width in pixels.
|
||||
width: Option<u64>,
|
||||
/// Height in pixels.
|
||||
height: Option<u64>,
|
||||
/// Duration.
|
||||
duration: Option<std::time::Duration>,
|
||||
/// BlurHash string.
|
||||
blurhash: Option<String>,
|
||||
/// Optional thumbnail.
|
||||
thumbnail: Option<DraftThumbnail>,
|
||||
},
|
||||
/// Audio attachment.
|
||||
Audio {
|
||||
/// The audio file data.
|
||||
data: Vec<u8>,
|
||||
/// MIME type.
|
||||
mimetype: Option<String>,
|
||||
/// File size in bytes.
|
||||
size: Option<u64>,
|
||||
/// Duration.
|
||||
duration: Option<std::time::Duration>,
|
||||
},
|
||||
/// Generic file attachment.
|
||||
File {
|
||||
/// The file data.
|
||||
data: Vec<u8>,
|
||||
/// MIME type.
|
||||
mimetype: Option<String>,
|
||||
/// File size in bytes.
|
||||
size: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Thumbnail data for a draft attachment.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DraftThumbnail {
|
||||
/// The filename of the thumbnail.
|
||||
pub filename: String,
|
||||
/// The thumbnail image data.
|
||||
pub data: Vec<u8>,
|
||||
/// MIME type of the thumbnail.
|
||||
pub mimetype: Option<String>,
|
||||
/// Width in pixels.
|
||||
pub width: Option<u64>,
|
||||
/// Height in pixels.
|
||||
pub height: Option<u64>,
|
||||
/// File size in bytes.
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
/// The type of draft of the composer.
|
||||
@@ -1245,9 +1355,14 @@ impl StateStoreDataValue {
|
||||
as_variant!(self, Self::ComposerDraft)
|
||||
}
|
||||
|
||||
/// Get this value if it is the server info metadata.
|
||||
pub fn into_server_info(self) -> Option<ServerInfo> {
|
||||
as_variant!(self, Self::ServerInfo)
|
||||
/// Get this value if it is the supported versions metadata.
|
||||
pub fn into_supported_versions(self) -> Option<TtlStoreValue<SupportedVersionsResponse>> {
|
||||
as_variant!(self, Self::SupportedVersions)
|
||||
}
|
||||
|
||||
/// Get this value if it is the well-known metadata.
|
||||
pub fn into_well_known(self) -> Option<TtlStoreValue<Option<WellKnownResponse>>> {
|
||||
as_variant!(self, Self::WellKnown)
|
||||
}
|
||||
|
||||
/// Get this value if it is the data for the ignored join requests.
|
||||
@@ -1270,8 +1385,11 @@ pub enum StateStoreDataKey<'a> {
|
||||
/// The sync token.
|
||||
SyncToken,
|
||||
|
||||
/// The server info,
|
||||
ServerInfo,
|
||||
/// The supported versions of the server,
|
||||
SupportedVersions,
|
||||
|
||||
/// The well-known information of the server,
|
||||
WellKnown,
|
||||
|
||||
/// A filter with the given name.
|
||||
Filter(&'a str),
|
||||
@@ -1307,10 +1425,14 @@ impl StateStoreDataKey<'_> {
|
||||
/// Key to use for the [`SyncToken`][Self::SyncToken] variant.
|
||||
pub const SYNC_TOKEN: &'static str = "sync_token";
|
||||
|
||||
/// Key to use for the [`ServerInfo`][Self::ServerInfo]
|
||||
/// Key to use for the [`SupportedVersions`][Self::SupportedVersions]
|
||||
/// variant.
|
||||
pub const SERVER_INFO: &'static str = "server_capabilities"; // Note: this is the old name, kept for backwards compatibility.
|
||||
//
|
||||
pub const SUPPORTED_VERSIONS: &'static str = "server_capabilities"; // Note: this is the old name, kept for backwards compatibility.
|
||||
|
||||
/// Key to use for the [`WellKnown`][Self::WellKnown]
|
||||
/// variant.
|
||||
pub const WELL_KNOWN: &'static str = "well_known";
|
||||
|
||||
/// Key prefix to use for the [`Filter`][Self::Filter] variant.
|
||||
pub const FILTER: &'static str = "filter";
|
||||
|
||||
@@ -1377,22 +1499,44 @@ pub fn compare_thread_subscription_bump_stamps(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ServerInfo, now_timestamp_ms};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{SupportedVersionsResponse, TtlStoreValue, now_timestamp_ms};
|
||||
|
||||
#[test]
|
||||
fn test_stale_server_info() {
|
||||
let mut server_info = ServerInfo {
|
||||
versions: Default::default(),
|
||||
unstable_features: Default::default(),
|
||||
well_known: Default::default(),
|
||||
last_fetch_ts: now_timestamp_ms() - ServerInfo::STALE_THRESHOLD - 1.0,
|
||||
};
|
||||
|
||||
fn test_stale_ttl_store_value() {
|
||||
// Definitely stale.
|
||||
assert!(server_info.maybe_decode().is_none());
|
||||
let ttl_value = TtlStoreValue {
|
||||
data: (),
|
||||
last_fetch_ts: now_timestamp_ms() - TtlStoreValue::<()>::STALE_THRESHOLD - 1.0,
|
||||
};
|
||||
assert!(ttl_value.into_data().is_none());
|
||||
|
||||
// Definitely not stale.
|
||||
server_info.last_fetch_ts = now_timestamp_ms() - 1.0;
|
||||
assert!(server_info.maybe_decode().is_some());
|
||||
let ttl_value = TtlStoreValue::new(());
|
||||
assert!(ttl_value.into_data().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stale_ttl_store_value_serialize_roundtrip() {
|
||||
let server_info = SupportedVersionsResponse {
|
||||
versions: vec!["1.2".to_owned(), "1.3".to_owned(), "1.4".to_owned()],
|
||||
unstable_features: [("org.matrix.msc3916.stable".to_owned(), true)].into(),
|
||||
};
|
||||
let ttl_value = TtlStoreValue { data: server_info.clone(), last_fetch_ts: 1000.0 };
|
||||
let json = json!({
|
||||
"versions": ["1.2", "1.3", "1.4"],
|
||||
"unstable_features": {
|
||||
"org.matrix.msc3916.stable": true,
|
||||
},
|
||||
"last_fetch_ts": 1000.0,
|
||||
});
|
||||
|
||||
assert_eq!(serde_json::to_value(&ttl_value).unwrap(), json);
|
||||
|
||||
let deserialized =
|
||||
serde_json::from_value::<TtlStoreValue<SupportedVersionsResponse>>(json).unwrap();
|
||||
assert_eq!(deserialized.data, server_info);
|
||||
assert!(deserialized.last_fetch_ts - ttl_value.last_fetch_ts < 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.16.0] - 2025-12-04
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] Cross-process lock can be dirty. The `CrossProcess::try_lock_once` now returns a new type `CrossProcessResult`, which is an enum with `Clean`, `Dirty` or `Unobtained` variants. When the lock is dirty it means it's been acquired once, then acquired another time from another holder, so the current holder may want to refresh its internal state.
|
||||
([#5672](https://github.com/matrix-org/matrix-rust-sdk/pull/5672)).
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features
|
||||
@@ -14,7 +21,7 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- [**breaking**] Use `Raw<AnyTimelineEvent>` in place of `Raw<AnyMessageLikeEvent>`
|
||||
in `DecryptedRoomEvent::event`.
|
||||
([#5512](https://github.com/matrix-org/matrix-rust-sdk/pull/5512/files)).
|
||||
([#5512](https://github.com/matrix-org/matrix-rust-sdk/pull/5512)).
|
||||
Affects the following functions:
|
||||
- `OlmMachine::decrypt_room_event` - existing matches on the result's event field
|
||||
should be updated to `AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::...)`
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version.workspace = true
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
|
||||
@@ -0,0 +1,932 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
//! A cross-process lock implementation.
|
||||
//!
|
||||
//! This is a per-process lock that may be used only for very specific use
|
||||
//! cases, where multiple processes might concurrently write to the same
|
||||
//! database at the same time; this would invalidate store caches, so
|
||||
//! that should be done mindfully. Such a lock can be obtained multiple times by
|
||||
//! the same process, and it remains active as long as there's at least one user
|
||||
//! in a given process.
|
||||
//!
|
||||
//! The lock is implemented using time-based leases. The lock maintains the lock
|
||||
//! identifier (key), who's the current holder (value), and an expiration
|
||||
//! timestamp on the side; see also `CryptoStore::try_take_leased_lock` for more
|
||||
//! details.
|
||||
//!
|
||||
//! The lock is initially obtained for a certain period of time (namely, the
|
||||
//! duration of a lease, aka `LEASE_DURATION_MS`), and then a “heartbeat” task
|
||||
//! renews the lease to extend its duration, every so often (namely, every
|
||||
//! `EXTEND_LEASE_EVERY_MS`). Since the Tokio scheduler might be busy, the
|
||||
//! extension request should happen way more frequently than the duration of a
|
||||
//! lease, in case a deadline is missed. The current values have been chosen to
|
||||
//! reflect that, with a ratio of 1:10 as of 2023-06-23.
|
||||
//!
|
||||
//! Releasing the lock happens naturally, by not renewing a lease. It happens
|
||||
//! automatically after the duration of the last lease, at most.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
future::Future,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, error, instrument, trace, warn};
|
||||
|
||||
use crate::{
|
||||
SendOutsideWasm,
|
||||
executor::{JoinHandle, spawn},
|
||||
sleep::sleep,
|
||||
};
|
||||
|
||||
/// A lock generation is an integer incremented each time the lock is taken by
|
||||
/// a different holder.
|
||||
///
|
||||
/// This is used to know if a lock has been dirtied.
|
||||
pub type CrossProcessLockGeneration = u64;
|
||||
|
||||
/// Trait used to try to take a lock. Foundation of [`CrossProcessLock`].
|
||||
pub trait TryLock {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
type LockError: Error + Send + Sync;
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
type LockError: Error;
|
||||
|
||||
/// Try to take a leased lock.
|
||||
///
|
||||
/// This attempts to take a lock for the given lease duration.
|
||||
///
|
||||
/// - If we already had the lease, this will extend the lease.
|
||||
/// - If we didn't, but the previous lease has expired, we will obtain the
|
||||
/// lock.
|
||||
/// - If there was no previous lease, we will obtain the lock.
|
||||
/// - Otherwise, we don't get the lock.
|
||||
///
|
||||
/// Returns `Some(_)` to indicate the lock succeeded, `None` otherwise. The
|
||||
/// cross-process lock generation must be compared to the generation before
|
||||
/// the call to see if the lock has been dirtied: a different generation
|
||||
/// means the lock has been dirtied, i.e. taken by a different holder in
|
||||
/// the meantime.
|
||||
fn try_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> impl Future<Output = Result<Option<CrossProcessLockGeneration>, Self::LockError>>
|
||||
+ SendOutsideWasm;
|
||||
}
|
||||
|
||||
/// Small state machine to handle wait times.
|
||||
#[derive(Clone, Debug)]
|
||||
enum WaitingTime {
|
||||
/// Some time to wait, in milliseconds.
|
||||
Some(u32),
|
||||
/// Stop waiting when seeing this value.
|
||||
Stop,
|
||||
}
|
||||
|
||||
/// A guard of a cross-process lock.
|
||||
///
|
||||
/// The lock will be automatically released a short period of time after all the
|
||||
/// guards have dropped.
|
||||
#[derive(Clone, Debug)]
|
||||
#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
|
||||
pub struct CrossProcessLockGuard {
|
||||
/// A clone of [`CrossProcessLock::num_holders`].
|
||||
num_holders: Arc<AtomicU32>,
|
||||
|
||||
/// A clone of [`CrossProcessLock::is_dirty`].
|
||||
is_dirty: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl CrossProcessLockGuard {
|
||||
fn new(num_holders: Arc<AtomicU32>, is_dirty: Arc<AtomicBool>) -> Self {
|
||||
Self { num_holders, is_dirty }
|
||||
}
|
||||
|
||||
/// Determine whether the cross-process lock associated to this guard is
|
||||
/// dirty.
|
||||
///
|
||||
/// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
|
||||
/// of _dirty_.
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.is_dirty.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Clear the dirty state from the cross-process lock associated to this
|
||||
/// guard.
|
||||
///
|
||||
/// If the cross-process lock is dirtied, it will remain dirtied until
|
||||
/// this method is called. This allows recovering from a dirty state and
|
||||
/// marking that it has recovered.
|
||||
pub fn clear_dirty(&self) {
|
||||
self.is_dirty.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CrossProcessLockGuard {
|
||||
fn drop(&mut self) {
|
||||
self.num_holders.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// A cross-process lock implementation.
|
||||
///
|
||||
/// See the doc-comment of this module for more information.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CrossProcessLock<L>
|
||||
where
|
||||
L: TryLock + Clone + SendOutsideWasm + 'static,
|
||||
{
|
||||
/// The locker implementation.
|
||||
///
|
||||
/// `L` is responsible for trying to take the lock, while
|
||||
/// [`CrossProcessLock`] is responsible to make it cross-process, with the
|
||||
/// retry mechanism, plus guard and so on.
|
||||
locker: L,
|
||||
|
||||
/// Number of holders of the lock in this process.
|
||||
///
|
||||
/// If greater than 0, this means we've already obtained this lock, in this
|
||||
/// process, and the store lock mustn't be touched.
|
||||
///
|
||||
/// When the number of holders is decreased to 0, then the lock must be
|
||||
/// released in the store.
|
||||
num_holders: Arc<AtomicU32>,
|
||||
|
||||
/// A mutex to control an attempt to take the lock, to avoid making it
|
||||
/// reentrant.
|
||||
locking_attempt: Arc<Mutex<()>>,
|
||||
|
||||
/// Current renew task spawned by `try_lock_once`.
|
||||
renew_task: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
|
||||
/// The key used in the key/value mapping for the lock entry.
|
||||
lock_key: String,
|
||||
|
||||
/// A specific value to identify the lock's holder.
|
||||
lock_holder: String,
|
||||
|
||||
/// Backoff time, in milliseconds.
|
||||
backoff: Arc<Mutex<WaitingTime>>,
|
||||
|
||||
/// This lock generation.
|
||||
generation: Arc<AtomicU64>,
|
||||
|
||||
/// Whether the lock has been dirtied.
|
||||
///
|
||||
/// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
|
||||
/// of _dirty_.
|
||||
is_dirty: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
/// Amount of time a lease of the lock should last, in milliseconds.
|
||||
pub const LEASE_DURATION_MS: u32 = 500;
|
||||
|
||||
/// Period of time between two attempts to extend the lease. We'll
|
||||
/// re-request a lease for an entire duration of `LEASE_DURATION_MS`
|
||||
/// milliseconds, every `EXTEND_LEASE_EVERY_MS`, so this has to
|
||||
/// be an amount safely low compared to `LEASE_DURATION_MS`, to make sure
|
||||
/// that we can miss a deadline without compromising the lock.
|
||||
pub const EXTEND_LEASE_EVERY_MS: u64 = 50;
|
||||
|
||||
/// Initial backoff, in milliseconds. This is the time we wait the first
|
||||
/// time, if taking the lock initially failed.
|
||||
const INITIAL_BACKOFF_MS: u32 = 10;
|
||||
|
||||
/// Maximal backoff, in milliseconds. This is the maximum amount of time
|
||||
/// we'll wait for the lock, *between two attempts*.
|
||||
pub const MAX_BACKOFF_MS: u32 = 1000;
|
||||
|
||||
/// Sentinel value representing the absence of a lock generation value.
|
||||
///
|
||||
/// When the lock is created, it has no generation. Once locked, it receives its
|
||||
/// first generation from [`TryLock::try_lock`]. Subsequent lockings may
|
||||
/// generate new lock generation. The generation is incremented by 1 every time.
|
||||
///
|
||||
/// The first generation is defined by [`FIRST_CROSS_PROCESS_LOCK_GENERATION`].
|
||||
pub const NO_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 0;
|
||||
|
||||
/// Describe the first lock generation value (see
|
||||
/// [`CrossProcessLockGeneration`]).
|
||||
pub const FIRST_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 1;
|
||||
|
||||
impl<L> CrossProcessLock<L>
|
||||
where
|
||||
L: TryLock + Clone + SendOutsideWasm + 'static,
|
||||
{
|
||||
/// Create a new cross-process lock.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `lock_key`: key in the key-value store to store the lock's state.
|
||||
/// - `lock_holder`: identify the lock's holder with this given value.
|
||||
pub fn new(locker: L, lock_key: String, lock_holder: String) -> Self {
|
||||
Self {
|
||||
locker,
|
||||
lock_key,
|
||||
lock_holder,
|
||||
backoff: Arc::new(Mutex::new(WaitingTime::Some(INITIAL_BACKOFF_MS))),
|
||||
num_holders: Arc::new(0.into()),
|
||||
locking_attempt: Arc::new(Mutex::new(())),
|
||||
renew_task: Default::default(),
|
||||
generation: Arc::new(AtomicU64::new(NO_CROSS_PROCESS_LOCK_GENERATION)),
|
||||
is_dirty: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether the cross-process lock is dirty.
|
||||
///
|
||||
/// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
|
||||
/// of _dirty_.
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.is_dirty.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Clear the dirty state from this cross-process lock.
|
||||
///
|
||||
/// If the cross-process lock is dirtied, it will remain dirtied until
|
||||
/// this method is called. This allows recovering from a dirty state and
|
||||
/// marking that it has recovered.
|
||||
pub fn clear_dirty(&self) {
|
||||
self.is_dirty.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Try to lock once, returns whether the lock was obtained or not.
|
||||
///
|
||||
/// The lock can be obtained but it can be dirty. In all cases, the renew
|
||||
/// task will run in the background.
|
||||
#[instrument(skip(self), fields(?self.lock_key, ?self.lock_holder))]
|
||||
pub async fn try_lock_once(
|
||||
&self,
|
||||
) -> Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, L::LockError> {
|
||||
// Hold onto the locking attempt mutex for the entire lifetime of this
|
||||
// function, to avoid multiple reentrant calls.
|
||||
let mut _attempt = self.locking_attempt.lock().await;
|
||||
|
||||
// If another thread obtained the lock, make sure to only superficially increase
|
||||
// the number of holders, and carry on.
|
||||
if self.num_holders.load(Ordering::SeqCst) > 0 {
|
||||
// Note: between the above load and the fetch_add below, another thread may
|
||||
// decrement `num_holders`. That's fine because that means the lock
|
||||
// was taken by at least one thread, and after this call it will be
|
||||
// taken by at least one thread.
|
||||
trace!("We already had the lock, incrementing holder count");
|
||||
|
||||
self.num_holders.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
return Ok(Ok(CrossProcessLockState::Clean(CrossProcessLockGuard::new(
|
||||
self.num_holders.clone(),
|
||||
self.is_dirty.clone(),
|
||||
))));
|
||||
}
|
||||
|
||||
if let Some(new_generation) =
|
||||
self.locker.try_lock(LEASE_DURATION_MS, &self.lock_key, &self.lock_holder).await?
|
||||
{
|
||||
match self.generation.swap(new_generation, Ordering::SeqCst) {
|
||||
// If there was no lock generation, it means this is the first time the lock is
|
||||
// obtained. It cannot be dirty.
|
||||
NO_CROSS_PROCESS_LOCK_GENERATION => {
|
||||
trace!(?new_generation, "Setting the lock generation for the first time");
|
||||
}
|
||||
|
||||
// This was NOT the same generation, the lock has been dirtied!
|
||||
previous_generation if previous_generation != new_generation => {
|
||||
warn!(
|
||||
?previous_generation,
|
||||
?new_generation,
|
||||
"The lock has been obtained, but it's been dirtied!"
|
||||
);
|
||||
self.is_dirty.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// This was the same generation, no problem.
|
||||
_ => {
|
||||
trace!("Same lock generation; no problem");
|
||||
}
|
||||
}
|
||||
|
||||
trace!("Lock obtained!");
|
||||
} else {
|
||||
trace!("Couldn't obtain the lock immediately.");
|
||||
return Ok(Err(CrossProcessLockUnobtained::Busy));
|
||||
}
|
||||
|
||||
trace!("Obtained the lock, spawning the lease extension task.");
|
||||
|
||||
// This is the first time we've obtaind the lock. We're going to spawn the task
|
||||
// that will renew the lease.
|
||||
|
||||
// Clone data to be owned by the task.
|
||||
let this = (*self).clone();
|
||||
|
||||
let mut renew_task = self.renew_task.lock().await;
|
||||
|
||||
// Cancel the previous task, if any. That's safe to do, because:
|
||||
// - either the task was done,
|
||||
// - or it was still running, but taking a lock in the db has to be an atomic
|
||||
// operation running in a transaction.
|
||||
|
||||
if let Some(_prev) = renew_task.take() {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
if !_prev.is_finished() {
|
||||
trace!("aborting the previous renew task");
|
||||
_prev.abort();
|
||||
}
|
||||
}
|
||||
|
||||
// Restart a new one.
|
||||
*renew_task = Some(spawn(async move {
|
||||
loop {
|
||||
{
|
||||
// First, check if there are still users of this lock.
|
||||
//
|
||||
// This is not racy, because:
|
||||
// - the `locking_attempt` mutex makes sure we don't have unexpected
|
||||
// interactions with the non-atomic sequence above in `try_lock_once`
|
||||
// (check > 0, then add 1).
|
||||
// - other entities holding onto the `num_holders` atomic will only
|
||||
// decrease it over time.
|
||||
|
||||
let _guard = this.locking_attempt.lock().await;
|
||||
|
||||
// If there are no more users, we can quit.
|
||||
if this.num_holders.load(Ordering::SeqCst) == 0 {
|
||||
trace!("exiting the lease extension loop");
|
||||
|
||||
// Cancel the lease with another 0ms lease.
|
||||
// If we don't get the lock, that's (weird but) fine.
|
||||
let fut = this.locker.try_lock(0, &this.lock_key, &this.lock_holder);
|
||||
let _ = fut.await;
|
||||
|
||||
// Exit the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(EXTEND_LEASE_EVERY_MS)).await;
|
||||
|
||||
match this
|
||||
.locker
|
||||
.try_lock(LEASE_DURATION_MS, &this.lock_key, &this.lock_holder)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_generation)) => {
|
||||
// It's impossible that the generation can be different
|
||||
// from the previous generation.
|
||||
//
|
||||
// As long as the task runs, the lock is renewed, so the
|
||||
// generation remains the same. If the lock is not
|
||||
// taken, it's because the lease has expired, which is
|
||||
// represented by the `Ok(None)` value, and the task
|
||||
// must stop.
|
||||
}
|
||||
|
||||
Ok(None) => {
|
||||
error!("Failed to renew the lock lease: the lock could not be obtained");
|
||||
|
||||
// Exit the loop.
|
||||
break;
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
error!("Error when extending the lock lease: {err:#}");
|
||||
|
||||
// Exit the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
self.num_holders.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let guard = CrossProcessLockGuard::new(self.num_holders.clone(), self.is_dirty.clone());
|
||||
|
||||
Ok(Ok(if self.is_dirty() {
|
||||
CrossProcessLockState::Dirty(guard)
|
||||
} else {
|
||||
CrossProcessLockState::Clean(guard)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Attempt to take the lock, with exponential backoff if the lock has
|
||||
/// already been taken before.
|
||||
///
|
||||
/// The `max_backoff` parameter is the maximum time (in milliseconds) that
|
||||
/// should be waited for, between two attempts. When that time is
|
||||
/// reached a second time, the lock will stop attempting to get the lock
|
||||
/// and will return a timeout error upon locking. If not provided,
|
||||
/// will wait for [`MAX_BACKOFF_MS`].
|
||||
#[instrument(skip(self), fields(?self.lock_key, ?self.lock_holder))]
|
||||
pub async fn spin_lock(
|
||||
&self,
|
||||
max_backoff: Option<u32>,
|
||||
) -> Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, L::LockError> {
|
||||
let max_backoff = max_backoff.unwrap_or(MAX_BACKOFF_MS);
|
||||
|
||||
// Note: reads/writes to the backoff are racy across threads in theory, but the
|
||||
// lock in `try_lock_once` should sequentialize it all.
|
||||
|
||||
loop {
|
||||
let lock_result = self.try_lock_once().await?;
|
||||
|
||||
if lock_result.is_ok() {
|
||||
// Reset backoff before returning, for the next attempt to lock.
|
||||
*self.backoff.lock().await = WaitingTime::Some(INITIAL_BACKOFF_MS);
|
||||
|
||||
return Ok(lock_result);
|
||||
}
|
||||
|
||||
// Exponential backoff! Multiply by 2 the time we've waited before, cap it to
|
||||
// max_backoff.
|
||||
let mut backoff = self.backoff.lock().await;
|
||||
|
||||
let wait = match &mut *backoff {
|
||||
WaitingTime::Some(val) => {
|
||||
let wait = *val;
|
||||
*val = val.saturating_mul(2);
|
||||
if *val >= max_backoff {
|
||||
*backoff = WaitingTime::Stop;
|
||||
}
|
||||
wait
|
||||
}
|
||||
WaitingTime::Stop => {
|
||||
// We've reached the maximum backoff, abandon.
|
||||
return Ok(Err(CrossProcessLockUnobtained::TimedOut));
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Waiting {wait} before re-attempting to take the lock");
|
||||
sleep(Duration::from_millis(wait.into())).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value in the database that represents the holder's
|
||||
/// identifier.
|
||||
pub fn lock_holder(&self) -> &str {
|
||||
&self.lock_holder
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent a successful result of a locking attempt, either by
|
||||
/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
|
||||
#[derive(Debug)]
|
||||
#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
|
||||
pub enum CrossProcessLockState {
|
||||
/// The lock has been obtained successfully, all good.
|
||||
Clean(CrossProcessLockGuard),
|
||||
|
||||
/// The lock has been obtained successfully, but the lock is dirty!
|
||||
///
|
||||
/// This holder has obtained this cross-process lock once, then another
|
||||
/// holder has obtained this cross-process lock _before_ this holder
|
||||
/// obtained it again. The lock is marked as dirty. It means the value
|
||||
/// protected by the cross-process lock may need to be reloaded if
|
||||
/// synchronisation is important.
|
||||
///
|
||||
/// Until [`CrossProcessLock::clear_dirty`] is called,
|
||||
/// [`CrossProcessLock::is_dirty`], [`CrossProcessLock::try_lock_once`] and
|
||||
/// [`CrossProcessLock::spin_lock`] will report the lock as dirty. Put it
|
||||
/// differently: dirty once, dirty forever, unless
|
||||
/// [`CrossProcessLock::clear_dirty`] is called.
|
||||
Dirty(CrossProcessLockGuard),
|
||||
}
|
||||
|
||||
impl CrossProcessLockState {
|
||||
/// Map this value into the inner [`CrossProcessLockGuard`].
|
||||
pub fn into_guard(self) -> CrossProcessLockGuard {
|
||||
match self {
|
||||
Self::Clean(guard) | Self::Dirty(guard) => guard,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map this [`CrossProcessLockState`] into a
|
||||
/// [`MappedCrossProcessLockState`].
|
||||
///
|
||||
/// This is helpful when one wants to create its own wrapper over
|
||||
/// [`CrossProcessLockGuard`].
|
||||
pub fn map<F, G>(self, mapper: F) -> MappedCrossProcessLockState<G>
|
||||
where
|
||||
F: FnOnce(CrossProcessLockGuard) -> G,
|
||||
{
|
||||
match self {
|
||||
Self::Clean(guard) => MappedCrossProcessLockState::Clean(mapper(guard)),
|
||||
Self::Dirty(guard) => MappedCrossProcessLockState::Dirty(mapper(guard)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A mapped [`CrossProcessLockState`].
|
||||
///
|
||||
/// Created by [`CrossProcessLockState::map`].
|
||||
#[derive(Debug)]
|
||||
#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
|
||||
pub enum MappedCrossProcessLockState<G> {
|
||||
/// The equivalent of [`CrossProcessLockState::Clean`].
|
||||
Clean(G),
|
||||
|
||||
/// The equivalent of [`CrossProcessLockState::Dirty`].
|
||||
Dirty(G),
|
||||
}
|
||||
|
||||
impl<G> MappedCrossProcessLockState<G> {
|
||||
/// Return `Some(G)` if `Self` is [`Clean`][Self::Clean].
|
||||
pub fn as_clean(&self) -> Option<&G> {
|
||||
match self {
|
||||
Self::Clean(guard) => Some(guard),
|
||||
Self::Dirty(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent an unsuccessful result of a lock attempt, either by
|
||||
/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CrossProcessLockUnobtained {
|
||||
/// The lock couldn't be obtained immediately because it is busy, i.e. it is
|
||||
/// held by another holder.
|
||||
#[error(
|
||||
"The lock couldn't be obtained immediately because it is busy, i.e. it is held by another holder"
|
||||
)]
|
||||
Busy,
|
||||
|
||||
/// The lock couldn't be obtained after several attempts: locking has timed
|
||||
/// out.
|
||||
#[error("The lock couldn't be obtained after several attempts: locking has timed out")]
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
/// Union of [`CrossProcessLockUnobtained`] and [`TryLock::LockError`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CrossProcessLockError {
|
||||
#[error(transparent)]
|
||||
Unobtained(#[from] CrossProcessLockUnobtained),
|
||||
|
||||
#[error(transparent)]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
TryLock(#[from] Box<dyn Error + Send + Sync>),
|
||||
|
||||
#[error(transparent)]
|
||||
#[cfg(target_family = "wasm")]
|
||||
TryLock(#[from] Box<dyn Error>),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(target_family = "wasm"))] // These tests require tokio::time, which is not implemented on wasm.
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::Not,
|
||||
sync::{Arc, RwLock, atomic},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
use tokio::{spawn, task::yield_now};
|
||||
|
||||
use super::{
|
||||
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockState,
|
||||
CrossProcessLockUnobtained, TryLock,
|
||||
memory_store_helper::{Lease, try_take_leased_lock},
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct TestStore {
|
||||
leases: Arc<RwLock<HashMap<String, Lease>>>,
|
||||
}
|
||||
|
||||
impl TestStore {
|
||||
fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Option<CrossProcessLockGeneration> {
|
||||
try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum DummyError {}
|
||||
|
||||
impl From<DummyError> for CrossProcessLockError {
|
||||
fn from(value: DummyError) -> Self {
|
||||
Self::TryLock(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryLock for TestStore {
|
||||
type LockError = DummyError;
|
||||
|
||||
/// Try to take a lock using the given store.
|
||||
async fn try_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::LockError> {
|
||||
Ok(self.try_take_leased_lock(lease_duration_ms, key, holder))
|
||||
}
|
||||
}
|
||||
|
||||
async fn release_lock(lock: CrossProcessLockState) {
|
||||
drop(lock);
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
type TestResult = Result<(), CrossProcessLockError>;
|
||||
|
||||
#[async_test]
|
||||
async fn test_simple_lock_unlock() -> TestResult {
|
||||
let store = TestStore::default();
|
||||
let lock = CrossProcessLock::new(store, "key".to_owned(), "first".to_owned());
|
||||
|
||||
// The lock plain works when used with a single holder.
|
||||
let guard = lock.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Clean(_));
|
||||
assert!(lock.is_dirty().not());
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
|
||||
|
||||
// Releasing works.
|
||||
release_lock(guard).await;
|
||||
assert!(lock.is_dirty().not());
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0);
|
||||
|
||||
// Spin locking on the same lock always works, assuming no concurrent access.
|
||||
let guard = lock.spin_lock(None).await?.expect("spin lock must be obtained successfully");
|
||||
assert!(lock.is_dirty().not());
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
|
||||
|
||||
// Releasing still works.
|
||||
release_lock(guard).await;
|
||||
assert!(lock.is_dirty().not());
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_self_recovery() -> TestResult {
|
||||
let store = TestStore::default();
|
||||
let lock = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned());
|
||||
|
||||
// When a lock is obtained…
|
||||
let guard = lock.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Clean(_));
|
||||
assert!(lock.is_dirty().not());
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
|
||||
|
||||
// But then forgotten… (note: no need to release the guard)
|
||||
drop(lock);
|
||||
|
||||
// And when rematerializing the lock with the same key/value…
|
||||
let lock = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned());
|
||||
|
||||
// We still got it.
|
||||
let guard =
|
||||
lock.try_lock_once().await?.expect("lock (again) must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Clean(_));
|
||||
assert!(lock.is_dirty().not());
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_multiple_holders_same_process() -> TestResult {
|
||||
let store = TestStore::default();
|
||||
let lock = CrossProcessLock::new(store, "key".to_owned(), "first".to_owned());
|
||||
|
||||
// Taking the lock twice…
|
||||
let guard1 = lock.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard1, CrossProcessLockState::Clean(_));
|
||||
let guard2 = lock.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard2, CrossProcessLockState::Clean(_));
|
||||
assert!(lock.is_dirty().not());
|
||||
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 2);
|
||||
|
||||
// … means we can release it twice.
|
||||
release_lock(guard1).await;
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
|
||||
|
||||
release_lock(guard2).await;
|
||||
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0);
|
||||
|
||||
assert!(lock.is_dirty().not());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_multiple_processes() -> TestResult {
|
||||
let store = TestStore::default();
|
||||
let lock1 = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned());
|
||||
let lock2 = CrossProcessLock::new(store, "key".to_owned(), "second".to_owned());
|
||||
|
||||
// `lock1` acquires the lock.
|
||||
let guard1 = lock1.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard1, CrossProcessLockState::Clean(_));
|
||||
assert!(lock1.is_dirty().not());
|
||||
|
||||
// `lock2` cannot acquire the lock.
|
||||
let err = lock2.try_lock_once().await?.expect_err("lock must NOT be obtained");
|
||||
assert_matches!(err, CrossProcessLockUnobtained::Busy);
|
||||
|
||||
// `lock2` is waiting in a task.
|
||||
let lock2_clone = lock2.clone();
|
||||
let task = spawn(async move { lock2_clone.spin_lock(Some(500)).await });
|
||||
|
||||
yield_now().await;
|
||||
|
||||
drop(guard1);
|
||||
|
||||
// Once `lock1` is released, `lock2` managed to obtain it.
|
||||
let guard2 = task
|
||||
.await
|
||||
.expect("join handle is properly awaited")
|
||||
.expect("lock is successfully attempted")
|
||||
.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard2, CrossProcessLockState::Clean(_));
|
||||
|
||||
// `lock1` and `lock2` are both clean!
|
||||
assert!(lock1.is_dirty().not());
|
||||
assert!(lock2.is_dirty().not());
|
||||
|
||||
// Now if `lock1` tries to obtain the lock with a small timeout, it will fail.
|
||||
assert_matches!(
|
||||
lock1.spin_lock(Some(200)).await,
|
||||
Ok(Err(CrossProcessLockUnobtained::TimedOut))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_multiple_processes_up_to_dirty() -> TestResult {
|
||||
let store = TestStore::default();
|
||||
let lock1 = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned());
|
||||
let lock2 = CrossProcessLock::new(store, "key".to_owned(), "second".to_owned());
|
||||
|
||||
// Obtain `lock1` once.
|
||||
{
|
||||
let guard = lock1.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Clean(_));
|
||||
assert!(lock1.is_dirty().not());
|
||||
drop(guard);
|
||||
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
// Obtain `lock2` once.
|
||||
{
|
||||
let guard = lock2.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Clean(_));
|
||||
assert!(lock1.is_dirty().not());
|
||||
drop(guard);
|
||||
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
for _ in 0..3 {
|
||||
// Obtain `lock1` once more. Now it's dirty because `lock2` has acquired the
|
||||
// lock meanwhile.
|
||||
{
|
||||
let guard =
|
||||
lock1.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Dirty(_));
|
||||
assert!(lock1.is_dirty());
|
||||
|
||||
drop(guard);
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
// Obtain `lock1` once more! It still dirty because it has not been marked as
|
||||
// non-dirty.
|
||||
{
|
||||
let guard =
|
||||
lock1.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Dirty(_));
|
||||
assert!(lock1.is_dirty());
|
||||
lock1.clear_dirty();
|
||||
|
||||
drop(guard);
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
// Obtain `lock1` once more. Now it's clear!
|
||||
{
|
||||
let guard =
|
||||
lock1.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Clean(_));
|
||||
assert!(lock1.is_dirty().not());
|
||||
|
||||
drop(guard);
|
||||
yield_now().await;
|
||||
}
|
||||
|
||||
// Same dance with `lock2`!
|
||||
{
|
||||
let guard =
|
||||
lock2.try_lock_once().await?.expect("lock must be obtained successfully");
|
||||
assert_matches!(guard, CrossProcessLockState::Dirty(_));
|
||||
assert!(lock2.is_dirty());
|
||||
lock2.clear_dirty();
|
||||
|
||||
drop(guard);
|
||||
yield_now().await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Some code that is shared by almost all `MemoryStore` implementations out
|
||||
/// there.
|
||||
pub mod memory_store_helper {
|
||||
use std::collections::{HashMap, hash_map::Entry};
|
||||
|
||||
use ruma::time::{Duration, Instant};
|
||||
|
||||
use super::{CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Lease {
|
||||
holder: String,
|
||||
expiration: Instant,
|
||||
generation: CrossProcessLockGeneration,
|
||||
}
|
||||
|
||||
pub fn try_take_leased_lock(
|
||||
leases: &mut HashMap<String, Lease>,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Option<CrossProcessLockGeneration> {
|
||||
let now = Instant::now();
|
||||
let expiration = now + Duration::from_millis(lease_duration_ms.into());
|
||||
|
||||
match leases.entry(key.to_owned()) {
|
||||
// There is an existing holder.
|
||||
Entry::Occupied(mut entry) => {
|
||||
let Lease {
|
||||
holder: current_holder,
|
||||
expiration: current_expiration,
|
||||
generation: current_generation,
|
||||
} = entry.get_mut();
|
||||
|
||||
if current_holder == holder {
|
||||
// We had the lease before, extend it.
|
||||
*current_expiration = expiration;
|
||||
|
||||
Some(*current_generation)
|
||||
} else {
|
||||
// We didn't have it.
|
||||
if *current_expiration < now {
|
||||
// Steal it!
|
||||
*current_holder = holder.to_owned();
|
||||
*current_expiration = expiration;
|
||||
*current_generation += 1;
|
||||
|
||||
Some(*current_generation)
|
||||
} else {
|
||||
// We tried our best.
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There is no holder, easy.
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Lease {
|
||||
holder: holder.to_owned(),
|
||||
expiration: Instant::now() + Duration::from_millis(lease_duration_ms.into()),
|
||||
generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
|
||||
});
|
||||
|
||||
Some(FIRST_CROSS_PROCESS_LOCK_GENERATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user