Compare commits
1239 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 | |||
| 9ffe5aa6ca | |||
| c604e4acd2 | |||
| f8b343bece | |||
| 94f8f8c44c | |||
| 4c1f80faf7 | |||
| f9bf492fdb | |||
| 824fc0b62e | |||
| 359db7f28b | |||
| 30672e6feb | |||
| f9b419077d | |||
| d46f934d57 | |||
| 0bed6afc29 | |||
| 412d4b80ee | |||
| bcabf1bda4 | |||
| 7767ef6ca3 | |||
| 6765ca0c39 | |||
| 17abab0d53 | |||
| cc0bf91a06 | |||
| 0b3345f592 | |||
| 472b934816 | |||
| 27a28e55d1 | |||
| d6a418f46a | |||
| 268e14e4f5 | |||
| f1190deef9 | |||
| ee62cd749f | |||
| cea5c190d8 | |||
| ad4cb4f6c9 | |||
| 949e7a6cac | |||
| 8e66963a1e | |||
| aa02e31cf6 | |||
| 57c7972c63 | |||
| e89ac3d7df | |||
| a3704c3563 | |||
| 5fb728e8f0 | |||
| eab62ec0b5 | |||
| 2fae949a42 | |||
| 4adbb4aa88 | |||
| 18affe3edd | |||
| ea59bc8955 | |||
| 68f6d927f1 | |||
| c3766789cc | |||
| 7c31525f68 | |||
| b2dd5ce02d | |||
| 1f2b4f87bc | |||
| 893c45af74 | |||
| a161dfa9a0 | |||
| 20cd0bedfa | |||
| d4a1ce06e4 | |||
| e53906a920 | |||
| 1e30916754 | |||
| 15c46b503c | |||
| 5cd3818841 | |||
| 79b7d6d235 | |||
| 05d0f9e077 | |||
| 4c1e2d6d51 | |||
| c9162373a1 | |||
| 9f22f550bf | |||
| 7a762035f1 | |||
| 8c0a918e6e | |||
| 33c317e6d2 | |||
| 371ed49670 | |||
| f0c1c65308 | |||
| ea5063ca84 | |||
| dcc07e1049 | |||
| 76a0eb1599 | |||
| a9c16c96e0 | |||
| 93f8ebba27 | |||
| 95bb153269 | |||
| ff72a09870 | |||
| 06de58dee9 | |||
| fbb49e9b65 | |||
| 04728cc1a6 | |||
| d43d141dc8 | |||
| 3c069f0c5c | |||
| a2200b6324 | |||
| 9caa0817ae | |||
| b3229041fb | |||
| 50446377de | |||
| 4f02a6d3be | |||
| 87fdd3c3bf | |||
| 990fe86fdc | |||
| be2ba26974 | |||
| 1392a0e637 | |||
| 03ffa4c9a4 | |||
| f8b5992101 | |||
| 97a5fbebfb | |||
| 74c2032974 | |||
| 274aaf5ba3 | |||
| 3e72cce7a0 | |||
| aa4b176ab3 | |||
| ad2f4c731a | |||
| f78015fae1 | |||
| 211a1f5a40 | |||
| b8be1fdb26 | |||
| a43e42c170 | |||
| f031eaf96b | |||
| 3ba31d1e97 | |||
| bbf8f9f900 | |||
| 49dc2bb640 | |||
| 99af951d7a | |||
| a17bf18ff2 | |||
| 5d87570a33 | |||
| 80806303b5 | |||
| 6155772bb1 | |||
| 33db267a89 | |||
| 6fc68dac83 | |||
| f3eeb82b0b | |||
| 951d22ac24 | |||
| 527d001010 | |||
| 759eeeb27f | |||
| 97d6f57aee | |||
| 6622a3ac93 | |||
| 39730173d4 | |||
| 763314645b | |||
| b2fee72d79 | |||
| 9803d2bcca | |||
| 296867d2ac | |||
| 710b57e035 | |||
| baa75368d6 | |||
| feb264e899 | |||
| 8e5075569e | |||
| 33c11d08f0 | |||
| 9714ac8e10 | |||
| e1d136aa6e | |||
| 8018753332 | |||
| 61824f866c | |||
| 6919444e98 | |||
| c5097cf07e | |||
| b77c6c65cc | |||
| f4ce4356ab | |||
| d66733052a | |||
| 001dadffe1 | |||
| b2387bf3a9 | |||
| d43858ecb2 | |||
| 8804966094 | |||
| a3ee011b61 | |||
| f586172f3e | |||
| 85d52586b6 | |||
| 40d3dd57db | |||
| 4e2655aa1b | |||
| 41fcebbcb0 | |||
| 13b86a3f5d | |||
| dba23b66fa | |||
| 132d0eb34a | |||
| e2e70448ca | |||
| adaccbab2c | |||
| 7a09ca0bbd | |||
| fffdd34ebd | |||
| 24502d2706 | |||
| b6433dea27 | |||
| 385f1a824f | |||
| dfc0ef8b35 | |||
| d2feeaac30 | |||
| 25a81876a0 | |||
| e388fe6522 | |||
| ef20342ddf | |||
| e6b1ffba99 | |||
| 9a3ceb8be6 | |||
| faee647c3a | |||
| 1866143456 | |||
| be8e322ad6 | |||
| 838f607b32 | |||
| 6965004812 | |||
| 8ec23a95d5 | |||
| 7880ec5b01 | |||
| 36428564fc | |||
| 7adaf7be73 | |||
| 7920723bb4 | |||
| b73163aa45 | |||
| feeeb53f19 | |||
| 1ac876db98 | |||
| afaf2cc036 | |||
| 5a3bb0a86d | |||
| 2640aa1e23 | |||
| d1a8392ce7 | |||
| 4d14dd3692 | |||
| 63defca8af | |||
| fd83904b4d | |||
| 9254c38a8d | |||
| 63d9dd5c6e | |||
| 23dacc329e | |||
| 5896a438f5 | |||
| 31a3d76436 | |||
| dfaaf323ad | |||
| 94439d8913 | |||
| 9cc29d7c65 | |||
| f2fbdfbac2 | |||
| fd34927f61 | |||
| 0190d3556d | |||
| 2d657fe908 | |||
| 5e43177d3a | |||
| e44b01cbe5 | |||
| 4882c98f99 | |||
| 4bf0187310 | |||
| 10ca400d4d | |||
| cc61e123b7 | |||
| 6f23981268 | |||
| 859044285a | |||
| 6c1134006e | |||
| 6d1cdbc613 | |||
| 6160c15103 | |||
| 100cbde526 | |||
| 6ff8a26cca | |||
| a1c484fb6e | |||
| d2ecc77014 | |||
| 2e86fbc234 | |||
| 64698eaf1a | |||
| f180a14c88 | |||
| bb0d480f24 | |||
| 7af1d3ab0e | |||
| 13ee4c8098 | |||
| 7bbd02ca73 | |||
| e22a7a2ed5 | |||
| 4aad2c6b07 | |||
| 960162453c | |||
| 1ea2162012 | |||
| 1f0151705a | |||
| 84ebbd913c | |||
| 756d50737e | |||
| c32877284c | |||
| 6260811ea5 | |||
| 4f0415d4d2 | |||
| b43aac129b | |||
| c019009d00 | |||
| a88d6b37dc | |||
| 705d6f870e | |||
| 4fc28c4701 | |||
| 64eecd0aee | |||
| c25be8b070 | |||
| 1554c9d8fa | |||
| d568c07489 | |||
| 13c30f6691 | |||
| 0e70a2fdfb | |||
| d70d758861 | |||
| eefa9ff556 | |||
| 28a8603f42 | |||
| ae7f0fe022 | |||
| d9f4e7c426 | |||
| 247ec1dcd2 | |||
| 558d7b56f9 | |||
| 1201be484a | |||
| 1ffc014621 | |||
| 9491757cad | |||
| 33df0422e8 | |||
| a3a239f999 | |||
| ca8b64e041 | |||
| 140e751af0 | |||
| a66b2c5123 | |||
| 69bef9a76a | |||
| b3c53dd08f | |||
| c8bffa26a4 | |||
| b4ef6cef55 | |||
| c6854a5c22 | |||
| fb563953c9 | |||
| bc0018aecb | |||
| 12292c5375 | |||
| cf9d058265 | |||
| 4da13e1096 | |||
| 333d4563ce | |||
| 01059ef26c | |||
| 7724271508 | |||
| 8dfe732cce | |||
| 1cf3477ada | |||
| 0a2205f540 | |||
| c586812159 | |||
| c6210cad21 | |||
| a9ce1c6e58 | |||
| 1eb8f6ac16 | |||
| e0feebdb2b | |||
| 0fee716c1e | |||
| c41ed8a78a | |||
| 53f02c9f2d | |||
| e2f0b4f3fd | |||
| 0a796cb468 | |||
| e3390c17ec | |||
| c6dc070c31 | |||
| 486befc7fb | |||
| 9848d1472e | |||
| 6c944a9b39 | |||
| b4b010f9fe | |||
| 536ba518bb | |||
| 917c46b570 | |||
| b29886c0df | |||
| 360c2d7f32 | |||
| 683f0f4027 | |||
| c783ed8a6f | |||
| 139673810f | |||
| 669ebf2408 | |||
| 992774b8b5 | |||
| 9d90a92b4c | |||
| d79975e0e3 | |||
| 2e598c0532 | |||
| 5a3ef30fdc | |||
| 05178ccaf9 | |||
| 65b9bd20a8 | |||
| 35505f9130 | |||
| a6d630216d | |||
| 159c9b4547 | |||
| aead1a4489 | |||
| 7fee1c7fd7 | |||
| ab9bfb2d61 | |||
| de5f00fd33 | |||
| 33c16b2979 | |||
| e9dcdb7176 | |||
| 0a3fe939c5 | |||
| 37e07ea331 | |||
| e4e3ff63f5 | |||
| 8409e52654 | |||
| e8096ee518 | |||
| 6814e70aa4 | |||
| efa4539a91 | |||
| 42d2b93489 | |||
| 872713c4bc | |||
| feb22d4370 | |||
| 6520c9b16e | |||
| cd6fe271ba | |||
| 5f447bbb17 | |||
| 94e7ddd1ab | |||
| 6ac4a8431d | |||
| b585963abb | |||
| 5719fde701 | |||
| 2914d7a727 | |||
| 0cdec9d912 | |||
| d180d49c07 | |||
| bcee5badae | |||
| ebb7059d55 | |||
| 8d3b1d3c7e | |||
| 056e90db25 | |||
| 787861eb35 | |||
| f081416baa | |||
| 8a6cc7bc22 | |||
| 61258e823f | |||
| e86aab68b4 | |||
| 48f1bc0780 | |||
| 1fe71acbcb | |||
| 0e054deb19 | |||
| d2b7dc6116 | |||
| 1089a25588 | |||
| 3276bc87ad | |||
| a4da6ba7c8 | |||
| 033c6bd6a4 | |||
| b02e1da471 | |||
| 5ad477ac96 | |||
| 975432565d | |||
| 46b0113765 | |||
| 09eff8c6bd | |||
| 7ee546a3d9 | |||
| b164cd6a51 | |||
| 6d95abfb36 | |||
| 33f09d6d26 | |||
| 8c01e99144 | |||
| 277cb7ac49 | |||
| fc7124fd1a | |||
| 30c0420f83 | |||
| cb13c345ad | |||
| cd26973082 | |||
| 0edcdd33b2 | |||
| c191eb7cd1 | |||
| fa6f270812 | |||
| d4e96595d9 | |||
| 540a11e7a8 | |||
| 92192c549b | |||
| 88360040fb | |||
| 4184e245a4 | |||
| f37bf2f5d1 | |||
| d57d3c4124 | |||
| 1a5cb2beb8 | |||
| b645c1101f | |||
| 8091094bbc | |||
| feadfde1b5 | |||
| e2ad07881c | |||
| 1be8b42d03 | |||
| 7a5f83f6ec | |||
| 88bb7a366f | |||
| 7d9bf56581 | |||
| 770f65ede0 | |||
| 1f33e0f4d1 | |||
| 117f76102d | |||
| fd04ebfaba | |||
| afe9f7a979 | |||
| 27a002c8e2 | |||
| b8ab0972b3 | |||
| d3419ea4ac | |||
| 019adb9a56 | |||
| ca89700dfe | |||
| a0c87cfe4f | |||
| 9ddc892aa0 | |||
| 0a7ac18d9f | |||
| d8b6966c0a | |||
| d40f04e32c | |||
| d8294a0788 | |||
| 06a4476e7f | |||
| def1fedea3 | |||
| d061e7a5b2 | |||
| f4619c91d3 | |||
| 227f6eab85 | |||
| 16d7c3c094 | |||
| c238a0edb8 | |||
| 06bf487512 | |||
| c636ec63f4 | |||
| ffe239d620 | |||
| 822b709107 | |||
| d75d7973b2 | |||
| cfe3adce48 | |||
| b478ae65f7 | |||
| ada68e1114 | |||
| c9137f0cad | |||
| 4e0dab959a | |||
| e862ded147 | |||
| 5cb033ad91 | |||
| 0e622cc5a1 | |||
| 6d562eff2f | |||
| af2e15e02f | |||
| 79aa5aaf16 | |||
| 0833ffdef2 | |||
| 16f7239215 | |||
| da946e51dd | |||
| cba711dbdf | |||
| e87e9331c1 | |||
| 6e9fc70d13 | |||
| e2148e46bc | |||
| d1163b75bf | |||
| 5ae7d0f60f | |||
| 6f067d5510 | |||
| d6fe654814 | |||
| 2710510786 | |||
| 35a8528712 | |||
| f9735c75d3 | |||
| 15e6b81835 | |||
| bcb4ab4b10 | |||
| 4931c0749e | |||
| 37626b5ad9 | |||
| d19616da03 | |||
| 7c8f870d16 | |||
| b482ccd318 | |||
| 165ec9db1b | |||
| 6f42210d6a | |||
| 6125580275 | |||
| 8b3e295429 | |||
| 1e568efbb5 | |||
| f89ced3ded | |||
| a5fbcf1491 | |||
| 8923e58ee3 | |||
| f14994baa9 | |||
| d42d0f3e17 | |||
| e4849d5cab | |||
| 65aec7ee7f | |||
| a6868386d0 | |||
| b3c1ca1577 | |||
| ce66ee4a16 | |||
| 2bbf6fc711 | |||
| 1a9e5b904b | |||
| 4c43b06445 | |||
| 49a0765880 | |||
| 39cf8b325d | |||
| bb67150d6b | |||
| 471e3c340c | |||
| 74972d8db7 | |||
| 03a76fbaf5 | |||
| 392a1ef47b | |||
| c5f2460e02 | |||
| 8ad785a117 | |||
| a5b936d0b6 | |||
| 1a0544c8eb | |||
| 232c23e8df | |||
| 7d9d5bf3b4 | |||
| ea076b3d76 | |||
| 8aa6f97f7c | |||
| 679aa07115 | |||
| 0c66d8a53f | |||
| 25ed7eef2b | |||
| a399840dff | |||
| 8d4e7f0478 | |||
| 3bd93130c5 | |||
| 153618b77c | |||
| dd871ef9ac | |||
| 8a29f17d1d | |||
| fab520ab33 | |||
| 435553c3d1 | |||
| 610ecd218c | |||
| fa300d1f33 | |||
| af2a483158 | |||
| 753b0d8584 | |||
| 36713adbdb | |||
| d73a02c608 | |||
| f73199b472 | |||
| 420d373144 | |||
| a79e9130e6 | |||
| 355b5327f8 | |||
| fa77852001 | |||
| 7b73311de5 | |||
| f03934bc4f | |||
| 014ee98fb7 | |||
| fbcf9fce7c | |||
| 6de403276a | |||
| 6209bc942c | |||
| edd371b570 | |||
| 817f32e15b | |||
| 30eb12ed2d | |||
| 900697bc3b |
@@ -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"]
|
||||
|
||||
+5
-15
@@ -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]
|
||||
@@ -17,6 +18,7 @@ version = 2
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"CDLA-Permissive-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
@@ -26,18 +28,6 @@ allow = [
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
exceptions = [
|
||||
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
|
||||
{ allow = ["CDDL-1.0"], crate = "inferno" },
|
||||
{ allow = ["LicenseRef-ring"], crate = "ring" },
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "LicenseRef-ring"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[bans]
|
||||
# We should disallow this, but it's currently a PITA.
|
||||
@@ -51,9 +41,7 @@ unknown-git = "deny"
|
||||
allow-git = [
|
||||
# A patch override for the bindings fixing a bug for Android before upstream
|
||||
# releases a new version.
|
||||
"https://github.com/element-hq/tracing.git",
|
||||
# Same as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
"https://github.com/tokio-rs/tracing.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
@@ -63,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
|
||||
|
||||
@@ -1,54 +1,99 @@
|
||||
name: Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
benchmarks:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
environment: matrix-rust-bot
|
||||
if: github.event_name == 'push'
|
||||
strategy:
|
||||
matrix:
|
||||
benchmark:
|
||||
- crypto_bench
|
||||
- event_cache
|
||||
- linked_chunk
|
||||
- store_bench
|
||||
- timeline
|
||||
- room_list
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
# 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
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-06-27
|
||||
components: rustfmt
|
||||
cd /opt
|
||||
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
|
||||
rm -rf /opt/hostedtoolcache
|
||||
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench | tee benchmark-output.txt
|
||||
# Get rid of binaries and libs we're not interested in.
|
||||
sudo rm -rf \
|
||||
/usr/local/julia* \
|
||||
/usr/local/aws*
|
||||
|
||||
- name: Check benchmark result for PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
auto-push: false
|
||||
# comment to alert the user this has gone bad
|
||||
github-token: ${{ secrets.MRB_ACCESS_TOKEN }}
|
||||
alert-threshold: '120%'
|
||||
comment-on-alert: true
|
||||
fail-threshold: '150%'
|
||||
fail-on-alert: true
|
||||
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
|
||||
|
||||
- name: Store benchmark result
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-push: true
|
||||
# Show alert with commit comment on detecting possible performance regression
|
||||
alert-threshold: '150%'
|
||||
comment-on-alert: true
|
||||
fail-on-alert: true
|
||||
alert-comment-cc-users: '@gnunicornBen,@jplatte,@poljar'
|
||||
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
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
|
||||
|
||||
- name: Run the benchmarks
|
||||
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@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -69,17 +69,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
ref: main
|
||||
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@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@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
|
||||
+30
-33
@@ -34,14 +34,16 @@ jobs:
|
||||
- no-sqlite
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- experimental-encrypted-state-events
|
||||
- rustls-tls
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
- search
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -83,7 +85,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -114,7 +116,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -123,6 +125,8 @@ jobs:
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -165,7 +169,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -237,7 +241,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -246,10 +250,10 @@ 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.10.3
|
||||
version: v0.13.1
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -287,10 +291,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.34.0
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -299,7 +303,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
@@ -309,7 +313,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-06-27
|
||||
toolchain: nightly-2025-10-01
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -333,8 +337,7 @@ jobs:
|
||||
target/debug/xtask ci clippy
|
||||
|
||||
integration-tests:
|
||||
name: Integration test
|
||||
|
||||
name: 'Integration test (features: ${{ matrix.feature }})'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
@@ -350,9 +353,16 @@ jobs:
|
||||
ports:
|
||||
- 8008:8008
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
feature:
|
||||
- "default"
|
||||
- "experimental-encrypted-state-events"
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -376,24 +386,11 @@ jobs:
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
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@v4
|
||||
|
||||
- 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 }}
|
||||
|
||||
@@ -42,10 +42,62 @@ jobs:
|
||||
# This CI workflow can run into space issue, so we're cleaning up some
|
||||
# space here.
|
||||
- name: Create some more space
|
||||
run: rm -rf /opt/hostedtoolcache
|
||||
run: |
|
||||
echo "Disk space before cleanup"
|
||||
df -h
|
||||
|
||||
cd /opt
|
||||
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
|
||||
rm -rf /opt/hostedtoolcache
|
||||
|
||||
# Get rid of binaries and libs we're not interested in.
|
||||
sudo rm -rf \
|
||||
/usr/local/julia* \
|
||||
/usr/local/aws*
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/bin/minikube \
|
||||
/usr/local/bin/node \
|
||||
/usr/local/bin/stack \
|
||||
/usr/local/bin/bicep \
|
||||
/usr/local/bin/pulumi* \
|
||||
/usr/local/bin/helm \
|
||||
/usr/local/bin/azcopy \
|
||||
/usr/local/bin/packer \
|
||||
/usr/local/bin/cmake-gui \
|
||||
/usr/local/bin/cpack
|
||||
|
||||
sudo rm -rf \
|
||||
/usr/local/share/powershell \
|
||||
/usr/local/share/chromium
|
||||
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
|
||||
echo "::group::/usr/local/bin/*"
|
||||
du -hsc /usr/local/bin/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/share/*"
|
||||
du -hsc /usr/local/share/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/*"
|
||||
du -hsc /usr/local/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/usr/local/lib/*"
|
||||
du -hsc /usr/local/lib/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::/opt/*"
|
||||
du -hsc /opt/* | sort -h
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "Disk space after cleanup"
|
||||
df -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -53,6 +105,8 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsqlite3-dev
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -81,6 +135,10 @@ jobs:
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Check total disk space before running
|
||||
run: |
|
||||
df -h
|
||||
|
||||
- name: Create the coverage report
|
||||
run: |
|
||||
target/debug/xtask ci coverage -o codecov
|
||||
@@ -109,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@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@v0.8.0
|
||||
uses: bnjbvr/cargo-machete@72602674bc341ca927683caddbf578672c352476
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
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-06-27
|
||||
toolchain: nightly-2025-10-01
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@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@v4
|
||||
- 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@v4
|
||||
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@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
|
||||
@@ -4,6 +4,7 @@ master.zip
|
||||
emsdk-*
|
||||
.idea/
|
||||
.env
|
||||
.envrc
|
||||
.build
|
||||
.swiftpm
|
||||
/Package.swift
|
||||
|
||||
+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
+1757
-971
File diff suppressed because it is too large
Load Diff
+74
-59
@@ -13,29 +13,34 @@ 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.85"
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.95"
|
||||
anyhow = "1.0.100"
|
||||
aquamarine = "0.6.0"
|
||||
as_variant = "1.3.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-compat = "0.2.4"
|
||||
async-compat = "0.2.5"
|
||||
async-rx = "0.1.3"
|
||||
# Bumping this to 0.3.6 produces a test failure because the semantic between the
|
||||
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.8.0"
|
||||
bitflags = "2.10.0"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
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"
|
||||
@@ -44,31 +49,32 @@ gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.2.0"
|
||||
imbl = "5.0.0"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json", "redactions"] }
|
||||
http = "1.3.1"
|
||||
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.69"
|
||||
js-sys = "0.3.82"
|
||||
mime = "0.3.17"
|
||||
once_cell = "1.20.2"
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
|
||||
once_cell = "1.21.3"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.12", default-features = false }
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.12.24", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.5", features = [
|
||||
ruma = { version = "0.14.0", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
"compat-encrypted-stickers",
|
||||
"compat-lax-room-create-deser",
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3230",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
@@ -78,46 +84,52 @@ ruma = { version = "0.12.5", features = [
|
||||
"unstable-msc4171",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
"unstable-msc4306",
|
||||
"unstable-msc4308",
|
||||
"unstable-msc4310",
|
||||
] }
|
||||
ruma-common = "0.15.4"
|
||||
sentry = "0.36.0"
|
||||
sentry-tracing = "0.36.0"
|
||||
serde = { version = "1.0.217", features = ["rc"] }
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.6.1"
|
||||
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.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.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.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
tracing-subscriber = "0.3.18"
|
||||
unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.4"
|
||||
uuid = "1.12.1"
|
||||
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.25"
|
||||
uniffi = { version = "0.30.0" }
|
||||
uniffi_bindgen = { version = "0.30.0" }
|
||||
url = "2.5.7"
|
||||
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"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
wasm-bindgen = "0.2.105"
|
||||
wasm-bindgen-test = "0.3.55"
|
||||
web-sys = "0.3.82"
|
||||
wiremock = "0.6.5"
|
||||
zeroize = "1.8.2"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.13.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.13.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.13.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.13.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.13.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.13.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.13.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.13.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.13.0", default-features = false }
|
||||
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"
|
||||
@@ -182,12 +194,15 @@ lto = false
|
||||
# Get symbol names for profiling purposes.
|
||||
debug = true
|
||||
|
||||
[profile.bench]
|
||||
inherits = "release"
|
||||
lto = false
|
||||
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-core = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-subscriber = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
tracing-appender = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git", rev = "69388ac5b4afeed7be4401c70ce17f6d9a2cf19b" }
|
||||
tracing = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-core = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-appender = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
|
||||
+17
-6
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "benchmarks"
|
||||
description = "Matrix SDK benchmarks"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version.workspace = true
|
||||
version = "1.0.0"
|
||||
@@ -10,24 +10,27 @@ publish = false
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[features]
|
||||
codspeed = []
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.5.1", features = ["async", "async_tokio", "html_reports"] }
|
||||
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
|
||||
tempfile = "3.3.0"
|
||||
tempfile.workspace = true
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
wiremock.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
@@ -47,3 +50,11 @@ harness = false
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "event_cache"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_list"
|
||||
harness = false
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
use matrix_sdk_test::ruma_response_from_json;
|
||||
use ruma::{
|
||||
DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
api::client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
device_id, room_id, user_id,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::runtime::Builder;
|
||||
@@ -58,10 +59,14 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Device keys query [memory]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
},
|
||||
);
|
||||
|
||||
// Benchmark sqlite store.
|
||||
|
||||
@@ -71,10 +76,14 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("Device keys query [SQLite]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
@@ -84,6 +93,8 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
/// This test panics on the CI, not sure why so we're disabling it for now.
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
pub fn keys_claiming(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
@@ -99,49 +110,65 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
|
||||
let name = format!("{count} one-time keys");
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("One-time keys claiming [memory]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine);
|
||||
})
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("One-time keys claiming [SQLite]", &name),
|
||||
&response,
|
||||
|b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = Arc::new(
|
||||
runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap(),
|
||||
);
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(
|
||||
alice_id(),
|
||||
alice_device_id(),
|
||||
store,
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
});
|
||||
|
||||
let _ = runtime.enter();
|
||||
drop(machine);
|
||||
})
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sqlite store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store =
|
||||
Arc::new(runtime.block_on(SqliteCryptoStore::open(dir.path(), None)).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store, None))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(async {
|
||||
machine.mark_request_as_sent(txn_id, response).await.unwrap();
|
||||
drop(machine)
|
||||
})
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
group.finish()
|
||||
}
|
||||
@@ -169,7 +196,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Room key sharing [memory]", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_room_key(
|
||||
@@ -201,7 +228,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Room key sharing [SQLite]", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_room_key(
|
||||
@@ -249,7 +276,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
// Benchmark memory store.
|
||||
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Devices collecting [memory]", &name), |b| {
|
||||
b.to_async(&runtime).iter_with_large_drop(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
@@ -266,7 +293,7 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sqlite store", &name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Devices collecting [SQLite]", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
@@ -280,21 +307,18 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = keys_query, keys_claiming, room_key_sharing, devices_missing_sessions_collecting,
|
||||
}
|
||||
|
||||
#[cfg(feature = "codspeed")]
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default();
|
||||
targets = keys_query, room_key_sharing, devices_missing_sessions_collecting,
|
||||
}
|
||||
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
RoomInfo, RoomState, SqliteEventCacheStore, StateStore,
|
||||
store::StoreConfig,
|
||||
sync::{JoinedRoomUpdate, RoomUpdates},
|
||||
test_utils::client::MockClientBuilder,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore};
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventId, RoomId, event_id,
|
||||
events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
|
||||
room_id,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
type StoreBuilder = Box<dyn Fn() -> Pin<Box<dyn Future<Output = Arc<DynEventCacheStore>>>>>;
|
||||
|
||||
fn handle_room_updates(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let mut group = c.benchmark_group("Event cache room updates");
|
||||
group.sample_size(10);
|
||||
|
||||
const NUM_EVENTS: usize = 1000;
|
||||
|
||||
for num_rooms in [1, 10, 100] {
|
||||
// Add some joined rooms, each with NUM_EVENTS in it, to the sync response.
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
|
||||
let mut changes = matrix_sdk::StateChanges::default();
|
||||
|
||||
for i in 0..num_rooms {
|
||||
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap();
|
||||
let event_factory = EventFactory::new().room(&room_id).sender(&ALICE);
|
||||
|
||||
let mut joined_room_update = JoinedRoomUpdate::default();
|
||||
for j in 0..NUM_EVENTS {
|
||||
let event_id = EventId::parse(format!("$ev{i}_{j}")).unwrap();
|
||||
let event =
|
||||
event_factory.text_msg(format!("Message {j}")).event_id(&event_id).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(room_id.clone(), joined_room_update);
|
||||
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
}
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let temp_dir = Arc::new(tempdir().unwrap());
|
||||
|
||||
let store_builders: Vec<(_, StoreBuilder)> = vec![
|
||||
(
|
||||
"memory",
|
||||
Box::new(|| Box::pin(async { MemoryStore::default().into_event_cache_store() })),
|
||||
),
|
||||
(
|
||||
"SQLite",
|
||||
Box::new(move || {
|
||||
let temp_dir = temp_dir.clone();
|
||||
Box::pin(async move {
|
||||
// Remove all the files in the temp_dir, to reset the event cache state.
|
||||
for entry in temp_dir.path().read_dir().unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
// If it's a directory, remove it recursively.
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
} else {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate a new store.
|
||||
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
})
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
let state_store = runtime.block_on(async {
|
||||
let state_store = matrix_sdk::MemoryStore::new();
|
||||
state_store.save_changes(&changes).await.unwrap();
|
||||
Arc::new(state_store)
|
||||
});
|
||||
|
||||
for (store_name, store_builder) in &store_builders {
|
||||
let client = runtime.block_on(async {
|
||||
let event_cache_store = store_builder().await;
|
||||
|
||||
let client = MockClientBuilder::new(None)
|
||||
.on_builder(|builder| {
|
||||
builder.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store.clone()),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
client
|
||||
});
|
||||
|
||||
// Define a state store with all rooms known in it.
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_rooms));
|
||||
|
||||
// Bench the handling of room updates.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
format!("Event cache room updates[{store_name}]"),
|
||||
format!("room count: {num_rooms}"),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter(
|
||||
// The routine itself.
|
||||
|| {
|
||||
let room_updates = room_updates.clone();
|
||||
let client = client.clone();
|
||||
|
||||
async move {
|
||||
client.event_cache().clear_all_rooms().await.unwrap();
|
||||
|
||||
client
|
||||
.event_cache()
|
||||
.handle_room_updates(room_updates.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn find_event_relations(c: &mut Criterion) {
|
||||
// Number of other events to saturate the DB, but that will not be affected by
|
||||
// the benchmark. A small multiple of this number will be added.
|
||||
// When running locally, run with more events than in Codespeed CI.
|
||||
#[cfg(feature = "codspeed")]
|
||||
const NUM_OTHER_EVENTS: usize = 100;
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
const NUM_OTHER_EVENTS: usize = 1000;
|
||||
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let mut group = c.benchmark_group("Event cache room updates");
|
||||
group.sample_size(10);
|
||||
|
||||
let room_id = room_id!("!room:ben.ch");
|
||||
let other_room_id = room_id!("!other-room:ben.ch");
|
||||
|
||||
// Make the state store aware of the room, so that `client.get_room()` works
|
||||
// with it.
|
||||
let mut changes = matrix_sdk::StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
|
||||
changes.add_room(RoomInfo::new(other_room_id, RoomState::Joined));
|
||||
let state_store = runtime.block_on(async {
|
||||
let state_store = matrix_sdk::MemoryStore::new();
|
||||
state_store.save_changes(&changes).await.unwrap();
|
||||
Arc::new(state_store)
|
||||
});
|
||||
|
||||
for num_related_events in [10, 100, 1000] {
|
||||
// Prefill the event cache store with one event and N related events.
|
||||
let mut room_updates = RoomUpdates::default();
|
||||
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut joined_room_update = JoinedRoomUpdate::default();
|
||||
|
||||
// Add the target event.
|
||||
let target_event_id = event_id!("$target");
|
||||
let target_event =
|
||||
event_factory.text_msg("hello world").event_id(target_event_id).into_event();
|
||||
joined_room_update.timeline.events.push(target_event);
|
||||
|
||||
// Add the numerous edits.
|
||||
for i in 0..num_related_events {
|
||||
let event_id = EventId::parse(format!("$edit{i}")).unwrap();
|
||||
let event = event_factory
|
||||
.text_msg(format!("* edit {i}"))
|
||||
.edit(
|
||||
target_event_id,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!("edit {i}")),
|
||||
)
|
||||
.event_id(&event_id)
|
||||
.into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
// Add other events, in the same room, without a relation.
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event_id = EventId::parse(format!("$msg{i}")).unwrap();
|
||||
let event =
|
||||
event_factory.text_msg(format!("unrelated message {i}")).event_id(&event_id).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
// Add other events, in the same room, related to other events.
|
||||
let other_target_event_id = event_id!("$other_target");
|
||||
let other_target_event =
|
||||
event_factory.text_msg("hello world").event_id(other_target_event_id).into_event();
|
||||
joined_room_update.timeline.events.push(other_target_event);
|
||||
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event_id = EventId::parse(format!("$unrelated{i}")).unwrap();
|
||||
let event =
|
||||
event_factory.reaction(other_target_event_id, "👍").event_id(&event_id).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
room_updates.joined.insert(room_id.to_owned(), joined_room_update);
|
||||
|
||||
// Add other events, in another room.
|
||||
let mut other_joined_room_update = JoinedRoomUpdate::default();
|
||||
let event_factory = event_factory.room(other_room_id);
|
||||
for i in 0..NUM_OTHER_EVENTS {
|
||||
let event_id = EventId::parse(format!("$other_room{i}")).unwrap();
|
||||
let event = event_factory.text_msg(format!("hi {i}")).event_id(&event_id).into();
|
||||
other_joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(other_room_id.to_owned(), other_joined_room_update);
|
||||
|
||||
changes.add_room(RoomInfo::new(room_id, RoomState::Joined));
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let temp_dir = Arc::new(tempdir().unwrap());
|
||||
|
||||
let stores = vec![
|
||||
("memory", MemoryStore::default().into_event_cache_store()),
|
||||
(
|
||||
"SQLite",
|
||||
runtime.block_on(async {
|
||||
SqliteEventCacheStore::open(temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, event_cache_store) in stores {
|
||||
let (client, room_event_cache, _drop_handles) = runtime.block_on(async {
|
||||
let client = MockClientBuilder::new(None)
|
||||
.on_builder(|builder| {
|
||||
builder.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
// Sync the updates before starting the benchmark.
|
||||
let mut update_recv = client.event_cache().subscribe_to_room_generic_updates();
|
||||
|
||||
client.event_cache().handle_room_updates(room_updates.clone()).await.unwrap();
|
||||
|
||||
// Wait for the event cache to notify us of the room updates.
|
||||
let update = update_recv.recv().await.unwrap();
|
||||
assert!(update.room_id == room_id || update.room_id == other_room_id);
|
||||
|
||||
let update = update_recv.recv().await.unwrap();
|
||||
assert!(update.room_id == room_id || update.room_id == other_room_id);
|
||||
|
||||
let room = client.get_room(room_id).unwrap();
|
||||
let room_event_cache = room.event_cache().await.unwrap();
|
||||
|
||||
(client, room_event_cache.0, room_event_cache.1)
|
||||
});
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_related_events));
|
||||
|
||||
for filter in [None, Some(vec![RelationType::Replacement])] {
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
format!("Event cache find_event_relations[{store_name}]"),
|
||||
format!(
|
||||
"{num_related_events} events, {} filter",
|
||||
if filter.is_some() { "edits" } else { "#no" },
|
||||
),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter_batched(
|
||||
// The setup.
|
||||
|| (room_event_cache.clone(), filter.clone()),
|
||||
// The routine itself.
|
||||
|(room_event_cache, filter)| async move {
|
||||
let (target, relations) = room_event_cache
|
||||
.find_event_with_relations(target_event_id, filter)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
|
||||
assert_eq!(relations.len(), num_related_events as usize);
|
||||
},
|
||||
criterion::BatchSize::PerIteration,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(room_event_cache);
|
||||
drop(client);
|
||||
drop(_drop_handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(state_store);
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = Criterion::default();
|
||||
targets = handle_room_updates, find_event_relations,
|
||||
}
|
||||
|
||||
criterion_main!(event_cache);
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
linked_chunk::{lazy_loader, LinkedChunk, LinkedChunkId, Update},
|
||||
SqliteEventCacheStore,
|
||||
linked_chunk::{LinkedChunk, LinkedChunkId, Update, lazy_loader},
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore, DEFAULT_CHUNK_CAPACITY},
|
||||
Event, Gap,
|
||||
store::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, IntoEventCacheStore, MemoryStore},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE};
|
||||
use ruma::{room_id, EventId};
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::{EventId, room_id};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
@@ -20,6 +20,11 @@ enum Operation {
|
||||
PushGapBack(Gap),
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "codspeed"))]
|
||||
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000, 10_000, 100_000];
|
||||
#[cfg(feature = "codspeed")]
|
||||
const NUMBER_OF_EVENTS: &[u64] = &[10, 100, 1000];
|
||||
|
||||
fn writing(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
@@ -32,10 +37,10 @@ fn writing(c: &mut Criterion) {
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("writing");
|
||||
let mut group = c.benchmark_group("Linked chunk writing");
|
||||
group.sample_size(10).measurement_time(Duration::from_secs(30));
|
||||
|
||||
for number_of_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
for &number_of_events in NUMBER_OF_EVENTS {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
@@ -96,7 +101,7 @@ fn writing(c: &mut Criterion) {
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(store_name, number_of_events),
|
||||
BenchmarkId::new(format!("Linked chunk writing [{store_name}]"), number_of_events),
|
||||
&operations,
|
||||
|bencher, operations| {
|
||||
// Bench the routine.
|
||||
@@ -149,10 +154,10 @@ fn reading(c: &mut Criterion) {
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("reading");
|
||||
let mut group = c.benchmark_group("Linked chunk reading");
|
||||
group.sample_size(10);
|
||||
|
||||
for num_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
for &num_events in NUMBER_OF_EVENTS {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
@@ -187,11 +192,14 @@ fn reading(c: &mut Criterion) {
|
||||
|
||||
while events.peek().is_some() {
|
||||
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
|
||||
if events_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
lc.push_items_back(events_chunk);
|
||||
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
|
||||
|
||||
num_gaps += 1;
|
||||
}
|
||||
|
||||
@@ -205,30 +213,47 @@ fn reading(c: &mut Criterion) {
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_function(BenchmarkId::new(store_name, num_events), |bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
// Bench the lazy loader.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(format!("Linked chunk lazy loader[{store_name}]"), num_events),
|
||||
|bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) =
|
||||
store.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
});
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(linked_chunk_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// Bench the metadata loader.
|
||||
group.bench_function(
|
||||
BenchmarkId::new(format!("Linked chunk metadata loader[{store_name}]"), num_events),
|
||||
|bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
let _metadata = store
|
||||
.load_all_chunks_metadata(linked_chunk_id)
|
||||
.await
|
||||
.expect("metadata must load");
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
@@ -240,21 +265,9 @@ fn reading(c: &mut Criterion) {
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = writing, reading,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
ThreadingSupport,
|
||||
BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore, ThreadingSupport,
|
||||
store::StoreConfig,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{MembershipState, RoomMemberEvent},
|
||||
mxc_uri, owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
user_id,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
@@ -84,7 +85,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(50);
|
||||
|
||||
group.bench_function(BenchmarkId::new("receive_members", name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Handle /members request [SQLite]", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
|
||||
});
|
||||
@@ -164,11 +165,11 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
|
||||
let count = PINNED_EVENTS_COUNT;
|
||||
let name = format!("{count} pinned events");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
let mut group = c.benchmark_group("Load pinned events");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
group.bench_function(BenchmarkId::new("Load pinned events [memory]", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
@@ -180,6 +181,8 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.as_clean()
|
||||
.unwrap()
|
||||
.clear_all_linked_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -207,24 +210,9 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = receive_all_members_benchmark, load_pinned_events_benchmark,
|
||||
}
|
||||
criterion_main!(room);
|
||||
|
||||
@@ -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,28 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::MatrixSession, config::StoreConfig, Client, RoomInfo, RoomState,
|
||||
SessionTokens, StateChanges,
|
||||
Client, RoomInfo, RoomState, SessionTokens, StateChanges,
|
||||
authentication::matrix::MatrixSession, config::StoreConfig,
|
||||
};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
use matrix_sdk_base::{SessionMeta, StateStore as _, store::MemoryStore};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use ruma::{device_id, user_id, RoomId};
|
||||
use ruma::{RoomId, device_id, user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
/// Number of joined rooms in the benchmark.
|
||||
const NUM_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
@@ -30,7 +17,7 @@ const NUM_JOINED_ROOMS: usize = 10000;
|
||||
const NUM_STRIPPED_JOINED_ROOMS: usize = 10000;
|
||||
|
||||
pub fn restore_session(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let runtime = Builder::new_multi_thread().enable_time().build().expect("Can't create runtime");
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
@@ -58,13 +45,11 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Client reload");
|
||||
group.throughput(Throughput::Elements(100));
|
||||
|
||||
const NAME: &str = "restore a session";
|
||||
|
||||
// Memory
|
||||
let mem_store = Arc::new(MemoryStore::new());
|
||||
runtime.block_on(mem_store.save_changes(&changes)).expect("initial filling of mem failed");
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", NAME), &mem_store, |b, store| {
|
||||
group.bench_with_input("Restore session [memory store]", &mem_store, |b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
@@ -92,7 +77,7 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(format!("sqlite store {encrypted_suffix}"), NAME),
|
||||
BenchmarkId::new("Restore session [SQLite]", encrypted_suffix),
|
||||
&sqlite_store,
|
||||
|b, store| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
@@ -124,7 +109,7 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = restore_session
|
||||
}
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::timeline::TimelineBuilder;
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineReadReceiptTracking};
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
|
||||
EventId,
|
||||
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
|
||||
owned_user_id,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
@@ -94,16 +94,16 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
room
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("Test");
|
||||
let mut group = c.benchmark_group("Create a timeline");
|
||||
group.throughput(Throughput::Elements(NUM_EVENTS as _));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|
||||
BenchmarkId::new("Create a timeline with initial events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
@@ -117,24 +117,9 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
config = Criterion::default();
|
||||
targets = create_timeline_with_initial_events
|
||||
}
|
||||
criterion_main!(room);
|
||||
|
||||
@@ -23,14 +23,20 @@ path = "uniffi-bindgen.rs"
|
||||
default = ["bundled-sqlite"]
|
||||
bundled-sqlite = ["matrix-sdk-sqlite/bundled"]
|
||||
|
||||
# Enable experimental support for encrypting state events; see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
|
||||
experimental-encrypted-state-events = [
|
||||
"matrix-sdk-crypto/experimental-encrypted-state-events",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures-util.workspace = true
|
||||
hmac = "0.12.1"
|
||||
hmac.workspace = true
|
||||
http.workspace = true
|
||||
matrix-sdk-common = { workspace = true, features = ["uniffi"] }
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
pbkdf2 = "0.12.2"
|
||||
pbkdf2.workspace = true
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -56,17 +62,17 @@ workspace = true
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.43.1"
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
vergen-gitcl = { workspace = true, features = ["build"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2.workspace = true
|
||||
tempfile = "3.8.0"
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
use vergen_gitcl::{Emitter, GitclBuilder};
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
@@ -59,7 +59,8 @@ fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
|
||||
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
|
||||
let git_config = GitclBuilder::default().sha(true).describe(true, false, None).build()?;
|
||||
Emitter::default().add_instructions(&git_config)?.emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use matrix_sdk_crypto::{
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
DecryptionSettings,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
@@ -154,9 +155,13 @@ impl Drop for RehydratedDevice {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RehydratedDevice {
|
||||
pub fn receive_events(&self, events: String) -> Result<(), crate::CryptoStoreError> {
|
||||
pub fn receive_events(
|
||||
&self,
|
||||
events: String,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<(), crate::CryptoStoreError> {
|
||||
let events: Vec<Raw<AnyToDeviceEvent>> = serde_json::from_str(&events)?;
|
||||
self.runtime.block_on(self.inner.receive_events(events))?;
|
||||
self.runtime.block_on(self.inner.receive_events(events, decryption_settings))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -665,6 +665,9 @@ impl From<HistoryVisibility> for RustHistoryVisibility {
|
||||
pub struct EncryptionSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
/// Whether state event encryption is enabled.
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
pub encrypt_state_events: bool,
|
||||
/// How long can the room key be used before it should be rotated. Time in
|
||||
/// seconds.
|
||||
pub rotation_period: u64,
|
||||
@@ -694,6 +697,8 @@ impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
|
||||
RustEncryptionSettings {
|
||||
algorithm: v.algorithm.into(),
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
@@ -910,6 +915,10 @@ impl From<matrix_sdk_crypto::CrossSigningStatus> for CrossSigningStatus {
|
||||
pub struct RoomSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
/// Whether state event encryption is enabled.
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
#[serde(default)]
|
||||
pub encrypt_state_events: bool,
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
pub only_allow_trusted_devices: bool,
|
||||
@@ -920,7 +929,12 @@ impl TryFrom<RustRoomSettings> for RoomSettings {
|
||||
|
||||
fn try_from(value: RustRoomSettings) -> Result<Self, Self::Error> {
|
||||
let algorithm = value.algorithm.try_into()?;
|
||||
Ok(Self { algorithm, only_allow_trusted_devices: value.only_allow_trusted_devices })
|
||||
Ok(Self {
|
||||
algorithm,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: value.encrypt_state_events,
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1173,6 +1187,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
Some(RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
only_allow_trusted_devices: true
|
||||
}),
|
||||
settings1
|
||||
@@ -1182,6 +1198,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
Some(RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
encrypt_state_events: false,
|
||||
only_allow_trusted_devices: false
|
||||
}),
|
||||
settings2
|
||||
|
||||
@@ -18,7 +18,8 @@ use matrix_sdk_crypto::{
|
||||
olm::ExportedRoomKey,
|
||||
store::types::{BackupDecryptionKey, Changes},
|
||||
types::requests::ToDeviceRequest,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
|
||||
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
@@ -38,7 +39,7 @@ use ruma::{
|
||||
},
|
||||
events::{
|
||||
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, MessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
@@ -526,6 +527,7 @@ impl OlmMachine {
|
||||
key_counts: HashMap<String, i32>,
|
||||
unused_fallback_keys: Option<Vec<String>>,
|
||||
next_batch_token: String,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<SyncChangesResult, CryptoStoreError> {
|
||||
let to_device: ToDevice = serde_json::from_str(&events)?;
|
||||
let device_changes: RumaDeviceLists = device_changes.into();
|
||||
@@ -544,15 +546,17 @@ impl OlmMachine {
|
||||
let unused_fallback_keys: Option<Vec<OneTimeKeyAlgorithm>> =
|
||||
unused_fallback_keys.map(|u| u.into_iter().map(OneTimeKeyAlgorithm::from).collect());
|
||||
|
||||
let (to_device_events, room_key_infos) = self.runtime.block_on(
|
||||
self.inner.receive_sync_changes(matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
}),
|
||||
)?;
|
||||
let (to_device_events, room_key_infos) =
|
||||
self.runtime.block_on(self.inner.receive_sync_changes(
|
||||
matrix_sdk_crypto::EncryptionSyncChanges {
|
||||
to_device_events: to_device.events,
|
||||
changed_devices: &device_changes,
|
||||
one_time_keys_counts: &key_counts,
|
||||
unused_fallback_keys: unused_fallback_keys.as_deref(),
|
||||
next_batch_token: Some(next_batch_token),
|
||||
},
|
||||
decryption_settings,
|
||||
))?;
|
||||
|
||||
let to_device_events = to_device_events
|
||||
.into_iter()
|
||||
@@ -829,6 +833,7 @@ impl OlmMachine {
|
||||
device_id: String,
|
||||
event_type: String,
|
||||
content: String,
|
||||
share_strategy: CollectStrategy,
|
||||
) -> Result<Option<Request>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
@@ -837,8 +842,11 @@ impl OlmMachine {
|
||||
let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?;
|
||||
|
||||
if let Some(device) = device {
|
||||
let encrypted_content =
|
||||
self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?;
|
||||
let encrypted_content = self.runtime.block_on(device.encrypt_event_raw(
|
||||
&event_type,
|
||||
&content,
|
||||
share_strategy,
|
||||
))?;
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
user_id.as_ref(),
|
||||
@@ -861,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,
|
||||
@@ -894,7 +911,7 @@ impl OlmMachine {
|
||||
))?;
|
||||
|
||||
if handle_verification_events {
|
||||
if let Ok(e) = decrypted.event.deserialize() {
|
||||
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
|
||||
original_event,
|
||||
@@ -1092,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,
|
||||
|
||||
@@ -27,7 +27,7 @@ use ruma::{
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
assign,
|
||||
events::EventContent,
|
||||
events::MessageLikeEventContent,
|
||||
OwnedTransactionId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -6,6 +6,190 @@ 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
|
||||
or exclude low priority rooms from the room list.
|
||||
([#5508](https://github.com/matrix-org/matrix-rust-sdk/pull/5508))
|
||||
- Add `room_version` and `privileged_creators_role` to `RoomInfo` ([#5449](https://github.com/matrix-org/matrix-rust-sdk/pull/5449)).
|
||||
- The [`unstable-hydra`] feature has been enabled, which enables room v12 changes in the SDK.
|
||||
([#5450](https://github.com/matrix-org/matrix-rust-sdk/pull/5450)).
|
||||
- Add experimental support for
|
||||
[MSC4306](https://github.com/matrix-org/matrix-spec-proposals/pull/4306), with the
|
||||
`Room::fetch_thread_subscription()` and `Room::set_thread_subscription()` methods.
|
||||
([#5442](https://github.com/matrix-org/matrix-rust-sdk/pull/5442))
|
||||
- [**breaking**] [`GalleryUploadParameters::reply`] and [`UploadParameters::reply`] have been both
|
||||
replaced with a new optional `in_reply_to` field, that's a string which will be parsed into an
|
||||
`OwnedEventId` when sending the event. The thread relationship will be automatically filled in,
|
||||
based on the timeline focus.
|
||||
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
|
||||
- [**breaking**] [`Timeline::send_reply()`] now automatically fills in the thread relationship,
|
||||
based on the timeline focus. As a result, it only takes an `OwnedEventId` parameter, instead of
|
||||
the `Reply` type. The proper way to start a thread is now thus to create a threaded-focused
|
||||
timeline, and then use `Timeline::send()`.
|
||||
([5427](https://github.com/matrix-org/matrix-rust-sdk/pull/5427))
|
||||
- Add `HomeserverLoginDetails::supports_sso_login` for legacy SSO support information.
|
||||
This is primarily for Element X to give a dedicated error message in case
|
||||
it connects a homeserver with only this method available.
|
||||
([#5222](https://github.com/matrix-org/matrix-rust-sdk/pull/5222))
|
||||
|
||||
### Breaking changes:
|
||||
|
||||
- The timeline will now always use the send queue to upload medias, so the
|
||||
`UploadParameters::use_send_queue` bool has been removed. Make sure to listen to the send queue's
|
||||
error updates, and to handle send queue restarts.
|
||||
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
|
||||
- Support for the legacy media upload progress has been disabled. Media upload progress is
|
||||
available through the send queue, and can be enabled thanks to
|
||||
`Client::enable_send_queue_upload_progress()`.
|
||||
([#5525](https://github.com/matrix-org/matrix-rust-sdk/pull/5525))
|
||||
- `TimelineDiff` is now exported as a true `uniffi::Enum` instead of the weird `uniffi::Object` hybrid. This matches
|
||||
both `RoomDirectorySearchEntryUpdate` and `RoomListEntriesUpdate` and can be used in the same way.
|
||||
([#5474](https://github.com/matrix-org/matrix-rust-sdk/pull/5474))
|
||||
- The `creator` field of `RoomInfo` has been renamed to `creators` and can now contain a list of
|
||||
user IDs, to reflect that a room can now have several creators, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- The `PowerLevel` type was introduced to represent power levels instead of `i64` to differentiate
|
||||
the infinite power level of creators, as introduced in room version 12. It is used in
|
||||
`suggested_role_for_power_level`, `suggested_power_level_for_role` and `RoomMember`.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- `Client::get_url` now returns a `Vec<u8>` instead of a `String`. It also throws an error when the
|
||||
response isn't status code 200 OK, instead of providing the error in the response body.
|
||||
([#5438](https://github.com/matrix-org/matrix-rust-sdk/pull/5438))
|
||||
- `RoomPreview::info()` doesn't return a result anymore. All unknown join rules are handled in the
|
||||
`JoinRule::Custom` variant.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The `reason` argument of `Room::report_room` is now required, do to a clarification in the spec.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- `PublicRoomJoinRule` has more variants, supporting all the known values from the spec.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The fields of `MediaPreviewConfig` are both optional, allowing to use the type for room account
|
||||
data as well as global account data.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- The `event_id` field of `PredecessorRoom` was removed, due to its removal in the Matrix
|
||||
specification with MSC4291.
|
||||
([#5419](https://github.com/matrix-org/matrix-rust-sdk/pull/5419))
|
||||
- `Client::url_for_oidc` now allows requesting additional scopes for the OAuth2 authorization code grant.
|
||||
([#5395](https://github.com/matrix-org/matrix-rust-sdk/pull/5395))
|
||||
- `Client::url_for_oidc` now allows passing an optional existing device id from a previous login call.
|
||||
([#5394](https://github.com/matrix-org/matrix-rust-sdk/pull/5394))
|
||||
- `ClientBuilder::build_with_qr_code` has been removed. Instead, the Client should be built by passing
|
||||
`QrCodeData::server_name` to `ClientBuilder::server_name_or_homeserver_url`, after which QR login can be performed by
|
||||
calling `Client::login_with_qr_code`. ([#5388](https://github.com/matrix-org/matrix-rust-sdk/pull/5388))
|
||||
- The MSRV has been bumped to Rust 1.88.
|
||||
([#5431](https://github.com/matrix-org/matrix-rust-sdk/pull/5431))
|
||||
- `Room::send_call_notification` and `Room::send_call_notification_if_needed` have been removed, since the event type they send is outdated, and `Client` is not actually supposed to be able to join MatrixRTC sessions (yet). In practice, users of these methods probably already rely on another MatrixRTC implementation to participate in sessions, and such an implementation should be capable of sending notifications itself.
|
||||
- The `GalleryItemInfo` variants now take an `UploadSource` rather than a `String` path to enable uploading
|
||||
from bytes directly.
|
||||
([#5529](https://github.com/matrix-org/matrix-rust-sdk/pull/5529))
|
||||
- Media and gallery uploads now use `UploadSource` to specify the thumbnail.
|
||||
([#5530](https://github.com/matrix-org/matrix-rust-sdk/pull/5530))
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
@@ -72,7 +256,8 @@ Additions:
|
||||
we can automatically update the UI.
|
||||
- `Client::get_max_media_upload_size` to get the max size of a request sent to the homeserver so we can tweak our media
|
||||
uploads by compressing/transcoding the media.
|
||||
- Add `ClientBuilder::enable_share_history_on_invite` to enable experimental support for sharing encrypted room history on invite, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
|
||||
- Add `ClientBuilder::enable_share_history_on_invite` to enable experimental support for sharing encrypted room history
|
||||
on invite, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
|
||||
([#5141](https://github.com/matrix-org/matrix-rust-sdk/pull/5141))
|
||||
- Support for adding a Sentry layer to the FFI bindings has been added. Only `tracing` statements with
|
||||
the field `sentry=true` will be forwarded to Sentry, in addition to default Sentry filters.
|
||||
@@ -165,7 +350,8 @@ Breaking changes:
|
||||
- The `dynamic_registrations_file` field of `OidcConfiguration` was removed.
|
||||
Clients are supposed to re-register with the homeserver for every login.
|
||||
|
||||
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an `Option<RoomMemberWithSenderInfo>`.
|
||||
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an
|
||||
`Option<RoomMemberWithSenderInfo>`.
|
||||
|
||||
Additions:
|
||||
|
||||
@@ -180,9 +366,11 @@ Additions:
|
||||
- Add `Timeline::send_thread_reply` for clients that need to start threads
|
||||
themselves.
|
||||
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
|
||||
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the stores configuration, especially their memory consumption
|
||||
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the
|
||||
stores configuration, especially their memory consumption
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
- Add `ClientBuilder::system_is_memory_constrained` to indicate that the system
|
||||
has less memory available than the current standard
|
||||
([#4894](https://github.com/matrix-org/matrix-rust-sdk/pull/4894))
|
||||
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member` event the `RoomMember` is based on.
|
||||
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member`
|
||||
event the `RoomMember` is based on.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.13.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,32 +41,34 @@ 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
|
||||
as_variant.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-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
|
||||
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sentry = { version = "0.36.0", optional = true, default-features = false, features = [
|
||||
sentry = { workspace = true, optional = true, default-features = false, features = [
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
@@ -69,20 +76,22 @@ sentry = { version = "0.36.0", optional = true, default-features = false, featur
|
||||
"reqwest",
|
||||
"sentry-debug-images",
|
||||
] }
|
||||
sentry-tracing = { version = "0.36.0", optional = true }
|
||||
sentry-tracing = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tracing-appender.workspace = true
|
||||
tracing-core.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
url.workspace = true
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
zeroize.workspace = true
|
||||
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,11 +99,14 @@ 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
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
vergen-gitcl = { workspace = true, features = ["build"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
use vergen_gitcl::{Emitter, GitclBuilder};
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
@@ -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,7 +75,11 @@ 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");
|
||||
EmitBuilder::builder().git_sha(true).emit()?;
|
||||
|
||||
let git_config = GitclBuilder::default().sha(true).build()?;
|
||||
Emitter::default().add_instructions(&git_config)?.emit()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct HomeserverLoginDetails {
|
||||
pub(crate) sliding_sync_version: SlidingSyncVersion,
|
||||
pub(crate) supports_oidc_login: bool,
|
||||
pub(crate) supported_oidc_prompts: Vec<OidcPrompt>,
|
||||
pub(crate) supports_sso_login: bool,
|
||||
pub(crate) supports_password_login: bool,
|
||||
}
|
||||
|
||||
@@ -43,6 +44,11 @@ impl HomeserverLoginDetails {
|
||||
self.supports_oidc_login
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using legacy SSO.
|
||||
pub fn supports_sso_login(&self) -> bool {
|
||||
self.supports_sso_login
|
||||
}
|
||||
|
||||
/// The prompts advertised by the authentication issuer for use in the login
|
||||
/// URL.
|
||||
pub fn supported_oidc_prompts(&self) -> Vec<OidcPrompt> {
|
||||
|
||||
@@ -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,
|
||||
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::{
|
||||
@@ -48,11 +49,18 @@ use matrix_sdk_ui::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
NotificationProcessSetup as MatrixNotificationProcessSetup,
|
||||
},
|
||||
spaces::SpaceService as UISpaceService,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use mime::Mime;
|
||||
use oauth2::Scope;
|
||||
use ruma::{
|
||||
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
|
||||
api::client::{
|
||||
alias::get_alias,
|
||||
error::ErrorKind,
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
events::{
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
@@ -66,19 +74,20 @@ 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,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
room_version_rules::AuthorizationRules,
|
||||
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
@@ -94,9 +103,13 @@ use crate::{
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClient,
|
||||
notification::{
|
||||
NotificationClient, NotificationEvent, NotificationItem, NotificationRoomInfo,
|
||||
NotificationSenderInfo,
|
||||
},
|
||||
notification_settings::NotificationSettings,
|
||||
room::{RoomHistoryVisibility, RoomInfoListener},
|
||||
qr_code::{GrantLoginWithQrCodeHandler, LoginWithQrCodeHandler},
|
||||
room::{RoomHistoryVisibility, RoomInfoListener, RoomSendQueueUpdate},
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{
|
||||
@@ -104,6 +117,7 @@ use crate::{
|
||||
MediaPreviews, MediaSource, RoomAccountDataEvent, RoomAccountDataEventType,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
spaces::SpaceService,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
utd::{UnableToDecryptDelegate, UtdHook},
|
||||
@@ -187,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 {
|
||||
@@ -209,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,
|
||||
@@ -227,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>,
|
||||
}
|
||||
|
||||
@@ -323,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();
|
||||
@@ -339,7 +386,24 @@ impl Client {
|
||||
}
|
||||
};
|
||||
|
||||
let supports_password_login = self.supports_password_login().await.ok().unwrap_or(false);
|
||||
let login_types = self.inner.matrix_auth().get_login_types().await.ok();
|
||||
let supports_password_login = login_types
|
||||
.as_ref()
|
||||
.map(|login_types| {
|
||||
login_types.flows.iter().any(|login_type| {
|
||||
matches!(login_type, get_login_types::v3::LoginType::Password(_))
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let supports_sso_login = login_types
|
||||
.as_ref()
|
||||
.map(|login_types| {
|
||||
login_types
|
||||
.flows
|
||||
.iter()
|
||||
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Sso(_)))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let sliding_sync_version = self.sliding_sync_version();
|
||||
|
||||
Arc::new(HomeserverLoginDetails {
|
||||
@@ -347,6 +411,7 @@ impl Client {
|
||||
sliding_sync_version,
|
||||
supports_oidc_login,
|
||||
supported_oidc_prompts,
|
||||
supports_sso_login,
|
||||
supports_password_login,
|
||||
})
|
||||
}
|
||||
@@ -456,16 +521,39 @@ impl Client {
|
||||
/// However, it should be noted that when providing a user ID as a hint
|
||||
/// for MAS (with no upstream provider), then the format to use is defined
|
||||
/// by [MSC4198]: https://github.com/matrix-org/matrix-spec-proposals/pull/4198
|
||||
///
|
||||
/// * `device_id` - The unique ID that will be associated with the session.
|
||||
/// If not set, a random one will be generated. It can be an existing
|
||||
/// device ID from a previous login call. Note that this should be done
|
||||
/// only if the client also holds the corresponding encryption keys.
|
||||
///
|
||||
/// * `additional_scopes` - Additional scopes to request from the
|
||||
/// authorization server, e.g. "urn:matrix:client:com.example.msc9999.foo".
|
||||
/// The scopes for API access and the device ID according to the
|
||||
/// [specification](https://spec.matrix.org/v1.15/client-server-api/#allocated-scope-tokens)
|
||||
/// are always requested.
|
||||
pub async fn url_for_oidc(
|
||||
&self,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
prompt: Option<OidcPrompt>,
|
||||
login_hint: Option<String>,
|
||||
device_id: Option<String>,
|
||||
additional_scopes: Option<Vec<String>>,
|
||||
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
|
||||
let registration_data = oidc_configuration.registration_data()?;
|
||||
let redirect_uri = oidc_configuration.redirect_uri()?;
|
||||
|
||||
let mut url_builder = self.inner.oauth().login(redirect_uri, None, Some(registration_data));
|
||||
let device_id = device_id.map(OwnedDeviceId::from);
|
||||
|
||||
let additional_scopes =
|
||||
additional_scopes.map(|scopes| scopes.into_iter().map(Scope::new).collect::<Vec<_>>());
|
||||
|
||||
let mut url_builder = self.inner.oauth().login(
|
||||
redirect_uri,
|
||||
device_id,
|
||||
Some(registration_data),
|
||||
additional_scopes,
|
||||
);
|
||||
|
||||
if let Some(prompt) = prompt {
|
||||
url_builder = url_builder.prompt(vec![prompt.into()]);
|
||||
@@ -494,6 +582,26 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a handler for requesting an existing device to grant login to
|
||||
/// this device by way of a QR code.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `oidc_configuration` - The data to restore or register the client with
|
||||
/// the server.
|
||||
pub fn new_login_with_qr_code_handler(
|
||||
self: Arc<Self>,
|
||||
oidc_configuration: OidcConfiguration,
|
||||
) -> LoginWithQrCodeHandler {
|
||||
LoginWithQrCodeHandler::new(self.inner.oauth(), oidc_configuration)
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
///
|
||||
/// It reloads the entire set of rooms from the previous session.
|
||||
@@ -539,6 +647,55 @@ impl Client {
|
||||
self.inner.send_queue().set_enabled(enable).await;
|
||||
}
|
||||
|
||||
/// Enables or disables progress reporting for media uploads in the send
|
||||
/// queue.
|
||||
pub fn enable_send_queue_upload_progress(&self, enable: bool) {
|
||||
self.inner.send_queue().enable_upload_progress(enable);
|
||||
}
|
||||
|
||||
/// Subscribe to the global 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.
|
||||
///
|
||||
@@ -694,24 +851,192 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows generic GET requests to be made through the SDKs internal HTTP
|
||||
/// client
|
||||
pub async fn get_url(&self, url: String) -> Result<String, ClientError> {
|
||||
let http_client = self.inner.http_client();
|
||||
Ok(http_client.get(url).send().await?.text().await?)
|
||||
/// 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
|
||||
/// method returns the raw bytes of the response, so that any kind of
|
||||
/// resource can be fetched including images, files, etc.
|
||||
///
|
||||
/// Note: When an HTTP error occurs, the error response can be found in the
|
||||
/// `ClientError::Generic`'s `details` field.
|
||||
pub async fn get_url(&self, url: String) -> Result<Vec<u8>, ClientError> {
|
||||
let response = self.inner.http_client().get(url).send().await?;
|
||||
if response.status().is_success() {
|
||||
Ok(response.bytes().await?.into())
|
||||
} else {
|
||||
Err(ClientError::Generic {
|
||||
msg: response.status().to_string(),
|
||||
details: response.text().await.ok(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty the server version and unstable features cache.
|
||||
///
|
||||
/// Since the SDK caches server 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
|
||||
@@ -725,34 +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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Whether or not the client's homeserver supports the password login flow.
|
||||
pub(crate) async fn supports_password_login(&self) -> anyhow::Result<bool> {
|
||||
let login_types = self.inner.matrix_auth().get_login_types().await?;
|
||||
let supports_password = login_types
|
||||
.flows
|
||||
.iter()
|
||||
.any(|login_type| matches!(login_type, get_login_types::v3::LoginType::Password(_)));
|
||||
Ok(supports_password)
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,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?;
|
||||
@@ -1144,15 +1486,8 @@ impl Client {
|
||||
}
|
||||
|
||||
pub async fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
|
||||
let owned_user_id = UserId::parse(user_id.clone())?;
|
||||
|
||||
let response = self.inner.account().fetch_user_profile_of(&owned_user_id).await?;
|
||||
|
||||
Ok(UserProfile {
|
||||
user_id,
|
||||
display_name: response.displayname.clone(),
|
||||
avatar_url: response.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
})
|
||||
let user_id = <&UserId>::try_from(user_id.as_str())?;
|
||||
UserProfile::fetch(&self.inner.account(), user_id).await
|
||||
}
|
||||
|
||||
pub async fn notification_client(
|
||||
@@ -1170,6 +1505,11 @@ impl Client {
|
||||
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
|
||||
}
|
||||
|
||||
pub fn space_service(&self) -> Arc<SpaceService> {
|
||||
let inner = UISpaceService::new((*self.inner).clone());
|
||||
Arc::new(SpaceService::new(inner))
|
||||
}
|
||||
|
||||
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
let inner = self.inner.notification_settings().await;
|
||||
|
||||
@@ -1183,13 +1523,10 @@ impl Client {
|
||||
// Ignored users
|
||||
|
||||
pub async fn ignored_users(&self) -> Result<Vec<String>, ClientError> {
|
||||
if let Some(raw_content) = self
|
||||
.inner
|
||||
.account()
|
||||
.fetch_account_data(RumaGlobalAccountDataEventType::IgnoredUserList)
|
||||
.await?
|
||||
if let Some(raw_content) =
|
||||
self.inner.account().fetch_account_data_static::<IgnoredUserListEventContent>().await?
|
||||
{
|
||||
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
|
||||
let content = raw_content.deserialize()?;
|
||||
let user_ids: Vec<String> =
|
||||
content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
@@ -1419,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?)
|
||||
};
|
||||
|
||||
@@ -1468,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
|
||||
@@ -1483,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());
|
||||
|
||||
@@ -1532,6 +1870,14 @@ impl Client {
|
||||
.any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
|
||||
}
|
||||
|
||||
/// Get server vendor information from the federation API.
|
||||
///
|
||||
/// This method retrieves information about the server's name and version
|
||||
/// by calling the `/_matrix/federation/v1/version` endpoint.
|
||||
pub async fn server_vendor_info(&self) -> Result<matrix_sdk::ServerVendorInfo, ClientError> {
|
||||
Ok(self.inner.server_vendor_info(None).await?)
|
||||
}
|
||||
|
||||
/// Subscribe to changes in the media preview configuration.
|
||||
pub async fn subscribe_to_media_preview_config(
|
||||
&self,
|
||||
@@ -1565,7 +1911,7 @@ impl Client {
|
||||
) -> Result<Option<MediaPreviews>, ClientError> {
|
||||
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
|
||||
match configuration {
|
||||
Some(configuration) => Ok(Some(configuration.media_previews.into())),
|
||||
Some(configuration) => Ok(configuration.media_previews.map(Into::into)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -1586,7 +1932,7 @@ impl Client {
|
||||
) -> Result<Option<InviteAvatars>, ClientError> {
|
||||
let configuration = self.inner.account().get_media_preview_config_event_content().await?;
|
||||
match configuration {
|
||||
Some(configuration) => Ok(Some(configuration.invite_avatars.into())),
|
||||
Some(configuration) => Ok(configuration.invite_avatars.map(Into::into)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -1649,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>);
|
||||
@@ -1718,6 +2100,18 @@ pub struct UserProfile {
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
impl UserProfile {
|
||||
/// Fetch the profile for the given user ID, using the given [`Account`]
|
||||
/// API.
|
||||
pub(crate) async fn fetch(account: &Account, user_id: &UserId) -> Result<Self, ClientError> {
|
||||
let response = account.fetch_user_profile_of(user_id).await?;
|
||||
let display_name = response.get_static::<DisplayName>()?;
|
||||
let avatar_url = response.get_static::<AvatarUrl>()?.map(|url| url.to_string());
|
||||
|
||||
Ok(UserProfile { user_id: user_id.to_string(), display_name, avatar_url })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&search_users::v3::User> for UserProfile {
|
||||
fn from(value: &search_users::v3::User) -> Self {
|
||||
UserProfile {
|
||||
@@ -1832,7 +2226,7 @@ pub struct PowerLevels {
|
||||
|
||||
impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
fn from(value: PowerLevels) -> Self {
|
||||
let mut power_levels = RoomPowerLevelsEventContent::new();
|
||||
let mut power_levels = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
|
||||
|
||||
if let Some(users_default) = value.users_default {
|
||||
power_levels.users_default = users_default.into();
|
||||
@@ -1937,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;
|
||||
@@ -2196,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()
|
||||
@@ -2223,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2402,9 +2809,7 @@ impl TryFrom<AllowRule> for RumaAllowRule {
|
||||
match value {
|
||||
AllowRule::RoomMembership { room_id } => {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
Ok(Self::RoomMembership(ruma::events::room::join_rules::RoomMembership::new(
|
||||
room_id,
|
||||
)))
|
||||
Ok(Self::RoomMembership(ruma::room::RoomMembership::new(room_id)))
|
||||
}
|
||||
AllowRule::Custom { json } => Ok(Self::_Custom(Box::new(serde_json::from_str(&json)?))),
|
||||
}
|
||||
@@ -2457,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,12 +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};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
crypto::{
|
||||
types::qr_login::QrCodeModeData, CollectStrategy, DecryptionSettings, TrustRequirement,
|
||||
},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
ruma::{ServerName, UserId},
|
||||
@@ -15,21 +14,20 @@ 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;
|
||||
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
|
||||
use crate::store;
|
||||
use crate::{
|
||||
authentication::OidcConfiguration,
|
||||
client::ClientSessionDelegate,
|
||||
error::ClientError,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
|
||||
runtime::get_runtime_handle,
|
||||
task_handle::TaskHandle,
|
||||
store::{StoreBuilder, StoreBuilderOutcome},
|
||||
};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
@@ -111,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>,
|
||||
@@ -141,31 +135,37 @@ pub struct ClientBuilder {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
|
||||
threads_enabled: bool,
|
||||
threading_support: ThreadingSupport,
|
||||
}
|
||||
|
||||
/// The timeout applies to each read operation, and resets after a successful
|
||||
/// read. This is more appropriate for detecting stalled connections when the
|
||||
/// size isn’t known beforehand.
|
||||
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
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,
|
||||
@@ -179,7 +179,7 @@ impl ClientBuilder {
|
||||
},
|
||||
enable_share_history_on_invite: false,
|
||||
request_config: Default::default(),
|
||||
threads_enabled: false,
|
||||
threading_support: ThreadingSupport::Disabled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -207,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;
|
||||
@@ -393,9 +326,27 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn threads_enabled(self: Arc<Self>, enabled: bool) -> Arc<Self> {
|
||||
/// Whether the client should support threads client-side or not, and enable
|
||||
/// experimental support for MSC4306 (threads subscriptions) or not.
|
||||
pub fn threads_enabled(
|
||||
self: Arc<Self>,
|
||||
enabled: bool,
|
||||
thread_subscriptions: bool,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.threads_enabled = enabled;
|
||||
let support = if enabled {
|
||||
ThreadingSupport::Enabled { with_subscriptions: thread_subscriptions }
|
||||
} else {
|
||||
ThreadingSupport::Disabled
|
||||
};
|
||||
builder.threading_support = support;
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -408,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
|
||||
};
|
||||
|
||||
@@ -550,6 +479,7 @@ impl ClientBuilder {
|
||||
if let Some(timeout) = config.timeout {
|
||||
updated_config = updated_config.timeout(Duration::from_millis(timeout));
|
||||
}
|
||||
updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
|
||||
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
|
||||
if max_concurrent_requests > 0 {
|
||||
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
|
||||
@@ -564,11 +494,7 @@ impl ClientBuilder {
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.with_threading_support(if builder.threads_enabled {
|
||||
ThreadingSupport::Enabled
|
||||
} else {
|
||||
ThreadingSupport::Disabled
|
||||
});
|
||||
inner_builder = inner_builder.with_threading_support(builder.threading_support);
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
@@ -582,74 +508,64 @@ impl ClientBuilder {
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the building of the client and attempt to log in using the
|
||||
/// provided [`QrCodeData`].
|
||||
#[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.
|
||||
///
|
||||
/// This method will build the client and immediately attempt to log the
|
||||
/// client in using the provided [`QrCodeData`] using the login
|
||||
/// mechanism described in [MSC4108]. As such this methods requires OAuth
|
||||
/// 2.0 support as well as sliding sync support.
|
||||
///
|
||||
/// The usage of the progress_listener is required to transfer the
|
||||
/// [`CheckCode`] to the existing client.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn build_with_qr_code(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<Arc<Client>, HumanQrLoginError> {
|
||||
let QrCodeModeData::Reciprocate { server_name } = &qr_code_data.inner.mode_data else {
|
||||
return Err(HumanQrLoginError::OtherDeviceNotSignedIn);
|
||||
};
|
||||
|
||||
let builder = self.server_name_or_homeserver_url(server_name.to_owned());
|
||||
|
||||
let client = builder.build().await.map_err(|e| match e {
|
||||
ClientBuildError::SlidingSync(_) => HumanQrLoginError::SlidingSyncNotAvailable,
|
||||
_ => {
|
||||
error!("Couldn't build the client {e:?}");
|
||||
HumanQrLoginError::Unknown
|
||||
}
|
||||
})?;
|
||||
|
||||
let registration_data = oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let oauth = client.inner.oauth();
|
||||
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data));
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(client)
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
#[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)
|
||||
}
|
||||
|
||||
@@ -658,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)
|
||||
}
|
||||
@@ -668,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 {
|
||||
|
||||
@@ -25,6 +25,8 @@ mod room_preview;
|
||||
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,12 +168,13 @@ 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() }
|
||||
}
|
||||
SdkPushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
Self::SenderNotificationPermission { key: key.to_string() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.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 {
|
||||
@@ -197,7 +200,7 @@ impl From<PushCondition> for SdkPushCondition {
|
||||
},
|
||||
},
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
Self::SenderNotificationPermission { key: key.into() }
|
||||
}
|
||||
PushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
@@ -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
|
||||
@@ -271,9 +275,11 @@ enum LogTarget {
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkBaseStoreAmbiguityMap,
|
||||
MatrixSdkBaseResponseProcessors,
|
||||
|
||||
// SDK common modules.
|
||||
MatrixSdkCommonStoreLocks,
|
||||
MatrixSdkCommonCrossProcessLock,
|
||||
MatrixSdkCommonDeserializedResponses,
|
||||
|
||||
// SDK modules.
|
||||
MatrixSdk,
|
||||
@@ -300,7 +306,11 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
|
||||
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
|
||||
LogTarget::MatrixSdkBaseResponseProcessors => "matrix_sdk_base::response_processors",
|
||||
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",
|
||||
@@ -333,17 +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
|
||||
@@ -358,6 +370,8 @@ pub enum TraceLogPacks {
|
||||
Timeline,
|
||||
/// Enables all the logs relevant to the notification client.
|
||||
NotificationClient,
|
||||
/// Enables all the logs relevant to sync profiling.
|
||||
SyncProfiling,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
@@ -369,10 +383,22 @@ 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,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,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()
|
||||
@@ -477,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
|
||||
@@ -675,6 +711,8 @@ fn setup_lightweight_tokio_runtime() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::build_tracing_filter;
|
||||
use crate::platform::TraceLogPacks;
|
||||
|
||||
@@ -710,9 +748,11 @@ 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,
|
||||
super_duper_app=error"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
@@ -753,9 +793,11 @@ 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,
|
||||
super_duper_app=trace,
|
||||
some_other_span=trace"#
|
||||
.split('\n')
|
||||
@@ -797,9 +839,11 @@ 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,
|
||||
super_duper_app=info"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
|
||||
@@ -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, Mentions, NotifyType},
|
||||
ruma::{
|
||||
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
EventTimelineItem, 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 => {
|
||||
@@ -304,6 +307,10 @@ impl Room {
|
||||
self.inner.latest_event_item().await.map(Into::into)
|
||||
}
|
||||
|
||||
async fn new_latest_event(&self) -> LatestEventValue {
|
||||
self.inner.new_latest_event().await.into()
|
||||
}
|
||||
|
||||
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
|
||||
Ok(self.inner.latest_encryption_state().await?)
|
||||
}
|
||||
@@ -484,7 +491,7 @@ impl Room {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the room is not found or on rate limit
|
||||
pub async fn report_room(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
pub async fn report_room(&self, reason: String) -> Result<(), ClientError> {
|
||||
self.inner.report_room(reason).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -666,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())))
|
||||
@@ -720,53 +746,6 @@ impl Room {
|
||||
Ok(self.inner.matrix_to_event_permalink(event_id).await?.to_string())
|
||||
}
|
||||
|
||||
/// This will only send a call notification event if appropriate.
|
||||
///
|
||||
/// This function is supposed to be called whenever the user creates a room
|
||||
/// call. It will send a `m.call.notify` event if:
|
||||
/// - there is not yet a running call.
|
||||
///
|
||||
/// It will configure the notify type: ring or notify based on:
|
||||
/// - is this a DM room -> ring
|
||||
/// - is this a group with more than one other member -> notify
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(true)` if the event was successfully sent.
|
||||
/// - `Ok(false)` if we didn't send it because it was unnecessary.
|
||||
/// - `Err(_)` if sending the event failed.
|
||||
pub async fn send_call_notification_if_needed(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.send_call_notification_if_needed().await?)
|
||||
}
|
||||
|
||||
/// Send a call notification event in the current room.
|
||||
///
|
||||
/// This is only supposed to be used in **custom** situations where the user
|
||||
/// explicitly chooses to send a `m.call.notify` event to invite/notify
|
||||
/// someone explicitly in unusual conditions. The default should be to
|
||||
/// use `send_call_notification_if_necessary` just before a new room call is
|
||||
/// created/joined.
|
||||
///
|
||||
/// One example could be that the UI allows to start a call with a subset of
|
||||
/// users of the room members first. And then later on the user can
|
||||
/// invite more users to the call.
|
||||
pub async fn send_call_notification(
|
||||
&self,
|
||||
call_id: String,
|
||||
application: RtcApplicationType,
|
||||
notify_type: NotifyType,
|
||||
mentions: Mentions,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_call_notification(
|
||||
call_id,
|
||||
application.into(),
|
||||
notify_type.into(),
|
||||
mentions.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether the send queue for that particular room is enabled or
|
||||
/// not.
|
||||
pub fn is_send_queue_enabled(&self) -> bool {
|
||||
@@ -778,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(
|
||||
@@ -1053,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.
|
||||
///
|
||||
@@ -1139,6 +1187,75 @@ impl Room {
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
}
|
||||
|
||||
/// Set a MSC4306 subscription to a thread in this room, based on the thread
|
||||
/// root event id.
|
||||
///
|
||||
/// If `subscribed` is `true`, it will subscribe to the thread, with a
|
||||
/// precision that the subscription was manually requested by the user
|
||||
/// (i.e. not automatic).
|
||||
///
|
||||
/// If the thread was already subscribed to (resp. unsubscribed from), while
|
||||
/// trying to subscribe to it (resp. unsubscribe from it), it will do
|
||||
/// nothing, i.e. subscribing (resp. unsubscribing) to a thread is an
|
||||
/// idempotent operation.
|
||||
pub async fn set_thread_subscription(
|
||||
&self,
|
||||
thread_root_event_id: String,
|
||||
subscribed: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
let thread_root = EventId::parse(thread_root_event_id)?;
|
||||
if subscribed {
|
||||
// This is a manual subscription.
|
||||
let automatic = None;
|
||||
self.inner.subscribe_thread(thread_root, automatic).await?;
|
||||
} else {
|
||||
self.inner.unsubscribe_thread(thread_root).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the current MSC4306 thread subscription for the given thread root
|
||||
/// in this room.
|
||||
///
|
||||
/// Returns `None` if the thread doesn't exist, or isn't subscribed to, or
|
||||
/// the server can't handle MSC4306; otherwise, returns the thread
|
||||
/// subscription status.
|
||||
pub async fn fetch_thread_subscription(
|
||||
&self,
|
||||
thread_root_event_id: String,
|
||||
) -> Result<Option<ThreadSubscription>, ClientError> {
|
||||
let thread_root = EventId::parse(thread_root_event_id)?;
|
||||
Ok(self
|
||||
.inner
|
||||
.fetch_thread_subscription(thread_root)
|
||||
.await?
|
||||
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ThreadSubscription {
|
||||
/// Whether the thread subscription happened automatically (e.g. after a
|
||||
/// mention) or if it was manually requested by the user.
|
||||
automatic: bool,
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
@@ -1147,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 {
|
||||
@@ -1311,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
|
||||
@@ -1330,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 {
|
||||
@@ -1352,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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1504,13 +1851,133 @@ impl From<SdkSuccessorRoom> for SuccessorRoom {
|
||||
pub struct PredecessorRoom {
|
||||
/// The ID of the replacement room.
|
||||
pub room_id: String,
|
||||
|
||||
/// The event ID of the last known event in the predecesssor room.
|
||||
pub last_event_id: String,
|
||||
}
|
||||
|
||||
impl From<SdkPredecessorRoom> for PredecessorRoom {
|
||||
fn from(value: SdkPredecessorRoom) -> Self {
|
||||
Self { room_id: value.room_id.to_string(), last_event_id: value.last_event_id.to_string() }
|
||||
Self { room_id: value.room_id.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
encryption_state: EncryptionState,
|
||||
creator: Option<String>,
|
||||
creators: Option<Vec<String>>,
|
||||
/// The room's name from the room state event if received from sync, or one
|
||||
/// that's been computed otherwise.
|
||||
display_name: Option<String>,
|
||||
@@ -74,6 +74,11 @@ pub struct RoomInfo {
|
||||
///
|
||||
/// Can be missing if the room power levels event is missing from the store.
|
||||
power_levels: Option<Arc<RoomPowerLevels>>,
|
||||
/// This room's version.
|
||||
room_version: Option<String>,
|
||||
/// Whether creators are privileged over every other user (have infinite
|
||||
/// power level).
|
||||
privileged_creators_role: bool,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
@@ -102,7 +107,9 @@ impl RoomInfo {
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
encryption_state: room.encryption_state(),
|
||||
creator: room.creator().as_ref().map(ToString::to_string),
|
||||
creators: room
|
||||
.creators()
|
||||
.map(|creators| creators.into_iter().map(Into::into).collect()),
|
||||
display_name: room.cached_display_name().map(|name| name.to_string()),
|
||||
raw_name: room.name(),
|
||||
topic: room.topic(),
|
||||
@@ -150,6 +157,12 @@ impl RoomInfo {
|
||||
join_rule,
|
||||
history_visibility: room.history_visibility_or_default().try_into()?,
|
||||
power_levels: power_levels.map(Arc::new),
|
||||
room_version: room.version().map(|version| version.to_string()),
|
||||
privileged_creators_role: room
|
||||
.version()
|
||||
.and_then(|version| version.rules())
|
||||
.map(|rules| rules.authorization.explicitly_privilege_room_creators)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,15 +28,21 @@ use crate::{error::ClientError, runtime::get_runtime_handle, task_handle::TaskHa
|
||||
pub enum PublicRoomJoinRule {
|
||||
Public,
|
||||
Knock,
|
||||
Restricted,
|
||||
KnockRestricted,
|
||||
Invite,
|
||||
}
|
||||
|
||||
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
|
||||
impl TryFrom<ruma::room::JoinRuleKind> for PublicRoomJoinRule {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: ruma::room::JoinRuleKind) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
|
||||
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
|
||||
ruma::room::JoinRuleKind::Public => Ok(Self::Public),
|
||||
ruma::room::JoinRuleKind::Knock => Ok(Self::Knock),
|
||||
ruma::room::JoinRuleKind::Restricted => Ok(Self::Restricted),
|
||||
ruma::room::JoinRuleKind::KnockRestricted => Ok(Self::KnockRestricted),
|
||||
ruma::room::JoinRuleKind::Invite => Ok(Self::Invite),
|
||||
rule => Err(format!("unsupported join rule: {rule:?}")),
|
||||
}
|
||||
}
|
||||
@@ -149,11 +155,6 @@ impl RoomDirectorySearch {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDirectorySearchEntriesResult {
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomDirectorySearchEntryUpdate {
|
||||
Append { values: Vec<RoomDescription> },
|
||||
|
||||
@@ -16,8 +16,9 @@ use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
|
||||
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_invite,
|
||||
new_filter_joined, new_filter_non_left, new_filter_none,
|
||||
new_filter_normalized_match_room_name, new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
new_filter_joined, new_filter_low_priority, new_filter_non_left, new_filter_none,
|
||||
new_filter_normalized_match_room_name, new_filter_not, new_filter_space, new_filter_unread,
|
||||
BoxedFilterFn, RoomCategory,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
@@ -167,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;
|
||||
|
||||
@@ -215,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.
|
||||
@@ -230,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(),
|
||||
);
|
||||
}
|
||||
@@ -454,10 +472,16 @@ impl RoomListDynamicEntriesController {
|
||||
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
|
||||
Joined,
|
||||
Unread,
|
||||
Favourite,
|
||||
LowPriority,
|
||||
NonLowPriority,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
None,
|
||||
@@ -492,10 +516,14 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
Kind::Any { filters } => Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
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()),
|
||||
Kind::LowPriority => Box::new(new_filter_low_priority()),
|
||||
Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
|
||||
Kind::Invite => Box::new(new_filter_invite()),
|
||||
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
|
||||
Kind::None => Box::new(new_filter_none()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::UserId;
|
||||
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
|
||||
|
||||
use crate::error::{ClientError, NotYetImplemented};
|
||||
|
||||
@@ -57,16 +57,25 @@ impl TryFrom<matrix_sdk::ruma::events::room::member::MembershipState> for Member
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the suggested role for the given power level.
|
||||
///
|
||||
/// Returns an error if the value of the power level is out of range for numbers
|
||||
/// accepted in canonical JSON.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
|
||||
pub fn suggested_role_for_power_level(
|
||||
power_level: PowerLevel,
|
||||
) -> Result<RoomMemberRole, ClientError> {
|
||||
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
|
||||
RoomMemberRole::suggested_role_for_power_level(power_level)
|
||||
Ok(RoomMemberRole::suggested_role_for_power_level(power_level.try_into()?))
|
||||
}
|
||||
|
||||
/// Get the suggested power level for the given role.
|
||||
///
|
||||
/// Returns an error if the value of the power level is unsupported.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> Result<PowerLevel, ClientError> {
|
||||
// It's not possible to expose methods on an Enum through Uniffi ☹️
|
||||
role.suggested_power_level()
|
||||
Ok(role.suggested_power_level().try_into()?)
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given userID.
|
||||
@@ -83,8 +92,7 @@ pub struct RoomMember {
|
||||
pub avatar_url: Option<String>,
|
||||
pub membership: MembershipState,
|
||||
pub is_name_ambiguous: bool,
|
||||
pub power_level: i64,
|
||||
pub normalized_power_level: i64,
|
||||
pub power_level: PowerLevel,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
@@ -100,8 +108,7 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
avatar_url: m.avatar_url().map(|a| a.to_string()),
|
||||
membership: m.membership().clone().try_into()?,
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level(),
|
||||
normalized_power_level: m.normalized_power_level(),
|
||||
power_level: m.power_level().try_into()?,
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
@@ -130,3 +137,42 @@ impl TryFrom<matrix_sdk::room::RoomMemberWithSenderInfo> for RoomMemberWithSende
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum PowerLevel {
|
||||
/// The user is a room creator and has infinite power level.
|
||||
///
|
||||
/// This power level was introduced in room version 12.
|
||||
Infinite,
|
||||
|
||||
/// The user has the given power level.
|
||||
Value { value: i64 },
|
||||
}
|
||||
|
||||
impl TryFrom<UserPowerLevel> for PowerLevel {
|
||||
type Error = NotYetImplemented;
|
||||
|
||||
fn try_from(value: UserPowerLevel) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
UserPowerLevel::Infinite => Ok(Self::Infinite),
|
||||
UserPowerLevel::Int(value) => Ok(Self::Value { value: value.into() }),
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PowerLevel> for UserPowerLevel {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: PowerLevel) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
PowerLevel::Infinite => Self::Infinite,
|
||||
PowerLevel::Value { value } => {
|
||||
Self::Int(value.try_into().map_err(|err| ClientError::Generic {
|
||||
msg: "Power level is out of range".to_owned(),
|
||||
details: Some(format!("{err:?}")),
|
||||
})?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
client::{AllowRule, JoinRule},
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
@@ -22,9 +21,9 @@ pub struct RoomPreview {
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomPreview {
|
||||
/// Returns the room info the preview contains.
|
||||
pub fn info(&self) -> Result<RoomPreviewInfo, ClientError> {
|
||||
pub fn info(&self) -> RoomPreviewInfo {
|
||||
let info = &self.inner;
|
||||
Ok(RoomPreviewInfo {
|
||||
RoomPreviewInfo {
|
||||
room_id: info.room_id.to_string(),
|
||||
canonical_alias: info.canonical_alias.as_ref().map(|alias| alias.to_string()),
|
||||
name: info.name.clone(),
|
||||
@@ -32,21 +31,16 @@ impl RoomPreview {
|
||||
avatar_url: info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
num_joined_members: info.num_joined_members,
|
||||
num_active_members: info.num_active_members,
|
||||
room_type: info.room_type.as_ref().into(),
|
||||
room_type: info.room_type.clone().into(),
|
||||
is_history_world_readable: info.is_world_readable,
|
||||
membership: info.state.map(|state| state.into()),
|
||||
join_rule: info
|
||||
.join_rule
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
join_rule: info.join_rule.clone().map(Into::into),
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
.heroes
|
||||
.as_ref()
|
||||
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
@@ -122,23 +116,29 @@ pub struct RoomPreviewInfo {
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
impl TryFrom<&SpaceRoomJoinRule> for JoinRule {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(join_rule: &SpaceRoomJoinRule) -> Result<Self, ()> {
|
||||
Ok(match join_rule {
|
||||
SpaceRoomJoinRule::Invite => JoinRule::Invite,
|
||||
SpaceRoomJoinRule::Knock => JoinRule::Knock,
|
||||
SpaceRoomJoinRule::Private => JoinRule::Private,
|
||||
SpaceRoomJoinRule::Restricted => JoinRule::Restricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted { rules: Vec::new() },
|
||||
SpaceRoomJoinRule::Public => JoinRule::Public,
|
||||
SpaceRoomJoinRule::_Custom(_) => JoinRule::Custom { repr: join_rule.to_string() },
|
||||
_ => {
|
||||
warn!("unhandled SpaceRoomJoinRule: {join_rule}");
|
||||
return Err(());
|
||||
}
|
||||
})
|
||||
impl From<JoinRuleSummary> for JoinRule {
|
||||
fn from(join_rule: JoinRuleSummary) -> Self {
|
||||
match join_rule {
|
||||
JoinRuleSummary::Invite => JoinRule::Invite,
|
||||
JoinRuleSummary::Knock => JoinRule::Knock,
|
||||
JoinRuleSummary::Private => JoinRule::Private,
|
||||
JoinRuleSummary::Restricted(summary) => JoinRule::Restricted {
|
||||
rules: summary
|
||||
.allowed_room_ids
|
||||
.iter()
|
||||
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
|
||||
.collect(),
|
||||
},
|
||||
JoinRuleSummary::KnockRestricted(summary) => JoinRule::KnockRestricted {
|
||||
rules: summary
|
||||
.allowed_room_ids
|
||||
.iter()
|
||||
.map(|room_id| AllowRule::RoomMembership { room_id: room_id.to_string() })
|
||||
.collect(),
|
||||
},
|
||||
JoinRuleSummary::Public => JoinRule::Public,
|
||||
_ => JoinRule::Custom { repr: join_rule.as_str().to_owned() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,8 +153,8 @@ pub enum RoomType {
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<Option<&RumaRoomType>> for RoomType {
|
||||
fn from(value: Option<&RumaRoomType>) -> Self {
|
||||
impl From<Option<RumaRoomType>> for RoomType {
|
||||
fn from(value: Option<RumaRoomType>) -> Self {
|
||||
match value {
|
||||
Some(RumaRoomType::Space) => RoomType::Space,
|
||||
Some(RumaRoomType::_Custom(_)) => RoomType::Custom {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1486,17 +1486,17 @@ impl From<RumaSecretStorageV1AesHmacSha2Properties> for SecretStorageV1AesHmacSh
|
||||
#[derive(Clone, uniffi::Record, Default)]
|
||||
pub struct MediaPreviewConfig {
|
||||
/// The media previews setting for the user.
|
||||
pub media_previews: MediaPreviews,
|
||||
pub media_previews: Option<MediaPreviews>,
|
||||
|
||||
/// The invite avatars setting for the user.
|
||||
pub invite_avatars: InviteAvatars,
|
||||
pub invite_avatars: Option<InviteAvatars>,
|
||||
}
|
||||
|
||||
impl From<MediaPreviewConfigEventContent> for MediaPreviewConfig {
|
||||
fn from(value: MediaPreviewConfigEventContent) -> Self {
|
||||
Self {
|
||||
media_previews: value.media_previews.into(),
|
||||
invite_avatars: value.invite_avatars.into(),
|
||||
media_previews: value.media_previews.map(Into::into),
|
||||
invite_avatars: value.invite_avatars.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,18 +254,14 @@ impl SessionVerificationController {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(sender_profile) = self.account.fetch_user_profile_of(sender).await else {
|
||||
let Ok(sender_profile) = UserProfile::fetch(&self.account, sender).await else {
|
||||
error!("Failed fetching user profile for verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_profile: UserProfile {
|
||||
user_id: request.other_user_id().to_string(),
|
||||
display_name: sender_profile.displayname,
|
||||
avatar_url: sender_profile.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
},
|
||||
sender_profile,
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
device_display_name: other_device_data.display_name().map(str::to_string),
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_preview::RoomType,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
/// The main entry point into the Spaces facilities.
|
||||
///
|
||||
/// The spaces service is responsible for retrieving one's joined rooms,
|
||||
/// building a graph out of their `m.space.parent` and `m.space.child` state
|
||||
/// events, and providing access to the top-level spaces and their children.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SpaceService {
|
||||
inner: UISpaceService,
|
||||
}
|
||||
|
||||
impl SpaceService {
|
||||
/// Creates a new `SpaceService` instance.
|
||||
pub(crate) fn new(inner: UISpaceService) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceService {
|
||||
/// Returns a list of all the top-level joined spaces. It will eagerly
|
||||
/// compute the latest version and also notify subscribers if there were
|
||||
/// any changes.
|
||||
pub async fn joined_spaces(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.joined_spaces().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Subscribes to updates on the joined spaces list. If space rooms are
|
||||
/// joined or left, the stream will yield diffs that reflect the changes.
|
||||
pub async fn subscribe_to_joined_spaces(
|
||||
&self,
|
||||
listener: Box<dyn SpaceServiceJoinedSpacesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_joined_spaces().await;
|
||||
|
||||
listener.on_update(vec![SpaceListUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns a 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.
|
||||
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).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()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The `SpaceRoomList`represents a paginated list of direct rooms
|
||||
/// that belong to a particular space.
|
||||
///
|
||||
/// It can be used to paginate through the list (and have live updates on the
|
||||
/// pagination state) as well as subscribe to changes as rooms are joined or
|
||||
/// left.
|
||||
///
|
||||
/// The `SpaceRoomList` also automatically subscribes to client room changes
|
||||
/// and updates the list accordingly as rooms are joined or left.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SpaceRoomList {
|
||||
inner: UISpaceRoomList,
|
||||
}
|
||||
|
||||
impl SpaceRoomList {
|
||||
/// Creates a new `SpaceRoomList` for the underlying UI crate room list.
|
||||
fn new(inner: UISpaceRoomList) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceRoomList {
|
||||
/// Returns 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()
|
||||
}
|
||||
|
||||
/// Subscribe to pagination updates.
|
||||
pub fn subscribe_to_pagination_state_updates(
|
||||
&self,
|
||||
listener: Box<dyn SpaceRoomListPaginationStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let pagination_state = self.inner.subscribe_to_pagination_state_updates();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(pagination_state);
|
||||
|
||||
while let Some(state) = pagination_state.next().await {
|
||||
listener.on_update(state);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Return the current list of rooms.
|
||||
pub fn rooms(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.rooms().into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Subscribes to room list updates.
|
||||
pub fn subscribe_to_room_update(
|
||||
&self,
|
||||
listener: Box<dyn SpaceRoomListEntriesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_room_updates();
|
||||
|
||||
listener.on_update(vec![SpaceListUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Ask the list to retrieve the next page if the end hasn't been reached
|
||||
/// yet. Otherwise it no-ops.
|
||||
pub async fn paginate(&self) -> Result<(), ClientError> {
|
||||
self.inner.paginate().await.map_err(ClientError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait 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);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceRoomListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, rooms: Vec<SpaceListUpdate>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceServiceJoinedSpacesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, room_updates: Vec<SpaceListUpdate>);
|
||||
}
|
||||
|
||||
/// Structure representing a room in a space and aggregated information
|
||||
/// relevant to the UI layer.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SpaceRoom {
|
||||
/// The ID of the room.
|
||||
pub room_id: String,
|
||||
/// The canonical alias of the room, if any.
|
||||
pub canonical_alias: Option<String>,
|
||||
/// The 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.
|
||||
pub avatar_url: Option<String>,
|
||||
/// The type of room from `m.room.create`, if any.
|
||||
pub room_type: RoomType,
|
||||
/// The number of members joined to the room.
|
||||
pub num_joined_members: u64,
|
||||
/// The join rule of the room.
|
||||
pub join_rule: Option<JoinRule>,
|
||||
/// Whether the room may be viewed by users without joining.
|
||||
pub world_readable: Option<bool>,
|
||||
/// Whether guest users may join the room and participate in it.
|
||||
pub guest_can_join: bool,
|
||||
|
||||
/// 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 {
|
||||
fn from(room: UISpaceRoom) -> Self {
|
||||
Self {
|
||||
room_id: room.room_id.into(),
|
||||
canonical_alias: room.canonical_alias.map(|alias| alias.into()),
|
||||
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(),
|
||||
num_joined_members: room.num_joined_members,
|
||||
join_rule: room.join_rule.map(Into::into),
|
||||
world_readable: room.world_readable,
|
||||
guest_can_join: room.guest_can_join,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SpaceListUpdate {
|
||||
Append { values: Vec<SpaceRoom> },
|
||||
Clear,
|
||||
PushFront { value: SpaceRoom },
|
||||
PushBack { value: SpaceRoom },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: SpaceRoom },
|
||||
Set { index: u32, value: SpaceRoom },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<SpaceRoom> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
|
||||
fn from(diff: VectorDiff<UISpaceRoom>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,15 @@ impl SyncService {
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Force expiring both sliding sync sessions.
|
||||
///
|
||||
/// This ensures that the sync service is stopped before expiring both
|
||||
/// sessions. It should be used sparingly, as it will cause a restart of
|
||||
/// the sessions on the server as well.
|
||||
pub async fn expire_sessions(&self) {
|
||||
self.inner.expire_sessions().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -15,32 +15,29 @@
|
||||
use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
BaseVideoInfo, Thumbnail,
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
event_cache::RoomPaginationStatus,
|
||||
room::{
|
||||
edit::EditedContent as SdkEditedContent,
|
||||
reply::{EnforceThread, Reply},
|
||||
},
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
executor::{AbortHandle, JoinHandle},
|
||||
stream::StreamExt,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, AttachmentSource, EventItemOrigin, Profile, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
self, AttachmentConfig, AttachmentSource, EventItemOrigin,
|
||||
LatestEventValue as UiLatestEventValue, MediaUploadProgress as SdkMediaUploadProgress, Profile,
|
||||
TimelineDetails, TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
poll::{
|
||||
@@ -52,8 +49,8 @@ use ruma::{
|
||||
},
|
||||
},
|
||||
room::message::{
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
|
||||
TextMessageEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
@@ -66,10 +63,8 @@ use uuid::Uuid;
|
||||
use self::content::TimelineItemContent;
|
||||
pub use self::msg_like::MessageContent;
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
event::EventOrTransactionId,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
ruma::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
|
||||
ThumbnailInfo, VideoInfo,
|
||||
@@ -105,45 +100,40 @@ impl Timeline {
|
||||
params: UploadParameters,
|
||||
attachment_info: AttachmentInfo,
|
||||
mime_type: Option<String>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
thumbnail: Option<Thumbnail>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
let in_reply_to_event_id = params
|
||||
.in_reply_to
|
||||
.map(EventId::parse)
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.thumbnail(thumbnail)
|
||||
.info(attachment_info)
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
let 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,
|
||||
mentions: params.mentions.map(Into::into),
|
||||
in_reply_to: in_reply_to_event_id,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut request =
|
||||
self.inner.send_attachment(params.source, mime_type, attachment_config);
|
||||
|
||||
if params.use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
}
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
self.inner
|
||||
.send_attachment(params.source, mime_type, attachment_config)
|
||||
.use_send_queue()
|
||||
.await
|
||||
.map_err(|_| RoomError::FailedSendingAttachment)
|
||||
}));
|
||||
|
||||
Ok(handle)
|
||||
@@ -151,15 +141,19 @@ impl Timeline {
|
||||
}
|
||||
|
||||
fn build_thumbnail_info(
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
thumbnail_info: Option<ThumbnailInfo>,
|
||||
) -> Result<Option<Thumbnail>, RoomError> {
|
||||
match (thumbnail_path, thumbnail_info) {
|
||||
match (thumbnail_source, thumbnail_info) {
|
||||
(None, None) => Ok(None),
|
||||
|
||||
(Some(thumbnail_path), Some(thumbnail_info)) => {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_path).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
(Some(thumbnail_source), Some(thumbnail_info)) => {
|
||||
let thumbnail_data = match thumbnail_source {
|
||||
UploadSource::File { filename } => {
|
||||
fs::read(filename).map_err(|_| RoomError::InvalidThumbnailData)?
|
||||
}
|
||||
UploadSource::Data { bytes, .. } => bytes,
|
||||
};
|
||||
|
||||
let height = thumbnail_info
|
||||
.height
|
||||
@@ -189,7 +183,7 @@ fn build_thumbnail_info(
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("Ignoring thumbnail because either the thumbnail path or info isn't defined");
|
||||
warn!("Ignoring thumbnail because either the thumbnail source or info isn't defined");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -205,16 +199,12 @@ pub struct UploadParameters {
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
/// Optional intentional mentions to be sent with the media.
|
||||
mentions: Option<Mentions>,
|
||||
/// Optional parameters for sending the media as (threaded) reply.
|
||||
reply_params: Option<ReplyParameters>,
|
||||
/// Should the media be sent with the send queue, or synchronously?
|
||||
///
|
||||
/// Watching progress only works with the synchronous method, at the moment.
|
||||
use_send_queue: bool,
|
||||
/// Optional Event ID to reply to.
|
||||
in_reply_to: Option<String>,
|
||||
}
|
||||
|
||||
/// A source for uploading a file
|
||||
#[derive(uniffi::Enum)]
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum UploadSource {
|
||||
/// Upload source is a file on disk
|
||||
File {
|
||||
@@ -239,34 +229,47 @@ impl From<UploadSource> for AttachmentSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ReplyParameters {
|
||||
/// The ID of the event to reply to.
|
||||
event_id: String,
|
||||
/// Whether to enforce a thread relation.
|
||||
enforce_thread: bool,
|
||||
/// If enforcing a threaded relation, whether the message is a reply on a
|
||||
/// thread.
|
||||
reply_within_thread: bool,
|
||||
/// This type represents the progress of a media (consisting of a file and
|
||||
/// possibly a thumbnail) being uploaded.
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct MediaUploadProgress {
|
||||
/// The index of the media within the transaction. A file and its
|
||||
/// thumbnail share the same index. Will always be 0 for non-gallery
|
||||
/// media uploads.
|
||||
pub index: u64,
|
||||
|
||||
/// The current combined upload progress for both the file and,
|
||||
/// if it exists, its thumbnail.
|
||||
pub progress: AbstractProgress,
|
||||
}
|
||||
|
||||
impl TryInto<Reply> for ReplyParameters {
|
||||
type Error = RoomError;
|
||||
impl From<SdkMediaUploadProgress> for MediaUploadProgress {
|
||||
fn from(value: SdkMediaUploadProgress) -> Self {
|
||||
Self { index: value.index, progress: value.progress.into() }
|
||||
}
|
||||
}
|
||||
|
||||
fn try_into(self) -> Result<Reply, Self::Error> {
|
||||
let event_id =
|
||||
EventId::parse(&self.event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
let enforce_thread = if self.enforce_thread {
|
||||
EnforceThread::Threaded(if self.reply_within_thread {
|
||||
ReplyWithinThread::Yes
|
||||
} else {
|
||||
ReplyWithinThread::No
|
||||
})
|
||||
} else {
|
||||
EnforceThread::MaybeThreaded
|
||||
};
|
||||
/// Progress of an operation in abstract units.
|
||||
///
|
||||
/// Contrary to [`TransmissionProgress`], this allows tracking the progress
|
||||
/// of sending or receiving a payload in estimated pseudo units representing a
|
||||
/// percentage. This is helpful in cases where the exact progress in bytes isn't
|
||||
/// known, for instance, because encryption (which changes the size) happens on
|
||||
/// the fly.
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct AbstractProgress {
|
||||
/// How many units were already transferred.
|
||||
pub current: u64,
|
||||
/// How many units there are in total.
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
Ok(Reply { event_id, enforce_thread })
|
||||
impl From<matrix_sdk::send_queue::AbstractProgress> for AbstractProgress {
|
||||
fn from(value: matrix_sdk::send_queue::AbstractProgress) -> Self {
|
||||
Self {
|
||||
current: value.current.try_into().unwrap_or(u64::MAX),
|
||||
total: value.total.try_into().unwrap_or(u64::MAX),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,17 +284,14 @@ impl Timeline {
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
|
||||
values: timeline_items,
|
||||
}))]);
|
||||
listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
|
||||
// Then forward new items.
|
||||
while let Some(diffs) = timeline_stream.next().await {
|
||||
listener
|
||||
.on_update(diffs.into_iter().map(|d| Arc::new(TimelineDiff::new(d))).collect());
|
||||
listener.on_update(diffs.into_iter().map(TimelineDiff::new).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
@@ -354,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.
|
||||
///
|
||||
@@ -386,80 +400,61 @@ impl Timeline {
|
||||
pub fn send_image(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
image_info: ImageInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Image(
|
||||
BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, image_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
image_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
let thumbnail = build_thumbnail_info(thumbnail_source, image_info.thumbnail_info)?;
|
||||
self.send_attachment(params, attachment_info, image_info.mimetype, thumbnail)
|
||||
}
|
||||
|
||||
pub fn send_video(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
video_info: VideoInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Video(
|
||||
BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, video_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
video_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
let thumbnail = build_thumbnail_info(thumbnail_source, video_info.thumbnail_info)?;
|
||||
self.send_attachment(params, attachment_info, video_info.mimetype, thumbnail)
|
||||
}
|
||||
|
||||
pub fn send_audio(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Audio(
|
||||
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
|
||||
}
|
||||
|
||||
pub fn send_voice_message(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
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, progress_watcher, 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(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
file_info: FileInfo,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::File(
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, file_info.mimetype, progress_watcher, None)
|
||||
self.send_attachment(params, attachment_info, file_info.mimetype, None)
|
||||
}
|
||||
|
||||
pub async fn create_poll(
|
||||
@@ -529,9 +524,10 @@ impl Timeline {
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
reply_params: ReplyParameters,
|
||||
event_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.send_reply((*msg).clone(), reply_params.try_into()?).await?;
|
||||
let event_id = EventId::parse(&event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
self.inner.send_reply((*msg).clone(), event_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -585,7 +581,7 @@ impl Timeline {
|
||||
description: Option<String>,
|
||||
zoom_level: Option<u8>,
|
||||
asset_type: Option<AssetType>,
|
||||
reply_params: Option<ReplyParameters>,
|
||||
replied_to_event_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut location_event_message_content =
|
||||
LocationMessageEventContent::new(body, geo_uri.clone());
|
||||
@@ -604,8 +600,8 @@ impl Timeline {
|
||||
MessageType::Location(location_event_message_content),
|
||||
);
|
||||
|
||||
if let Some(reply_params) = reply_params {
|
||||
self.send_reply(Arc::new(room_message_event_content), reply_params).await
|
||||
if let Some(replied_to_event_id) = replied_to_event_id {
|
||||
self.send_reply(Arc::new(room_message_event_content), replied_to_event_id).await
|
||||
} else {
|
||||
self.send(Arc::new(room_message_event_content)).await?;
|
||||
Ok(())
|
||||
@@ -623,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> {
|
||||
@@ -818,7 +815,7 @@ pub enum FocusEventError {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, diff: Vec<Arc<TimelineDiff>>);
|
||||
fn on_update(&self, diff: Vec<TimelineDiff>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -826,7 +823,7 @@ pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RoomPaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum TimelineDiff {
|
||||
Append { values: Vec<Arc<TimelineItem>> },
|
||||
Clear,
|
||||
@@ -834,10 +831,10 @@ pub enum TimelineDiff {
|
||||
PushBack { value: Arc<TimelineItem> },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: usize, value: Arc<TimelineItem> },
|
||||
Set { index: usize, value: Arc<TimelineItem> },
|
||||
Remove { index: usize },
|
||||
Truncate { length: usize },
|
||||
Insert { index: u32, value: Arc<TimelineItem> },
|
||||
Set { index: u32, value: Arc<TimelineItem> },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<Arc<TimelineItem>> },
|
||||
}
|
||||
|
||||
@@ -848,14 +845,18 @@ impl TimelineDiff {
|
||||
Self::Append { values: values.into_iter().map(TimelineItem::from_arc).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index, value: TimelineItem::from_arc(value) }
|
||||
VectorDiff::Insert { index, value } => Self::Insert {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: TimelineItem::from_arc(value),
|
||||
},
|
||||
VectorDiff::Set { index, value } => Self::Set {
|
||||
index: u32::try_from(index).unwrap(),
|
||||
value: TimelineItem::from_arc(value),
|
||||
},
|
||||
VectorDiff::Truncate { length } => {
|
||||
Self::Truncate { length: u32::try_from(length).unwrap() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index, value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length },
|
||||
VectorDiff::Remove { index } => Self::Remove { index },
|
||||
VectorDiff::Remove { index } => Self::Remove { index: u32::try_from(index).unwrap() },
|
||||
VectorDiff::PushBack { value } => {
|
||||
Self::PushBack { value: TimelineItem::from_arc(value) }
|
||||
}
|
||||
@@ -871,94 +872,6 @@ impl TimelineDiff {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineDiff {
|
||||
pub fn change(&self) -> TimelineChange {
|
||||
match self {
|
||||
Self::Append { .. } => TimelineChange::Append,
|
||||
Self::Insert { .. } => TimelineChange::Insert,
|
||||
Self::Set { .. } => TimelineChange::Set,
|
||||
Self::Remove { .. } => TimelineChange::Remove,
|
||||
Self::PushBack { .. } => TimelineChange::PushBack,
|
||||
Self::PushFront { .. } => TimelineChange::PushFront,
|
||||
Self::PopBack => TimelineChange::PopBack,
|
||||
Self::PopFront => TimelineChange::PopFront,
|
||||
Self::Clear => TimelineChange::Clear,
|
||||
Self::Truncate { .. } => TimelineChange::Truncate,
|
||||
Self::Reset { .. } => TimelineChange::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Append { values } => values)
|
||||
}
|
||||
|
||||
pub fn insert(self: Arc<Self>) -> Option<InsertData> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Insert { index, value } => {
|
||||
InsertData { index: index.try_into().unwrap(), item: value }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set(self: Arc<Self>) -> Option<SetData> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Set { index, value } => {
|
||||
SetData { index: index.try_into().unwrap(), item: value }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove(&self) -> Option<u32> {
|
||||
as_variant!(self, Self::Remove { index } => (*index).try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn push_back(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::PushBack { value } => value)
|
||||
}
|
||||
|
||||
pub fn push_front(self: Arc<Self>) -> Option<Arc<TimelineItem>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::PushFront { value } => value)
|
||||
}
|
||||
|
||||
pub fn reset(self: Arc<Self>) -> Option<Vec<Arc<TimelineItem>>> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
as_variant!(this, Self::Reset { values } => values)
|
||||
}
|
||||
|
||||
pub fn truncate(&self) -> Option<u32> {
|
||||
as_variant!(self, Self::Truncate { length } => (*length).try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct InsertData {
|
||||
pub index: u32,
|
||||
pub item: Arc<TimelineItem>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SetData {
|
||||
pub index: u32,
|
||||
pub item: Arc<TimelineItem>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Enum)]
|
||||
pub enum TimelineChange {
|
||||
Append,
|
||||
Clear,
|
||||
Insert,
|
||||
Set,
|
||||
Remove,
|
||||
PushBack,
|
||||
PushFront,
|
||||
PopBack,
|
||||
PopFront,
|
||||
Truncate,
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct TimelineUniqueId {
|
||||
id: String,
|
||||
@@ -1018,7 +931,11 @@ impl TimelineItem {
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EventSendState {
|
||||
/// The local event has not been sent yet.
|
||||
NotSentYet,
|
||||
NotSentYet {
|
||||
/// The progress of the sending operation, if the event involves a media
|
||||
/// upload.
|
||||
progress: Option<MediaUploadProgress>,
|
||||
},
|
||||
|
||||
/// The local event has been sent to the server, but unsuccessfully: The
|
||||
/// sending has failed.
|
||||
@@ -1043,7 +960,9 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
|
||||
use matrix_sdk_ui::timeline::EventSendState::*;
|
||||
|
||||
match value {
|
||||
NotSentYet => Self::NotSentYet,
|
||||
NotSentYet { progress } => {
|
||||
Self::NotSentYet { progress: progress.clone().map(|p| p.into()) }
|
||||
}
|
||||
SendingFailed { error, is_recoverable } => {
|
||||
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&**error).into();
|
||||
Self::SendingFailed {
|
||||
@@ -1381,6 +1300,52 @@ impl LazyTimelineItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mimic the [`UiLatestEventValue`] type.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum LatestEventValue {
|
||||
None,
|
||||
Remote {
|
||||
timestamp: Timestamp,
|
||||
sender: String,
|
||||
is_own: bool,
|
||||
profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
},
|
||||
Local {
|
||||
timestamp: Timestamp,
|
||||
sender: String,
|
||||
profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
is_sending: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<UiLatestEventValue> for LatestEventValue {
|
||||
fn from(value: UiLatestEventValue) -> Self {
|
||||
match value {
|
||||
UiLatestEventValue::None => Self::None,
|
||||
UiLatestEventValue::Remote { timestamp, sender, is_own, profile, content } => {
|
||||
Self::Remote {
|
||||
timestamp: timestamp.into(),
|
||||
sender: sender.to_string(),
|
||||
is_own,
|
||||
profile: profile.into(),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
UiLatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
|
||||
Self::Local {
|
||||
timestamp: timestamp.into(),
|
||||
sender: sender.to_string(),
|
||||
profile: profile.into(),
|
||||
content: content.into(),
|
||||
is_sending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-msc4274")]
|
||||
mod galleries {
|
||||
use std::{panic, sync::Arc};
|
||||
@@ -1394,6 +1359,7 @@ mod galleries {
|
||||
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
@@ -1401,7 +1367,7 @@ mod galleries {
|
||||
error::RoomError,
|
||||
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{build_thumbnail_info, ReplyParameters, Timeline},
|
||||
timeline::{build_thumbnail_info, Timeline, UploadSource},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -1412,37 +1378,37 @@ mod galleries {
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
/// Optional intentional mentions to be sent with the gallery.
|
||||
mentions: Option<Mentions>,
|
||||
/// Optional parameters for sending the media as (threaded) reply.
|
||||
reply_params: Option<ReplyParameters>,
|
||||
/// Optional Event ID to reply to.
|
||||
in_reply_to: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum GalleryItemInfo {
|
||||
Audio {
|
||||
audio_info: AudioInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
},
|
||||
File {
|
||||
file_info: FileInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
},
|
||||
Image {
|
||||
image_info: ImageInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
},
|
||||
Video {
|
||||
video_info: VideoInfo,
|
||||
filename: String,
|
||||
source: UploadSource,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1456,12 +1422,12 @@ mod galleries {
|
||||
}
|
||||
}
|
||||
|
||||
fn filename(&self) -> &String {
|
||||
fn source(&self) -> &UploadSource {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { filename, .. } => filename,
|
||||
GalleryItemInfo::File { filename, .. } => filename,
|
||||
GalleryItemInfo::Image { filename, .. } => filename,
|
||||
GalleryItemInfo::Video { filename, .. } => filename,
|
||||
GalleryItemInfo::File { source, .. } => source,
|
||||
GalleryItemInfo::Audio { source, .. } => source,
|
||||
GalleryItemInfo::Image { source, .. } => source,
|
||||
GalleryItemInfo::Video { source, .. } => source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1507,11 +1473,17 @@ mod galleries {
|
||||
fn thumbnail(&self) -> Result<Option<Thumbnail>, RoomError> {
|
||||
match self {
|
||||
GalleryItemInfo::Audio { .. } | GalleryItemInfo::File { .. } => Ok(None),
|
||||
GalleryItemInfo::Image { image_info, thumbnail_path, .. } => {
|
||||
build_thumbnail_info(thumbnail_path.clone(), image_info.thumbnail_info.clone())
|
||||
GalleryItemInfo::Image { image_info, thumbnail_source, .. } => {
|
||||
build_thumbnail_info(
|
||||
thumbnail_source.as_ref().cloned(),
|
||||
image_info.thumbnail_info.clone(),
|
||||
)
|
||||
}
|
||||
GalleryItemInfo::Video { video_info, thumbnail_path, .. } => {
|
||||
build_thumbnail_info(thumbnail_path.clone(), video_info.thumbnail_info.clone())
|
||||
GalleryItemInfo::Video { video_info, thumbnail_source, .. } => {
|
||||
build_thumbnail_info(
|
||||
thumbnail_source.as_ref().cloned(),
|
||||
video_info.thumbnail_info.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1526,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.filename().into(),
|
||||
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()?,
|
||||
})
|
||||
}
|
||||
@@ -1593,16 +1568,23 @@ 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
|
||||
.as_ref()
|
||||
.map(EventId::parse)
|
||||
.transpose()
|
||||
.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))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
.in_reply_to(in_reply_to);
|
||||
|
||||
for item_info in item_infos {
|
||||
gallery_config = gallery_config.add_item(item_info.try_into()?);
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
|
||||
use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
|
||||
use super::{
|
||||
content::Reaction,
|
||||
@@ -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 {
|
||||
@@ -197,6 +202,17 @@ pub fn get_element_call_required_permissions(
|
||||
.chain(read_send.clone())
|
||||
.collect(),
|
||||
send: vec![
|
||||
// To notify other users that a call has started.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: 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(),
|
||||
},
|
||||
// To send the call participation state event (main MatrixRTC event).
|
||||
// This is required for legacy state events (using only one event for all devices with
|
||||
// a membership array). TODO: remove once legacy call member events are
|
||||
@@ -211,6 +227,12 @@ pub fn get_element_call_required_permissions(
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// Same as above for [MSC3779] and [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143),
|
||||
// with application suffix
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("{own_user_id}_{own_device_id}_m.call"),
|
||||
},
|
||||
// The same as above but with an underscore.
|
||||
// To work around the issue that state events starting with `@` have to be Matrix id's
|
||||
// but we use mxId+deviceId.
|
||||
@@ -218,6 +240,11 @@ pub fn get_element_call_required_permissions(
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("_{own_user_id}_{own_device_id}"),
|
||||
},
|
||||
// Same as above for [MSC4143], with application suffix
|
||||
WidgetEventFilter::StateWithTypeAndStateKey {
|
||||
event_type: StateEventType::CallMember.to_string(),
|
||||
state_key: format!("_{own_user_id}_{own_device_id}_m.call"),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.chain(read_send)
|
||||
@@ -497,10 +524,20 @@ mod tests {
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI",
|
||||
);
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#@my_user:my_domain.org_ABCDEFGHI_m.call",
|
||||
);
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI",
|
||||
);
|
||||
cap_assert(
|
||||
"org.matrix.msc2762.send.state_event:org.matrix.msc3401.call.member#_@my_user:my_domain.org_ABCDEFGHI_m.call",
|
||||
);
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"rust-analyzer.checkOnSave.command": "check",
|
||||
"rust-analyzer.rustfmt.extraArgs": ["+nightly"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,114 @@ 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
|
||||
- Add `SyncResponse::RoomUpdates::is_empty` to check if there were any room updates.
|
||||
([#5593](https://github.com/matrix-org/matrix-rust-sdk/pull/5593))
|
||||
- Add `EncryptionState::StateEncrypted` to represent rooms supporting encrypted
|
||||
state events. Feature-gated behind `experimental-encrypted-state-events`.
|
||||
([#5523](https://github.com/matrix-org/matrix-rust-sdk/pull/5523))
|
||||
- [**breaking**] The `state` field of `JoinedRoomUpdate` and `LeftRoomUpdate`
|
||||
now uses the `State` enum, depending on whether the state changes were
|
||||
received in the `state` field or the `state_after` field.
|
||||
([#5488](https://github.com/matrix-org/matrix-rust-sdk/pull/5488))
|
||||
- [**breaking**] `RoomCreateWithCreatorEventContent` has a new field
|
||||
`additional_creators` that allows to specify additional room creators beside
|
||||
the user sending the `m.room.create` event, introduced with room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] The `RoomInfo` method now remembers the inviter at the time
|
||||
when the `BaseClient::room_joined()` method was called. The caller is
|
||||
responsible to remember the inviter before a server request to join the room
|
||||
is made. The `RoomInfo::invite_accepted_at` method was removed, the
|
||||
`RoomInfo::invite_details` method returns both the timestamp and the
|
||||
inviter.
|
||||
([#5390](https://github.com/matrix-org/matrix-rust-sdk/pull/5390))
|
||||
|
||||
### Refactor
|
||||
- [**breaking**] The `Stripped` variants of `RawAnySyncOrStrippedTimelineEvent`,
|
||||
`RawAnySyncOrStrippedState` and `AnySyncOrStrippedState` use `StrippedState`
|
||||
instead of `AnyStrippedStateEvent`.
|
||||
([#5473](https://github.com/matrix-org/matrix-rust-sdk/pull/5473))
|
||||
- [**breaking**] The `stripped_state` field of `StateChanges` uses
|
||||
`StrippedState` instead of `AnyStrippedStateEvent`.
|
||||
([#5473](https://github.com/matrix-org/matrix-rust-sdk/pull/5473))
|
||||
- [**breaking**] `RelationalLinkedChunk::items` now takes a `RoomId` instead of an
|
||||
`&OwnedLinkedChunkId` parameter.
|
||||
([#5445](https://github.com/matrix-org/matrix-rust-sdk/pull/5445))
|
||||
- [**breaking**] Add an `IsPrefix = False` bound to the
|
||||
`get_state_event_static()`, `get_state_event_static_for_key()` and
|
||||
`get_state_events_static()`, `get_account_data_event_static()` and
|
||||
`get_room_account_data_event_static` methods of `StateStoreExt`. These methods
|
||||
only worked for events where the full event type is statically-known, and this
|
||||
is now enforced at compile-time. The matching non-`static` methods of
|
||||
`StateStore` can be used instead for event types with a variable suffix.
|
||||
([#5444](https://github.com/matrix-org/matrix-rust-sdk/pull/5444))
|
||||
- [**breaking**] `SyncOrStrippedState<RoomPowerLevelsEventContent>::power_levels()`
|
||||
takes `AuthorizationRules` and a list of creators, because creators can have
|
||||
infinite power levels, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] `RoomMember::power_level()` and
|
||||
`RoomMember::normalized_power_level()` now use `UserPowerLevel` to represent
|
||||
power levels instead of `i64` to differentiate the infinite power level of
|
||||
creators, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] The `creator()` methods of `Room` and `RoomInfo` have been
|
||||
renamed to `creators()` and can now return a list of user IDs, to reflect that
|
||||
a room can have several creators, as introduced in room version 12.
|
||||
([#5436](https://github.com/matrix-org/matrix-rust-sdk/pull/5436))
|
||||
- [**breaking**] `RoomInfo::room_version_or_default()` was replaced with
|
||||
`room_version_rules_or_default()`. The room version should only be used for
|
||||
display purposes. The rules contain flags for all the differences in behavior
|
||||
between all known room versions.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- [**breaking**] `MinimalStateEvent::redact()` takes `RedactionRules` instead of
|
||||
a `RoomVersionId`.
|
||||
([#5337](https://github.com/matrix-org/matrix-rust-sdk/pull/5337))
|
||||
- [**breaking**] The `event_id` field of `PredecessorRoom` was removed, due to
|
||||
its removal in the Matrix specification with MSC4291.
|
||||
([#5419](https://github.com/matrix-org/matrix-rust-sdk/pull/5419))
|
||||
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
@@ -50,8 +158,8 @@ No notable changes in this release.
|
||||
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
|
||||
- There are new `'static` bounds in `MediaService` for the media cache stores
|
||||
- `event_cache::store::MemoryStore` implements `Clone`.
|
||||
- `BaseClient` now has a `handle_verification_events` field which is `true` by
|
||||
default and can be negated so the `NotificationClient` won't handle received
|
||||
- `BaseClient` now has a `handle_verification_events` field which is `true` by
|
||||
default and can be negated so the `NotificationClient` won't handle received
|
||||
verification events too, causing errors in the `VerificationMachine`.
|
||||
- [**breaking**] `Room::is_encryption_state_synced` has been removed
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "The base component to build a Matrix client library."
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version.workspace = true
|
||||
version = "0.13.0"
|
||||
version = "0.16.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -25,8 +25,21 @@ js = [
|
||||
"matrix-sdk-store-encryption/js",
|
||||
]
|
||||
qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
experimental-send-custom-to-device = ["matrix-sdk-crypto?/experimental-send-custom-to-device"]
|
||||
automatic-room-key-forwarding = [
|
||||
"matrix-sdk-crypto?/automatic-room-key-forwarding",
|
||||
]
|
||||
experimental-send-custom-to-device = [
|
||||
"matrix-sdk-crypto?/experimental-send-custom-to-device",
|
||||
]
|
||||
|
||||
# Enable experimental support for encrypting state events; see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
|
||||
experimental-encrypted-state-events = [
|
||||
"e2e-encryption",
|
||||
"ruma/unstable-msc4362",
|
||||
"matrix-sdk-crypto?/experimental-encrypted-state-events"
|
||||
]
|
||||
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
@@ -52,13 +65,15 @@ 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 }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait.workspace = true
|
||||
bitflags = { workspace = true, features = ["serde"] }
|
||||
decancer = "3.3.0"
|
||||
decancer = "3.3.3"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
@@ -69,7 +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.1"
|
||||
regex.workspace = true
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
@@ -93,6 +108,8 @@ assign = "1.1.1"
|
||||
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
|
||||
|
||||
@@ -101,6 +118,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen-test.workspace = true
|
||||
gloo-timers = { workspace = true, features = ["futures"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -24,49 +24,51 @@ use std::{
|
||||
use eyeball::{SharedObservable, Subscriber};
|
||||
use eyeball_im::{Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
use matrix_sdk_common::timer;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{
|
||||
store::DynCryptoStore, types::requests::ToDeviceRequest, CollectStrategy, DecryptionSettings,
|
||||
EncryptionSettings, OlmError, OlmMachine, TrustRequirement,
|
||||
CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
|
||||
TrustRequirement, store::DynCryptoStore, types::requests::ToDeviceRequest,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
|
||||
#[cfg(doc)]
|
||||
use ruma::DeviceId;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
events::{
|
||||
StateEvent, StateEventType,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
push_rules::{PushRulesEvent, PushRulesEventContent},
|
||||
room::member::SyncRoomMemberEvent,
|
||||
StateEvent, StateEventType,
|
||||
},
|
||||
push::Ruleset,
|
||||
time::Instant,
|
||||
OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tracing::{debug, enabled, info, instrument, warn, Level};
|
||||
use tracing::{Level, debug, enabled, info, instrument, warn};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
|
||||
deserialized_responses::DisplayName,
|
||||
error::{Error, Result},
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
|
||||
media::store::MediaStoreLock,
|
||||
response_processors::{self as processors, Context},
|
||||
room::{
|
||||
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
|
||||
},
|
||||
store::{
|
||||
ambiguity_map::AmbiguityCache, BaseStateStore, DynStateStore, MemoryStore,
|
||||
Result as StoreResult, RoomLoadSettings, StateChanges, StateStoreDataKey,
|
||||
StateStoreDataValue, StateStoreExt, StoreConfig,
|
||||
BaseStateStore, DynStateStore, MemoryStore, Result as StoreResult, RoomLoadSettings,
|
||||
StateChanges, StateStoreDataKey, StateStoreDataValue, StateStoreExt, StoreConfig,
|
||||
ambiguity_map::AmbiguityCache,
|
||||
},
|
||||
sync::{RoomUpdates, SyncResponse},
|
||||
RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
/// A no (network) IO client implementation.
|
||||
@@ -76,7 +78,7 @@ use crate::{
|
||||
/// rather through `matrix_sdk::Client`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use matrix_sdk_base::{store::StoreConfig, BaseClient, ThreadingSupport};
|
||||
/// use matrix_sdk_base::{BaseClient, ThreadingSupport, store::StoreConfig};
|
||||
///
|
||||
/// let client = BaseClient::new(
|
||||
/// StoreConfig::new("cross-process-holder-name".to_owned()),
|
||||
@@ -91,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.
|
||||
@@ -151,9 +156,16 @@ impl fmt::Debug for BaseClient {
|
||||
/// explicitly opted into).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ThreadingSupport {
|
||||
/// Threading enabled
|
||||
Enabled,
|
||||
/// Threading disabled
|
||||
/// Threading enabled.
|
||||
Enabled {
|
||||
/// Enable client-wide thread subscriptions support (MSC4306 / MSC4308).
|
||||
///
|
||||
/// This may cause filtering out of thread subscriptions, and loading
|
||||
/// the thread subscriptions via the sliding sync extension,
|
||||
/// when the room list service is being used.
|
||||
with_subscriptions: bool,
|
||||
},
|
||||
/// Threading disabled.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
@@ -182,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")]
|
||||
@@ -215,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
|
||||
@@ -273,7 +287,9 @@ impl BaseClient {
|
||||
|
||||
/// Get a stream of all the rooms changes, in addition to the existing
|
||||
/// rooms.
|
||||
pub fn rooms_stream(&self) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>>) {
|
||||
pub fn rooms_stream(
|
||||
&self,
|
||||
) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
|
||||
self.state_store.rooms_stream()
|
||||
}
|
||||
|
||||
@@ -297,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.
|
||||
@@ -404,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();
|
||||
@@ -432,21 +453,36 @@ impl BaseClient {
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The unique ID identifying the joined room.
|
||||
/// * `inviter` - When joining this room in response to an invitation, the
|
||||
/// inviter should be recorded before sending the join request to the
|
||||
/// server. Providing the inviter here ensures that the
|
||||
/// [`InviteAcceptanceDetails`] are stored for this room.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
|
||||
/// # use ruma::OwnedRoomId;
|
||||
/// # use ruma::{OwnedRoomId, OwnedUserId, RoomId};
|
||||
/// # async {
|
||||
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
|
||||
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
|
||||
/// # async fn maybe_get_inviter(room_id: &RoomId) -> anyhow::Result<Option<OwnedUserId>> { todo!() }
|
||||
/// # let room_id: &RoomId = todo!();
|
||||
/// let maybe_inviter = maybe_get_inviter(room_id).await?;
|
||||
/// let room_id = send_join_request().await?;
|
||||
/// let room = client.room_joined(&room_id).await?;
|
||||
/// let room = client.room_joined(&room_id, maybe_inviter).await?;
|
||||
///
|
||||
/// assert_eq!(room.state(), RoomState::Joined);
|
||||
/// # anyhow::Ok(()) };
|
||||
/// # matrix_sdk_test::TestResult::Ok(()) };
|
||||
/// ```
|
||||
pub async fn room_joined(&self, room_id: &RoomId) -> Result<Room> {
|
||||
pub async fn room_joined(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
inviter: Option<OwnedUserId>,
|
||||
) -> Result<Room> {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Joined,
|
||||
@@ -456,13 +492,18 @@ 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();
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
|
||||
// If our previous state was an invite and we're now in the joined state, this
|
||||
// means that the user has explicitly accepted the invite. Let's
|
||||
// remember when this has happened.
|
||||
// means that the user has explicitly accepted an invite. Let's
|
||||
// remember some details about the invite.
|
||||
//
|
||||
// This is somewhat of a workaround for our lack of cryptographic membership.
|
||||
// Later on we will decide if historic room keys should be accepted
|
||||
@@ -470,14 +511,16 @@ impl BaseClient {
|
||||
// key bundle shortly after, we might accept it. If we don't do
|
||||
// this, the homeserver could trick us into accepting any historic room key
|
||||
// bundle.
|
||||
if room.state() == RoomState::Invited {
|
||||
room_info.set_invite_accepted_now();
|
||||
if previous_state == RoomState::Invited
|
||||
&& let Some(inviter) = inviter
|
||||
{
|
||||
let details = InviteAcceptanceDetails {
|
||||
invite_accepted_at: MilliSecondsSinceUnixEpoch::now(),
|
||||
inviter,
|
||||
};
|
||||
room_info.set_invite_acceptance_details(details);
|
||||
}
|
||||
|
||||
room_info.mark_as_joined();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
|
||||
@@ -500,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();
|
||||
@@ -515,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.
|
||||
@@ -569,7 +615,12 @@ impl BaseClient {
|
||||
let processors::e2ee::to_device::Output {
|
||||
processed_to_device_events: to_device,
|
||||
room_key_updates,
|
||||
} = processors::e2ee::to_device::from_sync_v2(&response, olm_machine.as_ref()).await?;
|
||||
} = processors::e2ee::to_device::from_sync_v2(
|
||||
&response,
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
)
|
||||
.await?;
|
||||
|
||||
processors::latest_event::decrypt_from_rooms(
|
||||
&mut context,
|
||||
@@ -595,14 +646,25 @@ impl BaseClient {
|
||||
.events
|
||||
.into_iter()
|
||||
.map(|raw| {
|
||||
use matrix_sdk_common::deserialized_responses::{
|
||||
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
|
||||
ToDeviceUnableToDecryptReason,
|
||||
};
|
||||
|
||||
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
|
||||
if event_type == "m.room.encrypted" {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::UnableToDecrypt(raw)
|
||||
ProcessedToDeviceEvent::UnableToDecrypt {
|
||||
encrypted_event: raw,
|
||||
utd_info: ToDeviceUnableToDecryptInfo {
|
||||
reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::PlainText(raw)
|
||||
ProcessedToDeviceEvent::PlainText(raw)
|
||||
}
|
||||
} else {
|
||||
matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent::Invalid(raw) // Exclude events with no type
|
||||
// Exclude events with no type
|
||||
ProcessedToDeviceEvent::Invalid(raw)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -724,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,
|
||||
@@ -747,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) {
|
||||
@@ -837,14 +903,11 @@ impl BaseClient {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let StateEvent::Original(e) = &member {
|
||||
if let Some(d) = &e.content.displayname {
|
||||
let display_name = DisplayName::new(d);
|
||||
ambiguity_map
|
||||
.entry(display_name)
|
||||
.or_default()
|
||||
.insert(member.state_key().clone());
|
||||
}
|
||||
if let StateEvent::Original(e) = &member
|
||||
&& let Some(d) = &e.content.displayname
|
||||
{
|
||||
let display_name = DisplayName::new(d);
|
||||
ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
|
||||
}
|
||||
|
||||
let sync_member: SyncRoomMemberEvent = member.clone().into();
|
||||
@@ -871,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);
|
||||
|
||||
@@ -996,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(())
|
||||
}
|
||||
@@ -1016,9 +1090,10 @@ impl BaseClient {
|
||||
&self,
|
||||
global_account_data_processor: &processors::account_data::Global,
|
||||
) -> Result<Ruleset> {
|
||||
let _timer = timer!(Level::TRACE, "get_push_rules");
|
||||
if let Some(event) = global_account_data_processor
|
||||
.push_rules()
|
||||
.and_then(|ev| ev.deserialize_as::<PushRulesEvent>().ok())
|
||||
.and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
|
||||
{
|
||||
Ok(event.content.global)
|
||||
} else if let Some(event) = self
|
||||
@@ -1131,16 +1206,16 @@ impl From<&v5::Request> for RequestedRequiredStates {
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use assert_matches2::assert_let;
|
||||
use assert_matches2::{assert_let, assert_matches};
|
||||
use futures_util::FutureExt as _;
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, ruma_response_from_json, InvitedRoomBuilder,
|
||||
LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent, SyncResponseBuilder, BOB,
|
||||
BOB, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent,
|
||||
SyncResponseBuilder, async_test, event_factory::EventFactory, ruma_response_from_json,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
event_id,
|
||||
events::{room::member::MembershipState, StateEventType},
|
||||
events::{StateEventType, room::member::MembershipState},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
user_id,
|
||||
@@ -1149,10 +1224,10 @@ mod tests {
|
||||
|
||||
use super::{BaseClient, RequestedRequiredStates};
|
||||
use crate::{
|
||||
RoomDisplayName, RoomState, SessionMeta,
|
||||
client::ThreadingSupport,
|
||||
store::{RoomLoadSettings, StateStoreExt, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
RoomDisplayName, RoomState, SessionMeta,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -1662,18 +1737,10 @@ mod tests {
|
||||
let mut subscriber = client.subscribe_to_ignore_user_list_changes();
|
||||
assert!(subscriber.next().now_or_never().is_none());
|
||||
|
||||
let f = EventFactory::new();
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {
|
||||
*BOB: {}
|
||||
}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.add_global_account_data(f.ignored_user_list([(*BOB).into()]))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
@@ -1682,16 +1749,7 @@ mod tests {
|
||||
|
||||
// Receive the same response.
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {
|
||||
*BOB: {}
|
||||
}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.add_global_account_data(f.ignored_user_list([(*BOB).into()]))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
@@ -1699,16 +1757,8 @@ mod tests {
|
||||
assert!(subscriber.next().now_or_never().is_none());
|
||||
|
||||
// Now remove Bob from the ignored list.
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.build_sync_response();
|
||||
let response =
|
||||
sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
assert_let!(Some(ignored) = subscriber.next().await);
|
||||
@@ -1721,17 +1771,9 @@ mod tests {
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let f = EventFactory::new();
|
||||
let response = sync_builder
|
||||
.add_global_account_data_event(matrix_sdk_test::GlobalAccountDataTestEvent::Custom(
|
||||
json!({
|
||||
"content": {
|
||||
"ignored_users": {
|
||||
ignored_user_id: {}
|
||||
}
|
||||
},
|
||||
"type": "m.ignored_user_list",
|
||||
}),
|
||||
))
|
||||
.add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
@@ -1739,8 +1781,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_joined_at_timestamp_is_set() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
async fn test_invite_details_are_set() {
|
||||
let user_id = user_id!("@alice:localhost");
|
||||
let client = logged_in_base_client(Some(user_id)).await;
|
||||
let invited_room_id = room_id!("!invited:localhost");
|
||||
let unknown_room_id = room_id!("!unknown:localhost");
|
||||
|
||||
@@ -1757,27 +1800,41 @@ mod tests {
|
||||
.expect("The sync should have created a room in the invited state");
|
||||
|
||||
assert_eq!(invited_room.state(), RoomState::Invited);
|
||||
assert!(invited_room.inner.get().invite_accepted_at().is_none());
|
||||
assert!(invited_room.invite_acceptance_details().is_none());
|
||||
|
||||
// Now we join the room.
|
||||
let joined_room = client
|
||||
.room_joined(invited_room_id)
|
||||
.room_joined(invited_room_id, Some(user_id.to_owned()))
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
// Yup, there's a timestamp now.
|
||||
// Yup, we now have some invite details.
|
||||
assert_eq!(joined_room.state(), RoomState::Joined);
|
||||
assert!(joined_room.inner.get().invite_accepted_at().is_some());
|
||||
assert_matches!(joined_room.invite_acceptance_details(), Some(details));
|
||||
assert_eq!(details.inviter, user_id);
|
||||
|
||||
// If we didn't know about the room before the join, we assume that there wasn't
|
||||
// an invite and we don't record the timestamp.
|
||||
assert!(client.get_room(unknown_room_id).is_none());
|
||||
let unknown_room = client
|
||||
.room_joined(unknown_room_id)
|
||||
.room_joined(unknown_room_id, Some(user_id.to_owned()))
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
assert_eq!(unknown_room.state(), RoomState::Joined);
|
||||
assert!(unknown_room.inner.get().invite_accepted_at().is_none());
|
||||
assert!(unknown_room.invite_acceptance_details().is_none());
|
||||
|
||||
sync_builder.clear();
|
||||
let response =
|
||||
sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
// Now that we left the room, we shouldn't have any details anymore.
|
||||
let left_room = client
|
||||
.get_room(invited_room_id)
|
||||
.expect("The sync should have created a room in the invited state");
|
||||
|
||||
assert_eq!(left_room.state(), RoomState::Left);
|
||||
assert!(left_room.invite_acceptance_details().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,18 @@ pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
events::{
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
room_version_rules::AuthorizationRules,
|
||||
serde::Raw,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
@@ -304,8 +305,8 @@ impl RawAnySyncOrStrippedState {
|
||||
C::Redacted: RedactedStateEventContent,
|
||||
{
|
||||
match self {
|
||||
Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast()),
|
||||
Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast()),
|
||||
Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast_unchecked()),
|
||||
Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast_unchecked()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,10 +518,14 @@ impl MemberEvent {
|
||||
|
||||
impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
|
||||
/// The power levels of the event.
|
||||
pub fn power_levels(&self) -> RoomPowerLevels {
|
||||
pub fn power_levels(
|
||||
&self,
|
||||
rules: &AuthorizationRules,
|
||||
creators: Vec<OwnedUserId>,
|
||||
) -> RoomPowerLevels {
|
||||
match self {
|
||||
Self::Sync(e) => e.power_levels(),
|
||||
Self::Stripped(e) => e.power_levels(),
|
||||
Self::Sync(e) => e.power_levels(rules, creators),
|
||||
Self::Stripped(e) => e.power_levels(rules, creators),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,36 +14,35 @@
|
||||
|
||||
//! 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::{
|
||||
lazy_loader, ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update,
|
||||
ChunkContent, ChunkIdentifier as CId, LinkedChunkId, Position, Update, lazy_loader,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use matrix_sdk_test::{ALICE, DEFAULT_TEST_ROOM_ID, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
event_id,
|
||||
EventId, RoomId, event_id,
|
||||
events::{
|
||||
relation::RelationType,
|
||||
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
|
||||
AnyMessageLikeEvent, AnyTimelineEvent, relation::RelationType,
|
||||
room::message::RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
mxc_uri,
|
||||
push::Action,
|
||||
room_id, uint, EventId, RoomId,
|
||||
room_id,
|
||||
};
|
||||
|
||||
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
|
||||
use crate::{
|
||||
event_cache::{store::DEFAULT_CHUNK_CAPACITY, Gap},
|
||||
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.
|
||||
@@ -53,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,
|
||||
@@ -74,7 +91,7 @@ pub fn make_test_event_with_event_id(
|
||||
if let Some(event_id) = event_id {
|
||||
builder = builder.event_id(event_id);
|
||||
}
|
||||
let event = builder.into_raw_timeline().cast();
|
||||
let event = builder.into_raw();
|
||||
|
||||
TimelineEvent::from_decrypted(
|
||||
DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info: None },
|
||||
@@ -103,7 +120,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
|
||||
// Check event.
|
||||
let deserialized = d.event.deserialize().unwrap();
|
||||
assert_matches!(deserialized, ruma::events::AnyMessageLikeEvent::RoomMessage(msg) => {
|
||||
assert_matches!(deserialized, AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(msg)) => {
|
||||
assert_eq!(msg.as_original().unwrap().content.body(), text);
|
||||
});
|
||||
});
|
||||
@@ -115,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);
|
||||
@@ -151,195 +162,21 @@ 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);
|
||||
|
||||
/// Test multiple things related to distinguishing a thread linked chunk
|
||||
/// from a room linked chunk.
|
||||
async fn test_thread_vs_room_linked_chunk(&self);
|
||||
}
|
||||
|
||||
impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
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);
|
||||
@@ -760,31 +597,39 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(
|
||||
lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// Clear the chunks.
|
||||
self.clear_all_linked_chunks().await.unwrap();
|
||||
|
||||
// Both rooms now have no linked chunk.
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(
|
||||
lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id0).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(linked_chunk_id1).await.unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_remove_room(&self) {
|
||||
@@ -970,19 +815,21 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(event.event_id(), event_comte.event_id());
|
||||
|
||||
// Now let's try to find an event that exists, but not in the expected room.
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(
|
||||
self.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// Clearing the rooms also clears the event's storage.
|
||||
self.clear_all_linked_chunks().await.expect("failed to clear all rooms chunks");
|
||||
assert!(self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(
|
||||
self.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_find_event_relations(&self) {
|
||||
@@ -1029,12 +876,16 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
|
||||
assert_eq!(relations.len(), 2);
|
||||
// The position is `None` for items outside the linked chunk.
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(reaction_eid1) && pos.is_none()));
|
||||
assert!(
|
||||
relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none())
|
||||
);
|
||||
assert!(
|
||||
relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(reaction_eid1) && pos.is_none())
|
||||
);
|
||||
|
||||
// Finding relations with a filter only returns a subset.
|
||||
let relations = self
|
||||
@@ -1088,9 +939,139 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
}));
|
||||
|
||||
// But it's still not set for the other related events.
|
||||
assert!(relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none()));
|
||||
assert!(
|
||||
relations
|
||||
.iter()
|
||||
.any(|(ev, pos)| ev.event_id().as_deref() == Some(edit_eid1) && pos.is_none())
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -1123,16 +1104,151 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(event.event_id(), event_gruyere.event_id());
|
||||
|
||||
// But they won't be returned when searching in the wrong room.
|
||||
assert!(self
|
||||
.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
|
||||
assert!(
|
||||
self.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
self.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_thread_vs_room_linked_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let thread1_ev = event("comté");
|
||||
let thread2_ev = event("gruyère");
|
||||
let thread2_ev2 = event("beaufort");
|
||||
let room_ev = event("brillat savarin triple crème");
|
||||
|
||||
let thread_root1 = event("thread1");
|
||||
let thread_root2 = event("thread2");
|
||||
|
||||
// Add one event in a thread linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![thread1_ev.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add one event in another thread linked chunk (same room).
|
||||
self.handle_linked_chunk_updates(
|
||||
LinkedChunkId::Thread(room_id, thread_root2.event_id().unwrap().as_ref()),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![thread2_ev.clone(), thread2_ev2.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add another event to the room linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
LinkedChunkId::Room(room_id),
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![room_ev.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All the events can be found with `find_event()` for the room.
|
||||
self.find_event(room_id, thread2_ev.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.expect("failed to find thread1_ev");
|
||||
|
||||
self.find_event(room_id, thread2_ev.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
.expect("failed to find thread2_ev");
|
||||
|
||||
self.find_event(room_id, thread2_ev2.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find thread2_ev2");
|
||||
|
||||
self.find_event(room_id, room_ev.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find room_ev");
|
||||
|
||||
// Finding duplicates operates based on the linked chunk id.
|
||||
let dups = self
|
||||
.filter_duplicated_events(
|
||||
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
|
||||
vec![
|
||||
thread1_ev.event_id().unwrap().to_owned(),
|
||||
room_ev.event_id().unwrap().to_owned(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(dups.len(), 1);
|
||||
assert_eq!(dups[0].0, thread1_ev.event_id().unwrap());
|
||||
|
||||
// Loading all chunks operates based on the linked chunk id.
|
||||
let all_chunks = self
|
||||
.load_all_chunks(LinkedChunkId::Thread(
|
||||
room_id,
|
||||
thread_root2.event_id().unwrap().as_ref(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(all_chunks.len(), 1);
|
||||
assert_eq!(all_chunks[0].identifier, CId::new(0));
|
||||
assert_let!(ChunkContent::Items(observed_items) = all_chunks[0].content.clone());
|
||||
assert_eq!(observed_items.len(), 2);
|
||||
assert_eq!(observed_items[0].event_id(), thread2_ev.event_id());
|
||||
assert_eq!(observed_items[1].event_id(), thread2_ev2.event_id());
|
||||
|
||||
// Loading the metadata of all chunks operates based on the linked chunk
|
||||
// id.
|
||||
let metas = self
|
||||
.load_all_chunks_metadata(LinkedChunkId::Thread(
|
||||
room_id,
|
||||
thread_root2.event_id().unwrap().as_ref(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(metas.len(), 1);
|
||||
assert_eq!(metas[0].identifier, CId::new(0));
|
||||
assert_eq!(metas[0].num_items, 2);
|
||||
|
||||
// Loading the last chunk operates based on the linked chunk id.
|
||||
let (last_chunk, _chunk_identifier_generator) = self
|
||||
.load_last_chunk(LinkedChunkId::Thread(
|
||||
room_id,
|
||||
thread_root1.event_id().unwrap().as_ref(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let last_chunk = last_chunk.unwrap();
|
||||
assert_eq!(last_chunk.identifier, CId::new(0));
|
||||
assert_let!(ChunkContent::Items(observed_items) = last_chunk.content);
|
||||
assert_eq!(observed_items.len(), 1);
|
||||
assert_eq!(observed_items[0].event_id(), thread1_ev.event_id());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,8 +1271,8 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
/// mod tests {
|
||||
/// use super::{EventCacheStore, EventCacheStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_event_cache_store(
|
||||
/// ) -> EventCacheStoreResult<impl EventCacheStore> {
|
||||
/// async fn get_event_cache_store()
|
||||
/// -> EventCacheStoreResult<impl EventCacheStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
@@ -1175,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 =
|
||||
@@ -1252,12 +1354,33 @@ 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 =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_save_event().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_thread_vs_room_linked_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_thread_vs_room_linked_chunk().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1268,11 +1391,14 @@ macro_rules! event_cache_store_integration_tests {
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_integration_tests_time {
|
||||
() => {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod event_cache_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
||||
use gloo_timers::future::sleep;
|
||||
use matrix_sdk_test::async_test;
|
||||
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
|
||||
use tokio::time::sleep;
|
||||
use $crate::event_cache::store::IntoEventCacheStore;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
@@ -1282,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.
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(!acquired55);
|
||||
assert!(acquired55.is_none()); // not acquired
|
||||
|
||||
// Ok you can take another nap then.
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
sleep(Duration::from_millis(250)).await;
|
||||
|
||||
// At some point, we do get the lock.
|
||||
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
|
||||
assert!(acquired6);
|
||||
assert_eq!(acquired6, Some(2)); // new lock generation!
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// The other gets it almost immediately too.
|
||||
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert!(acquired7);
|
||||
assert_eq!(acquired7, Some(3)); // new lock generation!
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
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,35 +14,25 @@
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
num::NonZeroUsize,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator,
|
||||
ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Position, RawChunk, Update,
|
||||
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::{
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
};
|
||||
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
compute_filters_string, extract_event_relation,
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
EventCacheStore, EventCacheStoreError, Result,
|
||||
};
|
||||
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`.
|
||||
///
|
||||
@@ -50,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,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))
|
||||
@@ -223,11 +179,9 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
|
||||
|
||||
let event = inner
|
||||
.events
|
||||
.items(&target_linked_chunk_id)
|
||||
.items(room_id)
|
||||
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
|
||||
|
||||
Ok(event)
|
||||
@@ -241,16 +195,13 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let target_linked_chunk_id = OwnedLinkedChunkId::Room(room_id.to_owned());
|
||||
|
||||
let filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items(&target_linked_chunk_id)
|
||||
.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 {
|
||||
@@ -269,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");
|
||||
@@ -278,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())
|
||||
@@ -591,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,33 +24,29 @@ 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::{
|
||||
events::{relation::RelationType, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
OwnedEventId,
|
||||
};
|
||||
use ruma::{OwnedEventId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY},
|
||||
traits::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
|
||||
};
|
||||
|
||||
/// The high-level public type to represent an `EventCacheStore` lock.
|
||||
#[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,22 +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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,22 +16,17 @@ 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,
|
||||
},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
|
||||
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
EventCacheStoreError,
|
||||
};
|
||||
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
|
||||
@@ -128,6 +123,9 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
|
||||
|
||||
/// Find an event by its ID in a room.
|
||||
///
|
||||
/// This method must return events saved either in any linked chunks, *or*
|
||||
/// events saved "out-of-band" with the [`Self::save_event`] method.
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
@@ -147,6 +145,9 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
///
|
||||
/// An additional filter can be provided to only retrieve related events for
|
||||
/// a certain relationship.
|
||||
///
|
||||
/// This method must return events saved either in any linked chunks, *or*
|
||||
/// events saved "out-of-band" with the [`Self::save_event`] method.
|
||||
async fn find_event_relations(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
@@ -154,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.
|
||||
///
|
||||
@@ -164,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)]
|
||||
@@ -308,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)
|
||||
}
|
||||
|
||||
@@ -381,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)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +317,12 @@ pub trait IntoEventCacheStore {
|
||||
fn into_event_cache_store(self) -> Arc<DynEventCacheStore>;
|
||||
}
|
||||
|
||||
impl IntoEventCacheStore for Arc<DynEventCacheStore> {
|
||||
fn into_event_cache_store(self) -> Arc<DynEventCacheStore> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoEventCacheStore for T
|
||||
where
|
||||
T: EventCacheStore + Sized + 'static,
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
//! use as a [crate::Room::latest_event].
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
UserId,
|
||||
events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
call::invite::SyncCallInviteEvent,
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::{
|
||||
@@ -13,15 +16,146 @@ use ruma::{
|
||||
message::{MessageType, SyncRoomMessageEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
rtc::notification::SyncRtcNotificationEvent,
|
||||
sticker::SyncStickerEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
},
|
||||
UserId,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedEventId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
|
||||
|
||||
/// A latest event value!
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub enum LatestEventValue {
|
||||
/// No value has been computed yet, or no candidate value was found.
|
||||
#[default]
|
||||
None,
|
||||
|
||||
/// The latest event represents a remote event.
|
||||
Remote(RemoteLatestEventValue),
|
||||
|
||||
/// The latest event represents a local event that is sending.
|
||||
LocalIsSending(LocalLatestEventValue),
|
||||
|
||||
/// The latest event represents a local event that cannot be sent, either
|
||||
/// because a previous local event, or this local event cannot be sent.
|
||||
LocalCannotBeSent(LocalLatestEventValue),
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/// Represents the value for [`LatestEventValue::LocalIsSending`] and
|
||||
/// [`LatestEventValue::LocalCannotBeSent`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocalLatestEventValue {
|
||||
/// The time where the event has been created (by this module).
|
||||
pub timestamp: MilliSecondsSinceUnixEpoch,
|
||||
|
||||
/// The content of the local event.
|
||||
pub content: SerializableEventContent,
|
||||
}
|
||||
|
||||
#[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
|
||||
@@ -41,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.
|
||||
@@ -101,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)) => {
|
||||
@@ -125,21 +259,21 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
AnySyncTimelineEvent::State(state) => {
|
||||
// But we make an exception for knocked state events *if* the current user
|
||||
// can either accept or decline them
|
||||
if let AnySyncStateEvent::RoomMember(member) = state {
|
||||
if matches!(member.membership(), MembershipState::Knock) {
|
||||
let can_accept_or_decline_knocks = match power_levels_info {
|
||||
Some((own_user_id, room_power_levels)) => {
|
||||
room_power_levels.user_can_invite(own_user_id)
|
||||
|| room_power_levels.user_can_kick(own_user_id)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// The current user can act on the knock changes, so they should be
|
||||
// displayed
|
||||
if can_accept_or_decline_knocks {
|
||||
return PossibleLatestEvent::YesKnockedStateEvent(member);
|
||||
if let AnySyncStateEvent::RoomMember(member) = state
|
||||
&& matches!(member.membership(), MembershipState::Knock)
|
||||
{
|
||||
let can_accept_or_decline_knocks = match power_levels_info {
|
||||
Some((own_user_id, room_power_levels)) => {
|
||||
room_power_levels.user_can_invite(own_user_id)
|
||||
|| room_power_levels.user_can_kick(own_user_id)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// The current user can act on the knock changes, so they should be
|
||||
// displayed
|
||||
if can_accept_or_decline_knocks {
|
||||
return PossibleLatestEvent::YesKnockedStateEvent(member);
|
||||
}
|
||||
}
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
@@ -302,77 +436,64 @@ 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::{
|
||||
call::{
|
||||
invite::{CallInviteEventContent, SyncCallInviteEvent},
|
||||
notify::{
|
||||
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
|
||||
},
|
||||
SessionDescription,
|
||||
},
|
||||
SyncMessageLikeEvent,
|
||||
call::{SessionDescription, invite::CallInviteEventContent},
|
||||
poll::{
|
||||
unstable_response::{
|
||||
SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
|
||||
},
|
||||
unstable_response::UnstablePollResponseEventContent,
|
||||
unstable_start::{
|
||||
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
|
||||
UnstablePollAnswer, UnstablePollStartContentBlock,
|
||||
NewUnstablePollStartEventContent, UnstablePollAnswer,
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
relation::Replacement,
|
||||
room::{
|
||||
ImageInfo, MediaSource,
|
||||
encrypted::{
|
||||
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
|
||||
SyncRoomEncryptedEvent,
|
||||
},
|
||||
message::{
|
||||
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
|
||||
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
Relation, RoomMessageEventContent,
|
||||
},
|
||||
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
|
||||
ImageInfo, MediaSource,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
sticker::{StickerEventContent, SyncStickerEvent},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
|
||||
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
UnsignedRoomRedactionEvent,
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
|
||||
VoipVersionId,
|
||||
owned_event_id, owned_mxc_uri, user_id,
|
||||
};
|
||||
use ruma::{
|
||||
events::rtc::notification::{NotificationType, RtcNotificationEventContent},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::LatestEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{is_suitable_for_latest_event, PossibleLatestEvent};
|
||||
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
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)
|
||||
@@ -384,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)
|
||||
@@ -408,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)
|
||||
@@ -431,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)
|
||||
);
|
||||
}
|
||||
@@ -454,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),
|
||||
@@ -477,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),
|
||||
@@ -500,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: UnsignedRoomRedactionEvent = 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),
|
||||
@@ -529,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),
|
||||
@@ -553,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),
|
||||
@@ -579,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),
|
||||
@@ -600,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 =
|
||||
@@ -657,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!();
|
||||
|
||||
@@ -55,20 +58,22 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use room::{
|
||||
apply_redaction, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
|
||||
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomMember, RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter, SuccessorRoom,
|
||||
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
|
||||
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp,
|
||||
RoomState, RoomStateFilter, SuccessorRoom, apply_redaction,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
StateStoreDataValue, StoreError,
|
||||
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
|
||||
QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
|
||||
ThreadSubscriptionCatchupToken,
|
||||
};
|
||||
pub use utils::{
|
||||
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test::init_tracing_for_tests!();
|
||||
matrix_sdk_test_utils::init_tracing_for_tests!();
|
||||
|
||||
/// The Matrix user session info.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
//! 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,
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
events::{
|
||||
room::{
|
||||
MediaSource,
|
||||
message::{
|
||||
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
|
||||
LocationMessageEventContent, VideoMessageEventContent,
|
||||
},
|
||||
MediaSource,
|
||||
},
|
||||
sticker::StickerEventContent,
|
||||
},
|
||||
MxcUri, UInt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
+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::{
|
||||
media_service::IgnoreMediaRetentionPolicy, EventCacheStoreMedia, MediaRetentionPolicy,
|
||||
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)]
|
||||
+50
-180
@@ -12,25 +12,24 @@
|
||||
// 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::{
|
||||
executor::{spawn, JoinHandle},
|
||||
SendOutsideWasm, SyncOutsideWasm,
|
||||
executor::{JoinHandle, spawn},
|
||||
locks::Mutex,
|
||||
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
|
||||
};
|
||||
use ruma::{time::SystemTime, MxcUri};
|
||||
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.
|
||||
///
|
||||
@@ -538,24 +408,24 @@ mod tests {
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
MxcUri, OwnedMxcUri,
|
||||
events::room::MediaSource,
|
||||
mxc_uri,
|
||||
time::{Duration, SystemTime},
|
||||
MxcUri, OwnedMxcUri,
|
||||
};
|
||||
|
||||
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
|
||||
use crate::{
|
||||
event_cache::store::{media::MediaRetentionPolicy, EventCacheStoreError},
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -127,15 +127,15 @@ use matrix_sdk_common::{
|
||||
serde_helpers::extract_thread_root,
|
||||
};
|
||||
use ruma::{
|
||||
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
|
||||
SyncMessageLikeEvent,
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
|
||||
room::message::Relation,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
@@ -212,7 +212,7 @@ impl RoomReadReceipts {
|
||||
user_id: &UserId,
|
||||
threading_support: ThreadingSupport,
|
||||
) {
|
||||
if matches!(threading_support, ThreadingSupport::Enabled)
|
||||
if matches!(threading_support, ThreadingSupport::Enabled { .. })
|
||||
&& extract_thread_root(event.raw()).is_some()
|
||||
{
|
||||
return;
|
||||
@@ -264,15 +264,15 @@ impl RoomReadReceipts {
|
||||
// Sliding sync sometimes sends the same event multiple times, so it can be at
|
||||
// the beginning and end of a batch, for instance. In that case, just reset
|
||||
// every time we see the event matching the receipt.
|
||||
if let Some(event_id) = event.event_id() {
|
||||
if event_id == receipt_event_id {
|
||||
// Bingo! Switch over to the counting state, after resetting the
|
||||
// previous counts.
|
||||
trace!("Found the event the receipt was referring to! Starting to count.");
|
||||
self.reset();
|
||||
counting_receipts = true;
|
||||
continue;
|
||||
}
|
||||
if let Some(event_id) = event.event_id()
|
||||
&& event_id == receipt_event_id
|
||||
{
|
||||
// Bingo! Switch over to the counting state, after resetting the
|
||||
// previous counts.
|
||||
trace!("Found the event the receipt was referring to! Starting to count.");
|
||||
self.reset();
|
||||
counting_receipts = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if counting_receipts {
|
||||
@@ -387,17 +387,17 @@ impl ReceiptSelector {
|
||||
// Now consider new receipts.
|
||||
for (event_id, receipts) in &receipt_event.0 {
|
||||
for ty in [ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
if let Some(receipt) = receipts.get(&ty).and_then(|receipts| receipts.get(user_id))
|
||||
if let Some(receipts) = receipts.get(&ty)
|
||||
&& let Some(receipt) = receipts.get(user_id)
|
||||
&& matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded)
|
||||
{
|
||||
if matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded) {
|
||||
trace!(%event_id, "found new candidate");
|
||||
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
|
||||
self.try_select_later(event_id, *event_pos);
|
||||
} else {
|
||||
// It's a new pending receipt.
|
||||
trace!(%event_id, "stashed as pending");
|
||||
pending.push(event_id.clone());
|
||||
}
|
||||
trace!(%event_id, "found new candidate");
|
||||
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
|
||||
self.try_select_later(event_id, *event_pos);
|
||||
} else {
|
||||
// It's a new pending receipt.
|
||||
trace!(%event_id, "stashed as pending");
|
||||
pending.push(event_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(_)
|
||||
@@ -631,20 +631,20 @@ mod tests {
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
use ruma::{
|
||||
event_id,
|
||||
EventId, UserId, event_id,
|
||||
events::{
|
||||
receipt::{ReceiptThread, ReceiptType},
|
||||
room::{member::MembershipState, message::MessageType},
|
||||
},
|
||||
owned_event_id, owned_user_id,
|
||||
push::Action,
|
||||
room_id, user_id, EventId, UserId,
|
||||
room_id, user_id,
|
||||
};
|
||||
|
||||
use super::compute_unread_counts;
|
||||
use crate::{
|
||||
read_receipts::{marks_as_unread, ReceiptSelector, RoomReadReceipts},
|
||||
ThreadingSupport,
|
||||
read_receipts::{ReceiptSelector, RoomReadReceipts, marks_as_unread},
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -805,9 +805,9 @@ mod tests {
|
||||
// When provided with no events, we report not finding the event to which the
|
||||
// receipt relates.
|
||||
let mut receipts = RoomReadReceipts::default();
|
||||
assert!(receipts
|
||||
.find_and_process_events(ev0, user_id, &[], ThreadingSupport::Disabled)
|
||||
.not());
|
||||
assert!(
|
||||
receipts.find_and_process_events(ev0, user_id, &[], ThreadingSupport::Disabled).not()
|
||||
);
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
assert_eq!(receipts.num_notifications, 0);
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
@@ -828,14 +828,16 @@ mod tests {
|
||||
num_mentions: 37,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[make_event(event_id!("$1"))],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not());
|
||||
assert!(
|
||||
receipts
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[make_event(event_id!("$1"))],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not()
|
||||
);
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
assert_eq!(receipts.num_notifications, 13);
|
||||
assert_eq!(receipts.num_mentions, 37);
|
||||
@@ -867,18 +869,20 @@ mod tests {
|
||||
num_mentions: 37,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(receipts
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[
|
||||
make_event(event_id!("$1")),
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not());
|
||||
assert!(
|
||||
receipts
|
||||
.find_and_process_events(
|
||||
ev0,
|
||||
user_id,
|
||||
&[
|
||||
make_event(event_id!("$1")),
|
||||
make_event(event_id!("$2")),
|
||||
make_event(event_id!("$3"))
|
||||
],
|
||||
ThreadingSupport::Disabled
|
||||
)
|
||||
.not()
|
||||
);
|
||||
assert_eq!(receipts.num_unread, 42);
|
||||
assert_eq!(receipts.num_notifications, 13);
|
||||
assert_eq!(receipts.num_mentions, 37);
|
||||
@@ -1617,23 +1621,23 @@ mod tests {
|
||||
receipts.process_event(
|
||||
&make_event(own_alice, event_id!("$some_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(own_alice, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
receipts.process_event(
|
||||
&make_event(bob, event_id!("$some_other_thread_root")),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
|
||||
assert_eq!(receipts.num_unread, 0);
|
||||
@@ -1644,7 +1648,7 @@ mod tests {
|
||||
receipts.process_event(
|
||||
&EventFactory::new().text_msg("A").sender(bob).event_id(event_id!("$ida")).into_event(),
|
||||
own_alice,
|
||||
ThreadingSupport::Enabled,
|
||||
ThreadingSupport::Enabled { with_subscriptions: false },
|
||||
);
|
||||
|
||||
assert_eq!(receipts.num_unread, 1);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,18 @@ use std::{
|
||||
mem,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::timer;
|
||||
use ruma::{
|
||||
RoomId,
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
|
||||
AnyGlobalAccountDataEvent, GlobalAccountDataEventType, direct::OwnedDirectUserIdentifier,
|
||||
},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::{store::BaseStateStore, RoomInfo, StateChanges};
|
||||
use crate::{RoomInfo, StateChanges, store::BaseStateStore};
|
||||
|
||||
/// Create the [`Global`] account data processor.
|
||||
pub fn global(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Global {
|
||||
@@ -43,6 +44,8 @@ pub struct Global {
|
||||
impl Global {
|
||||
/// Creates a new processor for global account data.
|
||||
fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
|
||||
let _timer = timer!(tracing::Level::TRACE, "Global::process (global account data)");
|
||||
|
||||
let mut raw_by_type = BTreeMap::new();
|
||||
let mut parsed_events = Vec::new();
|
||||
|
||||
@@ -102,10 +105,10 @@ impl Global {
|
||||
|
||||
// Update the direct targets of rooms if they changed.
|
||||
for (room_id, new_direct_targets) in new_dms {
|
||||
if let Some(old_direct_targets) = old_dms.remove(&room_id) {
|
||||
if old_direct_targets == new_direct_targets {
|
||||
continue;
|
||||
}
|
||||
if let Some(old_direct_targets) = old_dms.remove(&room_id)
|
||||
&& old_direct_targets == new_direct_targets
|
||||
{
|
||||
continue;
|
||||
}
|
||||
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
|
||||
map_info(room_id, state_changes, state_store, |info| {
|
||||
@@ -125,6 +128,8 @@ impl Global {
|
||||
|
||||
/// Applies the processed data to the state changes and the state store.
|
||||
pub async fn apply(mut self, context: &mut Context, state_store: &BaseStateStore) {
|
||||
let _timer = timer!(tracing::Level::TRACE, "Global::apply (global account data)");
|
||||
|
||||
// Fill in the content of `changes.account_data`.
|
||||
mem::swap(&mut context.state_changes.account_data, &mut self.raw_by_type);
|
||||
|
||||
@@ -167,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
mod global;
|
||||
mod room;
|
||||
|
||||
pub use global::{global, Global};
|
||||
pub use global::{Global, global};
|
||||
pub use room::for_room;
|
||||
|
||||
@@ -13,20 +13,20 @@
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{
|
||||
events::{marked_unread::MarkedUnreadEventContent, AnyRoomAccountDataEvent},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
events::{AnyRoomAccountDataEvent, marked_unread::MarkedUnreadEventContent},
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::super::{Context, RoomInfoNotableUpdates};
|
||||
use crate::{
|
||||
room::AccountDataSource, store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons,
|
||||
StateChanges,
|
||||
RoomInfo, RoomInfoNotableUpdateReasons, StateChanges, room::AccountDataSource,
|
||||
store::BaseStateStore,
|
||||
};
|
||||
|
||||
#[instrument(skip_all, fields(?room_id))]
|
||||
pub async fn for_room(
|
||||
pub fn for_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
events: &[Raw<AnyRoomAccountDataEvent>],
|
||||
|
||||
@@ -13,22 +13,25 @@
|
||||
// limitations under the License.
|
||||
|
||||
use eyeball::SharedObservable;
|
||||
use matrix_sdk_common::timer;
|
||||
use ruma::{
|
||||
events::{ignored_user_list::IgnoredUserListEvent, GlobalAccountDataEventType},
|
||||
events::{GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent},
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
use super::Context;
|
||||
use crate::{
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
Result,
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
};
|
||||
|
||||
/// Save the [`StateChanges`] from the [`Context`] inside the [`BaseStateStore`]
|
||||
/// only! The changes aren't applied on the in-memory rooms.
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save_only(context: Context, state_store: &BaseStateStore) -> Result<()> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "_method");
|
||||
|
||||
save_changes(&context, state_store, None).await?;
|
||||
broadcast_room_info_notable_updates(&context, state_store);
|
||||
|
||||
@@ -44,6 +47,8 @@ pub async fn save_and_apply(
|
||||
ignore_user_list_changes: &SharedObservable<Vec<String>>,
|
||||
sync_token: Option<String>,
|
||||
) -> Result<()> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "_method");
|
||||
|
||||
trace!("ready to submit changes to store");
|
||||
|
||||
let previous_ignored_user_list =
|
||||
@@ -80,7 +85,7 @@ fn apply_changes(
|
||||
if let Some(event) =
|
||||
context.state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
{
|
||||
match event.deserialize_as::<IgnoredUserListEvent>() {
|
||||
match event.deserialize_as_unchecked::<IgnoredUserListEvent>() {
|
||||
Ok(event) => {
|
||||
let user_ids: Vec<String> =
|
||||
event.content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
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(), 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),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -14,13 +14,17 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
|
||||
use matrix_sdk_crypto::{store::types::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use matrix_sdk_common::deserialized_responses::{
|
||||
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo, ToDeviceUnableToDecryptReason,
|
||||
};
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings, EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5, DeviceLists},
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
api::client::sync::sync_events::{DeviceLists, v3, v5},
|
||||
events::AnyToDeviceEvent,
|
||||
serde::Raw,
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
};
|
||||
|
||||
use crate::Result;
|
||||
@@ -34,6 +38,7 @@ pub async fn from_msc4186(
|
||||
to_device: Option<&v5::response::ToDevice>,
|
||||
e2ee: &v5::response::E2EE,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
olm_machine,
|
||||
@@ -42,6 +47,7 @@ pub async fn from_msc4186(
|
||||
&e2ee.device_one_time_keys_count,
|
||||
e2ee.device_unused_fallback_key_types.as_deref(),
|
||||
to_device.as_ref().map(|to_device| to_device.next_batch.clone()),
|
||||
decryption_settings,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -54,6 +60,7 @@ pub async fn from_msc4186(
|
||||
pub async fn from_sync_v2(
|
||||
response: &v3::Response,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
olm_machine,
|
||||
@@ -62,6 +69,7 @@ pub async fn from_sync_v2(
|
||||
&response.device_one_time_keys_count,
|
||||
response.device_unused_fallback_key_types.as_deref(),
|
||||
Some(response.next_batch.clone()),
|
||||
decryption_settings,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -77,6 +85,7 @@ async fn process(
|
||||
one_time_keys_counts: &BTreeMap<OneTimeKeyAlgorithm, UInt>,
|
||||
unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>,
|
||||
next_batch_token: Option<String>,
|
||||
decryption_settings: &DecryptionSettings,
|
||||
) -> Result<Output> {
|
||||
let encryption_sync_changes = EncryptionSyncChanges {
|
||||
to_device_events,
|
||||
@@ -92,7 +101,7 @@ async fn process(
|
||||
// This makes sure that we have the decryption keys for the room
|
||||
// events at hand.
|
||||
let (events, room_key_updates) =
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes, decryption_settings).await?;
|
||||
|
||||
Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
} else {
|
||||
@@ -107,7 +116,12 @@ async fn process(
|
||||
.map(|raw| {
|
||||
if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
|
||||
if event_type == "m.room.encrypted" {
|
||||
ProcessedToDeviceEvent::UnableToDecrypt(raw)
|
||||
ProcessedToDeviceEvent::UnableToDecrypt {
|
||||
encrypted_event: raw,
|
||||
utd_info: ToDeviceUnableToDecryptInfo {
|
||||
reason: ToDeviceUnableToDecryptReason::NoOlmMachine,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
ProcessedToDeviceEvent::PlainText(raw)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use matrix_sdk_common::timer;
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{OwnedUserId, RoomId};
|
||||
|
||||
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
|
||||
use crate::{EncryptionState, Result, RoomMemberships, store::BaseStateStore};
|
||||
|
||||
/// Update tracked users, if the room is encrypted.
|
||||
pub async fn update(
|
||||
@@ -25,12 +26,11 @@ pub async fn update(
|
||||
room_encryption_state: EncryptionState,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
) -> Result<()> {
|
||||
if room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
}
|
||||
if room_encryption_state.is_encrypted()
|
||||
&& let Some(olm) = olm_machine
|
||||
&& !user_ids_to_track.is_empty()
|
||||
{
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -46,19 +46,21 @@ pub async fn update_or_set_if_room_is_newly_encrypted(
|
||||
room_id: &RoomId,
|
||||
state_store: &BaseStateStore,
|
||||
) -> Result<()> {
|
||||
if new_room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !previous_room_encryption_state.is_encrypted() {
|
||||
// The room turned on encryption in this sync, we need
|
||||
// to also get all the existing users and mark them for
|
||||
// tracking.
|
||||
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
|
||||
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
let _timer = timer!(tracing::Level::TRACE, "update_or_set_if_room_is_newly_encrypted");
|
||||
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
|
||||
}
|
||||
if new_room_encryption_state.is_encrypted()
|
||||
&& let Some(olm) = olm_machine
|
||||
{
|
||||
if !previous_room_encryption_state.is_encrypted() {
|
||||
// The room turned on encryption in this sync, we need
|
||||
// to also get all the existing users and mark them for
|
||||
// tracking.
|
||||
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
|
||||
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{events::AnySyncEphemeralRoomEvent, serde::Raw, RoomId};
|
||||
use ruma::{RoomId, events::AnySyncEphemeralRoomEvent, serde::Raw};
|
||||
use tracing::info;
|
||||
|
||||
use super::Context;
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
|
||||
use super::{e2ee::E2EE, verification, Context};
|
||||
use super::{Context, e2ee::E2EE, verification};
|
||||
use crate::{
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
Result, Room,
|
||||
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
|
||||
};
|
||||
|
||||
/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
|
||||
@@ -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));
|
||||
@@ -111,11 +111,15 @@ async fn decrypt_sync_room_event(
|
||||
let event = match e2ee
|
||||
.olm_machine
|
||||
.expect("An `OlmMachine` is expected")
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, e2ee.decryption_settings)
|
||||
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
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)
|
||||
}
|
||||
};
|
||||
@@ -137,11 +144,11 @@ async fn decrypt_sync_room_event(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
|
||||
};
|
||||
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
|
||||
|
||||
use super::{decrypt_from_rooms, Context, E2EE};
|
||||
use super::{Context, E2EE, decrypt_from_rooms};
|
||||
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
|
||||
|
||||
#[async_test]
|
||||
@@ -192,11 +199,13 @@ mod tests {
|
||||
assert!(room.latest_encrypted_events().is_empty());
|
||||
assert!(room.latest_event().is_none());
|
||||
assert!(context.state_changes.room_infos.is_empty());
|
||||
assert!(!context
|
||||
.room_info_notable_updates
|
||||
.get(room_id)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
|
||||
assert!(
|
||||
!context
|
||||
.room_info_notable_updates
|
||||
.get(room_id)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user