Compare commits
711 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f92ccd8d40 | |||
| 2c2e0f1b15 | |||
| f314578df5 | |||
| 739289cd82 | |||
| 1d7d6c943b | |||
| 917f9ee298 | |||
| c7d44ddad3 | |||
| 1a170ddf90 | |||
| 696d1cacbe | |||
| c6d1cf20b6 | |||
| 38f34e66eb | |||
| 8fa411aaa0 | |||
| b4b3b5258c | |||
| fd6cb6647f | |||
| 103ff7d3ec | |||
| efa28a1ffd | |||
| c4640897d4 | |||
| 0487a143ed | |||
| 7ab97d59b8 | |||
| 940862ebed | |||
| e507eaabf6 | |||
| a2bbc33920 | |||
| db787c28b8 | |||
| a3d6114ddd | |||
| 58a139bf96 | |||
| 012bc03014 | |||
| 38adf952bd | |||
| 99d675d85a | |||
| be5eebaa35 | |||
| cf4d3cb492 | |||
| ad5241b2e5 | |||
| d083c56ccb | |||
| eb493de1b8 | |||
| 6aa4fdaed4 | |||
| 9c6bbd98a7 | |||
| 8b14a8920c | |||
| 98b36fe5e0 | |||
| a1f32d741f | |||
| ce01854078 | |||
| 7f8c9b0244 | |||
| 3621acb445 | |||
| 3789374951 | |||
| e54a36a1ca | |||
| cf95c3f4cc | |||
| 6ccfc92139 | |||
| 4ee20befa3 | |||
| 4348d9de23 | |||
| 477ee70e1c | |||
| 83989c030a | |||
| 717b6371f8 | |||
| 6fa832cfc8 | |||
| 1bff8cbe5d | |||
| 9bb67e8422 | |||
| c962a52f73 | |||
| 4d20b481c1 | |||
| a5136c7384 | |||
| 0e120549ce | |||
| 1e74c48351 | |||
| 9e242abda9 | |||
| 62ff895ce8 | |||
| d0d22ece78 | |||
| d49d86e8b0 | |||
| b226191bf4 | |||
| 21d7cd1b9d | |||
| b066260554 | |||
| dbc6e3c3aa | |||
| 3293c94451 | |||
| 53385798f6 | |||
| d49736455b | |||
| e278a99a20 | |||
| d5c7c886a1 | |||
| b9b40c0ef2 | |||
| 99d60bcc70 | |||
| 64d602141a | |||
| 59d2f8d4af | |||
| 80a253ada9 | |||
| ec5082bb9c | |||
| 63f5be6935 | |||
| dab87b7731 | |||
| 63f33a081f | |||
| 8b762b04df | |||
| a9ef675b1c | |||
| e69bb96144 | |||
| 63b8fc8b47 | |||
| d68879d0aa | |||
| 1afebd6d4e | |||
| db4c1f8dbd | |||
| c1b89a2751 | |||
| 9d39e4bae8 | |||
| 936f173e6f | |||
| 9eb4a5a24c | |||
| 49bba4d471 | |||
| f23deacec4 | |||
| 9c28ee7041 | |||
| 5d420b683f | |||
| 2d5b95e45e | |||
| 3d29db5ebb | |||
| 8e041d6958 | |||
| c02d0f1d01 | |||
| 2da262e56c | |||
| 31e4657718 | |||
| 5b2fe3cf47 | |||
| 49d7d4528d | |||
| 3d7e505530 | |||
| 56d4163982 | |||
| 4cafc48cc4 | |||
| 8ea55b30bc | |||
| d438ee2ca0 | |||
| a3fe9eef12 | |||
| 60056ba205 | |||
| ff62ed667c | |||
| 4168362912 | |||
| 96d63b7c8d | |||
| 260eaeea2b | |||
| f7442fa279 | |||
| da3cf3d54b | |||
| 872861e90e | |||
| 36cca4e444 | |||
| bb6202c76b | |||
| 5458a5afd2 | |||
| 45fa860b6b | |||
| 4ad25e32af | |||
| 423ebf9502 | |||
| 7fbb8cf9e5 | |||
| e0cac4eb7a | |||
| aa9cc67f32 | |||
| 7d6569ff27 | |||
| 4d7b19999b | |||
| d47306045f | |||
| ba461598b2 | |||
| 0233d03d4e | |||
| 725f74cbe8 | |||
| 98aceb40c2 | |||
| 34a35ad32a | |||
| 99971a2347 | |||
| b5f5a9c90e | |||
| ec4e228620 | |||
| a2c254d7da | |||
| 557c0698ba | |||
| 99a5568dbf | |||
| 94d8674709 | |||
| dfc4e03d5b | |||
| 14b8248e4b | |||
| 447c89aed4 | |||
| 894f94c383 | |||
| 1f3dea778b | |||
| b07069170f | |||
| f685feed40 | |||
| 610d4ca4e5 | |||
| 41edb08283 | |||
| 44aad14732 | |||
| b1fbf31c2e | |||
| 2bcab72dbb | |||
| 3bac5d250d | |||
| 4ec75b5b5c | |||
| c8c9a94995 | |||
| 08b80c533a | |||
| 0e679904e3 | |||
| 89c50c7ecd | |||
| d2857ee20a | |||
| c1274cea14 | |||
| 12cd1effdc | |||
| 3a14b73258 | |||
| d97f4ab8b3 | |||
| 3539487c49 | |||
| 526f35e3d3 | |||
| 6b4e2655cc | |||
| 30de5372eb | |||
| ba451b67c9 | |||
| a65fec3175 | |||
| 6298b5c176 | |||
| 0283ae14d9 | |||
| a1157d2c38 | |||
| fcce02fc11 | |||
| 7cea5be9e2 | |||
| 32e9626e0f | |||
| e376670af3 | |||
| 7f08793360 | |||
| 1fc7b34016 | |||
| 37447e16e5 | |||
| 782355b556 | |||
| eb51c862ce | |||
| 676c81ca80 | |||
| 644c5e8a4c | |||
| 34d5e6da2a | |||
| 5936a7285f | |||
| 62d58a21ab | |||
| 2e334fafa2 | |||
| e66967c46f | |||
| c237659aca | |||
| 8cb6f74996 | |||
| 0afd7c9528 | |||
| 388ced09a6 | |||
| 4071a55cd7 | |||
| c04a97f706 | |||
| 5b0c6bbafc | |||
| 1fb0f7f56a | |||
| ee9a05defe | |||
| c0f746f88f | |||
| 839786f810 | |||
| 4e4b508c38 | |||
| 313e634996 | |||
| ae72fcf663 | |||
| 12e9a2a159 | |||
| 16c1b9b57f | |||
| 06e6dd05c3 | |||
| 1cf84e203e | |||
| 9913cce946 | |||
| 1ee88b176c | |||
| f0193cc8ac | |||
| 5d2eab119d | |||
| ca91b6a278 | |||
| 260fec8750 | |||
| 950fcd289d | |||
| 2214aded9d | |||
| 02c41e20ad | |||
| 0e627b4ba2 | |||
| 55d2e6a740 | |||
| ee2ed6033e | |||
| fb97922a1c | |||
| 85b8c83d51 | |||
| 179f38b252 | |||
| 2c6409cb86 | |||
| 4d2f8bb82c | |||
| c0b26b6f25 | |||
| 8f96c32d56 | |||
| bdd0162831 | |||
| 752695c6ff | |||
| 552639763e | |||
| e65d4c44b4 | |||
| d151340882 | |||
| 27415dc64d | |||
| 4feeaf0cf5 | |||
| 87a2f8fab7 | |||
| 871cb2221b | |||
| d1d174d137 | |||
| bd7dca45ef | |||
| 856b2be992 | |||
| 161750f6d0 | |||
| c4a95f9bbe | |||
| ac5552807b | |||
| ff8b27f99b | |||
| bf9ffd7d09 | |||
| 9f18d76355 | |||
| 83c6a7fdf4 | |||
| 67fbba1e87 | |||
| 9c66221830 | |||
| f00d09597c | |||
| 339f4be49c | |||
| f5e3e7db83 | |||
| c3a0ce15cc | |||
| c708ed18c0 | |||
| 3db4767523 | |||
| 050fc3d845 | |||
| fd572d9b33 | |||
| 1dd413e4bc | |||
| fe45ba5cc2 | |||
| 46a2ba29a0 | |||
| e877466fdc | |||
| be47a6435b | |||
| 3bad26e39f | |||
| 0c3392d1d2 | |||
| 8dcbac3b16 | |||
| ec3657d707 | |||
| 1bf831fbb7 | |||
| 708ee63ffd | |||
| f6ff8621dc | |||
| 96b82a4f5d | |||
| 84bdf604e5 | |||
| 56b13806bd | |||
| b6edf826b0 | |||
| e335cac8f7 | |||
| cad9c42a7a | |||
| 907a23f252 | |||
| 0505edc380 | |||
| 47c7a205c7 | |||
| 7986a6627e | |||
| 0358552086 | |||
| 46462599c0 | |||
| 98a5b05c4b | |||
| 727af9e654 | |||
| 0d59b43dfd | |||
| d2a019a25f | |||
| cc4c296f74 | |||
| 61b3966cf9 | |||
| b31b743435 | |||
| 2a00401e79 | |||
| 39255c0542 | |||
| d9ed4f61af | |||
| 7beda7990f | |||
| ec975094e2 | |||
| cbfecf520c | |||
| 6151120621 | |||
| 84ddafbd6c | |||
| 57e9225152 | |||
| 36e49c2702 | |||
| 6bce60ed8f | |||
| 43005ba0c1 | |||
| 4540dd7eef | |||
| 91074ed3da | |||
| 3606dc1ed9 | |||
| 7f1d49f8f5 | |||
| 1afd4e9fa1 | |||
| b5153a8b23 | |||
| 9c4a47b20b | |||
| 43e468e149 | |||
| 993e02e02c | |||
| cd99520a6e | |||
| 69a1bd5019 | |||
| 1d87b33b79 | |||
| cbc7228e08 | |||
| affce2d43c | |||
| 29aa3c2e08 | |||
| 81bed18550 | |||
| 9082cb94a9 | |||
| fd806a9c11 | |||
| 7a26db66b5 | |||
| 635fdd3f80 | |||
| ba9e876dfe | |||
| bfe24d90cd | |||
| 60ce062ac5 | |||
| daa1120286 | |||
| 4ad046d4b8 | |||
| 33c156f231 | |||
| 9405908eb8 | |||
| 5742f49886 | |||
| cabaede7fc | |||
| a4bb2f3ff4 | |||
| 816530c89c | |||
| e72950ea15 | |||
| 793f62049a | |||
| 2652db661b | |||
| 2459348b2e | |||
| b85bb5463f | |||
| 7929bcc956 | |||
| a91d24c53e | |||
| e55db521a4 | |||
| c71e5c14c1 | |||
| 33f3a67020 | |||
| 290650c6b1 | |||
| 538afaf252 | |||
| 6a408d673f | |||
| 8a6f822993 | |||
| aa669585b4 | |||
| 109db5ee58 | |||
| abc6b4ad01 | |||
| 241584366d | |||
| 8a611305cf | |||
| 0c149155be | |||
| bc69d36d3e | |||
| a453f019d9 | |||
| f1a50bb68f | |||
| 9cae7ba44f | |||
| f00f6103c8 | |||
| d0980b6608 | |||
| 74c1044b7d | |||
| 14b21e2a9a | |||
| d6c666a88d | |||
| acda2e88e3 | |||
| b6c4bca5a0 | |||
| 3f7d53a3ce | |||
| a99e79ac5d | |||
| a191ab45ad | |||
| b5cf360111 | |||
| 56d5086aa4 | |||
| 6309498f20 | |||
| f4bee4b7a5 | |||
| 9a257a4ca3 | |||
| cca8b0898f | |||
| a0ce0cfaf2 | |||
| 627e118ef3 | |||
| c55935f92c | |||
| be1f525ccc | |||
| a48f23881d | |||
| 95aaa6558c | |||
| 375d3a4921 | |||
| 8b29fc8c08 | |||
| 50f97f3377 | |||
| 0ce36d0bfb | |||
| 9608aa5840 | |||
| 99c7e27fcf | |||
| 74fc96eec2 | |||
| ea74fda969 | |||
| 18ea1cafc7 | |||
| d58873f1c3 | |||
| f7d818a8ba | |||
| f62fbfc4ee | |||
| 260e227f24 | |||
| 791f4bc871 | |||
| f812afca21 | |||
| dd1c16a873 | |||
| 999d612a33 | |||
| f25af6be6b | |||
| af8236d708 | |||
| 0cdaf8d794 | |||
| 6b414b7791 | |||
| 4ec9124ce1 | |||
| b65e450813 | |||
| cee74d4965 | |||
| a845bdf26a | |||
| 40ff661358 | |||
| 9de2d70bad | |||
| 6d141c07bc | |||
| 608091fbe7 | |||
| 5e1c917459 | |||
| 3e5e6efb31 | |||
| 5902a857d9 | |||
| 168fd7232e | |||
| e17ca1071c | |||
| 99ac38eb20 | |||
| 698ffba88e | |||
| 23a312ed68 | |||
| 1f69fcb80b | |||
| deabb8b6e7 | |||
| d8cbb1b77b | |||
| 7c16d673fb | |||
| e9fbbb52d5 | |||
| c05df39132 | |||
| d572d201c9 | |||
| 39aa777b9b | |||
| f0ae0e53f9 | |||
| 7ec331c842 | |||
| 8472b5504e | |||
| 2ca717b4af | |||
| c864fac823 | |||
| c62da4a026 | |||
| 471e045ea3 | |||
| 29ca03bb81 | |||
| 29e3b7766d | |||
| 2dd43fc9de | |||
| 7fbc3e78e9 | |||
| ac6ccd3384 | |||
| f49784a15e | |||
| b0aadd1574 | |||
| 37ae2af67e | |||
| 4d64f3885a | |||
| 436c598da5 | |||
| 034667cf3f | |||
| 5f867ee982 | |||
| 8521b7b65b | |||
| eca633b1cf | |||
| 6c769d1d33 | |||
| 862a0e6f57 | |||
| 2083e20592 | |||
| e61e86c3b4 | |||
| 17a9ab41e4 | |||
| 007eb15bce | |||
| d37d65614e | |||
| 38afc0b1cd | |||
| 00bfffed99 | |||
| 964f6c8638 | |||
| df7823f1cf | |||
| 3457b5fa79 | |||
| 21e8138805 | |||
| 20d1087658 | |||
| 1edbad0bd8 | |||
| 1825cd5816 | |||
| 97e2b1c1b2 | |||
| 02d0298b66 | |||
| 6fd4988849 | |||
| ed319eed64 | |||
| 359f50f368 | |||
| c6bf11e836 | |||
| 3aec150697 | |||
| ddd07443f1 | |||
| 8dbb6e5c1d | |||
| cdd40d0308 | |||
| e9f398472e | |||
| 88eb4a0da9 | |||
| 77dddf2540 | |||
| 2cf8ce2a7d | |||
| 12b1102ca9 | |||
| 81286ad1e7 | |||
| 280eae0b71 | |||
| bc88e96e62 | |||
| 37708d509c | |||
| 619ded3147 | |||
| 30de09aef6 | |||
| 0b6d00cfad | |||
| 029148ef6e | |||
| 8d38f6109e | |||
| 75c5528942 | |||
| dd5cb220a0 | |||
| 4b23378d29 | |||
| 66c97dc02f | |||
| a2f77c79bc | |||
| 2f791d19e3 | |||
| 066dd4aa21 | |||
| a4c3a4eb87 | |||
| c23dd1ec0a | |||
| 80ac2b8c38 | |||
| 617c646a52 | |||
| 29b506f301 | |||
| cf84f2ff18 | |||
| 949bdb5bb3 | |||
| cc8d6d8482 | |||
| 779d6a0925 | |||
| 1fdebd7d56 | |||
| 39c0ee1f9b | |||
| 1542a5b79e | |||
| ae53b62762 | |||
| 4576013878 | |||
| 73028a834e | |||
| 8c4f5c60b7 | |||
| aaeab050da | |||
| 6b120505b8 | |||
| a791243202 | |||
| c5b7dbd0c0 | |||
| 16e09dfefa | |||
| 2d13a682a2 | |||
| c50bab4847 | |||
| 768ded4b90 | |||
| 19ffa2fe0e | |||
| 3a57b547da | |||
| c3e238d2d6 | |||
| c257525b5a | |||
| 909dbd7b78 | |||
| 6c9892ee84 | |||
| fe4381d38c | |||
| ea41e03ca3 | |||
| 9fb54074ee | |||
| 597f66f71a | |||
| 44c71eea1b | |||
| d291be4235 | |||
| 1342daf381 | |||
| 07f9e998e9 | |||
| a9f946d365 | |||
| 248b65d2d5 | |||
| 500a637bc4 | |||
| 7da0d903e5 | |||
| 3cf4faec83 | |||
| 2cfd53e64d | |||
| bbdfe7b38f | |||
| b4977bbe5d | |||
| 4bc4263e1c | |||
| ef734e0876 | |||
| a2881d5aca | |||
| fadfd98bee | |||
| 640fa4854f | |||
| a90cf28d3c | |||
| aa11e9f062 | |||
| 4d9268d104 | |||
| b7b96d49c5 | |||
| 37823df753 | |||
| 096dd59c07 | |||
| 13fb3e76c5 | |||
| 8e614c23eb | |||
| f324191c7f | |||
| dc66994d66 | |||
| 8ac5d42fbe | |||
| 8f14ee976a | |||
| da92379e22 | |||
| 0cbe48e986 | |||
| 99bfb94d6d | |||
| 3aa424117c | |||
| 4160192709 | |||
| 6bc09cbbd9 | |||
| 6c18f73b51 | |||
| a8e4630f56 | |||
| 45d16919a0 | |||
| 9fc10e90a3 | |||
| d00aa2ccd2 | |||
| 79155bb59b | |||
| 2f26e346ed | |||
| 2d32096534 | |||
| 73814c5be0 | |||
| e84e83c716 | |||
| 0edff99390 | |||
| cdd7be13f1 | |||
| 9729e633f2 | |||
| f796232220 | |||
| 13d46e6aeb | |||
| 6c75571c38 | |||
| ec719bbb6b | |||
| f3cb3c7317 | |||
| eab7dd02d2 | |||
| ed8d75289e | |||
| 540abe2a7c | |||
| c017ce0928 | |||
| 747b5db764 | |||
| 5d18820120 | |||
| 9870228a76 | |||
| 3c8666ae2c | |||
| 3783f4925e | |||
| 1667db12ec | |||
| 8c114ad57e | |||
| 862126800d | |||
| b1e4e16f8e | |||
| f283126e6e | |||
| fc70a7da2c | |||
| aff26b1ed9 | |||
| fd5c1d847e | |||
| 956a5d46f1 | |||
| 1dbd2caeb2 | |||
| 4257649933 | |||
| c996307265 | |||
| fc97b04ec8 | |||
| e1abe99ad0 | |||
| 07f894c2a5 | |||
| 11f2bf0ad6 | |||
| 54e4cb2d10 | |||
| 855bdadbad | |||
| 287049c719 | |||
| 12e377d296 | |||
| 4f7f4a054f | |||
| c18b1cfab9 | |||
| d6b6eb9e31 | |||
| 52981a839a | |||
| 50b1e3baf2 | |||
| 3169c65bf2 | |||
| 1b724f288f | |||
| def89d2661 | |||
| d0cce0eafe | |||
| 901e280d5c | |||
| a44dc4b70c | |||
| 7d474c1415 | |||
| f95d174d9a | |||
| 4fe4e9cb7c | |||
| 209595e66e | |||
| 7e69880148 | |||
| ad1e93473d | |||
| 449fbd3ad4 | |||
| c3e3cdabf8 | |||
| 258e520cee | |||
| 17622911d4 | |||
| 19c22a1883 | |||
| 188d3a2ead | |||
| 4ec7d99d71 | |||
| 01b21bd5e8 | |||
| a6663718d0 | |||
| 72c6dc8e08 | |||
| 961edaf4b9 | |||
| ea0a81989a | |||
| b68c22bd56 | |||
| 130e3ad04b | |||
| 9252c72dea | |||
| 6d2f787623 | |||
| e3042e664b | |||
| e401ece78f | |||
| e79654c80b | |||
| 0777425a73 | |||
| e83d085e77 | |||
| 026acf7f45 | |||
| ed4454fcbb | |||
| 3749c9c616 | |||
| b95b0ba1c4 | |||
| 4c6d5f9654 | |||
| 6b99f37268 | |||
| 818d0ef233 | |||
| 2022017b29 | |||
| 3c13b3edd4 | |||
| 3bd3ba7aff | |||
| 9c6a3d6cf4 | |||
| 86ae50d1bd | |||
| 347d348bf4 | |||
| 079c8175f2 | |||
| 64163c32f6 | |||
| bcb84cc27c | |||
| e299adbdeb | |||
| 2ab1d8a985 | |||
| d71387e7f7 | |||
| fa638ae630 | |||
| 4965800a0a | |||
| f7b55bfb5c | |||
| 1fdf23a456 | |||
| 5c1ee281a9 | |||
| f4b401ac58 | |||
| 47e4c44d1b | |||
| 1ba8704941 | |||
| 7f13f86912 | |||
| 622825f099 | |||
| 16e50ee484 | |||
| 7dc76fa423 | |||
| db3308981b | |||
| e5cf097e32 | |||
| 3ac2339475 | |||
| 9b4e60d075 | |||
| 66ea5d990d | |||
| ea40bf35b8 | |||
| 4ca1e45271 | |||
| 771de205b3 | |||
| 07fe503cd4 | |||
| deeaa862d8 | |||
| 91ae6a1eab | |||
| 86480cb37a | |||
| feed4f725b | |||
| e7d2c729c2 | |||
| 75d39c8186 | |||
| 726e5c95b1 | |||
| 19572c9712 | |||
| ce1ec8bab3 | |||
| 1833e9185d | |||
| d7c6af9677 | |||
| 26d76d128a | |||
| b35bea6c2b | |||
| f69e154e30 | |||
| 4d3e7f7336 | |||
| ee1eb99134 | |||
| 70665a84aa | |||
| c6b58b0e2b | |||
| ce0bba5e6d | |||
| 87787dc04e | |||
| a12f659653 | |||
| f4da1fa582 | |||
| 7fbb2ddae1 | |||
| 1e0c930a12 | |||
| 0715da0626 | |||
| b1d878ddc2 | |||
| 13b07c3ece | |||
| 00e58056a4 | |||
| e1e86a78c9 |
@@ -11,3 +11,6 @@ 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"]
|
||||
|
||||
[env]
|
||||
IPHONEOS_DEPLOYMENT_TARGET = "16.0"
|
||||
|
||||
@@ -5,3 +5,5 @@ updates:
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- description of the changes in this PR -->
|
||||
|
||||
- [ ] Public API changes documented in changelogs (optional)
|
||||
- [ ] I've documented the public API Changes in the appropriate `CHANGELOG.md` files.
|
||||
- [ ] This PR was made with the help of AI.
|
||||
|
||||
<!-- Sign-off, if not part of the commits -->
|
||||
<!-- See CONTRIBUTING.md if you don't know what this is -->
|
||||
|
||||
@@ -11,6 +11,9 @@ jobs:
|
||||
benchmarks:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
benchmark:
|
||||
@@ -79,10 +82,12 @@ jobs:
|
||||
echo "Disk space after cleanup"
|
||||
df -h
|
||||
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670
|
||||
uses: moonrepo/setup-rust@abb2d32350334249b178c401e5ec5836e0cd88d3
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
@@ -92,8 +97,7 @@ jobs:
|
||||
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f
|
||||
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
mode: "instrumentation"
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
mode: simulation
|
||||
|
||||
@@ -12,6 +12,8 @@ on:
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -31,15 +33,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -47,12 +53,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -69,32 +75,37 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Rust SDK
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout Kotlin Rust Components project
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-rust-components-kotlin
|
||||
path: rust-components-kotlin
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
|
||||
- name: Install android sdk
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.7
|
||||
uses: malinskiy/action-android/install-sdk@fa103ef30331e95f266418a6a97e98f61f626887 # release/0.1.7
|
||||
|
||||
- name: Install android ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
|
||||
id: install-ndk
|
||||
with:
|
||||
ndk-version: r27
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -102,12 +113,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -136,16 +147,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install aarch64-apple-ios target
|
||||
run: rustup target install aarch64-apple-ios
|
||||
@@ -156,12 +171,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
@@ -179,7 +194,7 @@ jobs:
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
|
||||
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@399a1deeab0d7e4fa9604cbe83b1df6058c40193 # main
|
||||
with:
|
||||
use_rust_sdk: "." # use local checkout
|
||||
use_complement_crypto: "MATCHING_BRANCH"
|
||||
@@ -191,16 +206,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Add rust targets
|
||||
run: |
|
||||
@@ -212,7 +231,7 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
|
||||
+77
-40
@@ -7,6 +7,8 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -35,7 +37,6 @@ jobs:
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- experimental-encrypted-state-events
|
||||
- rustls-tls
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
@@ -43,10 +44,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -54,7 +59,7 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -65,10 +70,12 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -85,21 +92,27 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -116,7 +129,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -124,20 +139,23 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -169,10 +187,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
@@ -183,17 +203,19 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -241,22 +263,25 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: wasm32-unknown-unknown
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.3
|
||||
uses: qmaru/wasm-pack-action@785fe709cd17eb6a97607eda9b6f5dbebed2b89c # v0.5.3
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.13.1
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -267,10 +292,12 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -291,10 +318,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.43.0
|
||||
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -303,26 +332,28 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: nightly-2025-10-01
|
||||
toolchain: nightly-2026-02-26
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -362,7 +393,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -370,15 +403,19 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
@@ -390,7 +427,7 @@ jobs:
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
|
||||
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3
|
||||
with:
|
||||
files: ./target/nextest/ci/junit.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -24,6 +26,9 @@ jobs:
|
||||
name: Code Coverage
|
||||
needs: xtask
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
@@ -97,9 +102,10 @@ jobs:
|
||||
df -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -109,7 +115,9 @@ jobs:
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -117,19 +125,18 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
prefix-key: "coverage"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Install nextest and llvm-cov
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest,cargo-llvm-cov
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -148,35 +155,14 @@ jobs:
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
|
||||
- name: Get PR number and commit SHA
|
||||
run: |
|
||||
echo "Storing PR number ${{ github.event.number }}"
|
||||
echo "${{ github.event.number }}" > pr_number.txt
|
||||
|
||||
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
|
||||
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
|
||||
|
||||
- name: Move the JUnit file into the root directory
|
||||
shell: bash
|
||||
run: |
|
||||
mv target/nextest/ci/junit.xml ./junit.xml
|
||||
|
||||
# This stores the coverage report and metadata in artifacts.
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
- name: Store coverage report in artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
coverage.xml
|
||||
junit.xml
|
||||
pr_number.txt
|
||||
commit_sha.txt
|
||||
if-no-files-found: error
|
||||
use_oidc: true
|
||||
|
||||
- run: |
|
||||
echo 'The coverage report was stored in Github artifacts.'
|
||||
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
|
||||
with:
|
||||
use_oidc: true
|
||||
report_type: "test_results"
|
||||
|
||||
@@ -6,9 +6,14 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
|
||||
|
||||
@@ -8,6 +8,8 @@ on:
|
||||
pull_request: # focus on the changed files in current PR
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -17,10 +19,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v47.0.1
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
|
||||
@@ -2,11 +2,15 @@ name: Detects unused dependencies
|
||||
on:
|
||||
pull_request: { branches: "*" }
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@78beac95c8fd7c25bdfb194415128523e41512d5
|
||||
uses: bnjbvr/cargo-machete@ac30a525c0a8d163a92d727b3ff079ee3f6ecb08
|
||||
|
||||
@@ -21,25 +21,27 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: nightly-2025-10-01
|
||||
toolchain: nightly-2026-02-26
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
@@ -52,11 +54,11 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
|
||||
@@ -2,11 +2,15 @@ name: Git Checks
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
block-fixup:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Block Fixup Commit Merge
|
||||
uses: 13rac1/block-fixup-merge-action@v2.0.0
|
||||
uses: 13rac1/block-fixup-merge-action@bd5504fb9ca0253e109d98eb86b7debc01970cdc # v2.0.0
|
||||
|
||||
@@ -7,10 +7,16 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # cargo-hack
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
|
||||
name: Upload code coverage
|
||||
|
||||
on:
|
||||
# This workflow is triggered after every successful execution
|
||||
# of `coverage` workflow.
|
||||
workflow_run:
|
||||
workflows: ["Code Coverage"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Upload coverage report
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: 'Fetch coverage report from artifacts'
|
||||
id: prepare_report
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
|
||||
// List artifacts of the workflow run that triggered this workflow
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "codecov_report";
|
||||
});
|
||||
|
||||
if (codecovReport.length != 1) {
|
||||
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
|
||||
}
|
||||
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: codecovReport[0].id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
|
||||
|
||||
- id: parse_previous_artifacts
|
||||
run: |
|
||||
unzip codecov_report.zip
|
||||
|
||||
echo "Detected PR is: $(<pr_number.txt)"
|
||||
echo "Detected commit_sha is: $(<commit_sha.txt)"
|
||||
|
||||
# Make the params available as step output
|
||||
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
path: repo_root
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
@@ -20,6 +20,8 @@ on:
|
||||
description: "The cache key for the macos build artifact"
|
||||
value: "${{ jobs.xtask.outputs.cachekey-macos }}"
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -43,7 +45,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Calculate cache key
|
||||
id: cachekey
|
||||
@@ -53,7 +57,7 @@ jobs:
|
||||
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check xtask cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
@@ -64,7 +68,9 @@ jobs:
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Lint GHA workflows with zizmor
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
+1
-1
@@ -7,4 +7,4 @@ group_imports = "StdExternalCrate"
|
||||
format_code_in_doc_comments = true
|
||||
doc_comment_code_block_width = 80
|
||||
# Workaround for https://github.com/rust-lang/rust.vim/issues/464
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
severity: high
|
||||
+15
-25
@@ -81,10 +81,10 @@ contributors' pull requests and pushes.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
each commit should have a *good commit message*.
|
||||
Ideally, a PR should have a _proper title_, with _atomic logical commits_, and
|
||||
each commit should have a _good commit message_.
|
||||
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR,
|
||||
A _proper PR title_ would be a one-liner summary of the changes in the PR,
|
||||
following the same guidelines of a good commit message, including the
|
||||
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
|
||||
be a good PR title.
|
||||
@@ -126,10 +126,10 @@ A good example of a changelog entry could look like the following:
|
||||
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
|
||||
- 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
|
||||
- GitHub Advisory Link: Provide a link to the corresponding GitHub security
|
||||
advisory for further context.
|
||||
|
||||
```markdown
|
||||
@@ -156,12 +156,12 @@ Conventional Commits are structured as follows:
|
||||
The type of changes which will be included in changelogs is one of the
|
||||
following:
|
||||
|
||||
* `feat`: A new feature
|
||||
* `fix`: A bugfix
|
||||
* `doc`: Documentation changes
|
||||
* `refactor`: Code refactoring
|
||||
* `perf`: Performance improvements
|
||||
* `ci`: Changes to CI configuration files and scripts
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bugfix
|
||||
- `doc`: Documentation changes
|
||||
- `refactor`: Code refactoring
|
||||
- `perf`: Performance improvements
|
||||
- `ci`: Changes to CI configuration files and scripts
|
||||
|
||||
The scope is optional and can specify the area of the codebase affected (e.g.,
|
||||
olm, cipher).
|
||||
@@ -174,10 +174,10 @@ changelog entry.
|
||||
|
||||
The metadata must be included in the following git-trailers:
|
||||
|
||||
* `Security-Impact`: The magnitude of harm that can be expected, i.e.
|
||||
- `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.
|
||||
- `CVE`: The CVE that was assigned to this issue.
|
||||
- `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
|
||||
Please include all the fields that are available.
|
||||
|
||||
@@ -334,16 +334,6 @@ on Git 2.17+ you can mass signoff using rebase:
|
||||
git rebase --signoff origin/main
|
||||
```
|
||||
|
||||
## Tips for working on the `matrix-rust-sdk` with specific IDEs
|
||||
|
||||
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
|
||||
with all features enabled, causing an error in `matrix-sdk` ("only one of the
|
||||
features `native-tls` or `rustls-tls` can be enabled"). To work around this,
|
||||
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
|
||||
`native-tls` or `rustls-tls` feature definitions:
|
||||
|
||||

|
||||
|
||||
## AI policy
|
||||
|
||||
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
|
||||
|
||||
Generated
+819
-484
File diff suppressed because it is too large
Load Diff
+17
-15
@@ -11,12 +11,12 @@ members = [
|
||||
"xtask",
|
||||
]
|
||||
exclude = ["testing/data"]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
# xtask, multiverse, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.93"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.100", default-features = false }
|
||||
@@ -28,7 +28,7 @@ assert_matches2 = { version = "0.1.2", default-features = false }
|
||||
async_cell = { version = "0.2.3", default-features = false }
|
||||
async-compat = { version = "0.2.5", default-features = false }
|
||||
async-once-cell = { version = "0.5.4", default-features = false }
|
||||
async-rx = { version = "0.1.3", default-features = false }
|
||||
async-rx = { version = "0.2.0", default-features = false }
|
||||
# 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 = { version = "0.3.6", default-features = false }
|
||||
@@ -46,7 +46,7 @@ eyeball-im-util = { version = "0.10.0", default-features = false }
|
||||
futures-core = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
futures-executor = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
getrandom = { version = "0.2.15", default-features = false }
|
||||
getrandom = { version = "0.4.2", default-features = false }
|
||||
gloo-timers = { version = "0.3.0", default-features = false }
|
||||
gloo-utils = { version = "0.2.0", default-features = false, features = ["serde"] }
|
||||
growable-bloom-filter = { version = "2.1.1", default-features = false }
|
||||
@@ -60,19 +60,20 @@ insta = { version = "1.44.1", features = ["json", "redactions"] }
|
||||
itertools = { version = "0.14.0", default-features = false, features = ["use_std"] }
|
||||
js-sys = { version = "0.3.82", default-features = false, features = ["std"] }
|
||||
mime = { version = "0.3.17", default-features = false }
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
|
||||
once_cell = { version = "1.21.3", default-features = false }
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["timing-resistant-secret-traits"] }
|
||||
oauth2-reqwest = { version = "0.1.0-alpha.3", default-features = false }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
pin-project-lite = { version = "0.2.16", default-features = false }
|
||||
proc-macro2 = { version = "1.0.106", default-features = false }
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
proptest = { version = "1.9.0", default-features = false, features = ["std"] }
|
||||
quote = { version = "1.0.37", default-features = false }
|
||||
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
|
||||
rand = { version = "0.10.1", default-features = false, features = ["std", "std_rng", "thread_rng"] }
|
||||
regex = { version = "1.12.2", default-features = false }
|
||||
reqwest = { version = "0.12.24", default-features = false }
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rmp-serde = { version = "1.3.0", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c15801c80b683b67dc", features = [
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "7680eebd9586669e1a4e5b1fd1c2c691221369d4", features = [
|
||||
"client-api-c",
|
||||
"compat-unset-avatar",
|
||||
"compat-upload-signatures",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
@@ -81,6 +82,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c158
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3230",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3417",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
@@ -93,8 +95,8 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c158
|
||||
"unstable-msc4308",
|
||||
"unstable-msc4310",
|
||||
] }
|
||||
sentry = { version = "0.46.0", default-features = false }
|
||||
sentry-tracing = { version = "0.46.0", default-features = false }
|
||||
sentry = { version = "0.47.0", default-features = false }
|
||||
sentry-tracing = { version = "0.47.0", default-features = false }
|
||||
serde = { version = "1.0.228", default-features = false, features = ["std", "rc", "derive"] }
|
||||
serde_html_form = { version = "0.2.8", default-features = false }
|
||||
serde_json = { version = "1.0.145", default-features = false, features = ["std"] }
|
||||
@@ -117,7 +119,7 @@ uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["ca
|
||||
url = { version = "2.5.7", default-features = false }
|
||||
uuid = { version = "1.18.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
vodozemac = { version = "0.9.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption"] }
|
||||
vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] }
|
||||
wasm-bindgen = { version = "0.2.105", default-features = false }
|
||||
wasm-bindgen-test = { version = "0.3.55", default-features = false, features = ["std"] }
|
||||
web-sys = { version = "0.3.82", default-features = false }
|
||||
@@ -230,7 +232,7 @@ 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" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "e0b317a9a7bde2d48a7d15b6a60d70e4a41d3b5f" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
tracing-core = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
|
||||
|
||||
@@ -17,7 +17,7 @@ codspeed = []
|
||||
assert_matches.workspace = true
|
||||
criterion = { version = "4.2.1", 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 = { workspace = true, features = ["e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
|
||||
@@ -3,14 +3,15 @@ use std::{pin::Pin, sync::Arc};
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{
|
||||
RoomInfo, RoomState, SqliteEventCacheStore, StateStore,
|
||||
cross_process_lock::CrossProcessLockConfig,
|
||||
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 matrix_sdk_test::{ALICE, base64_sha256_hash, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventId, RoomId, event_id,
|
||||
OwnedRoomId, RoomId,
|
||||
events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
|
||||
room_id,
|
||||
};
|
||||
@@ -39,14 +40,21 @@ fn handle_room_updates(c: &mut Criterion) {
|
||||
let mut changes = matrix_sdk::StateChanges::default();
|
||||
|
||||
for i in 0..num_rooms {
|
||||
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap();
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!firstbatchroom{i:04}:example.com");
|
||||
|
||||
let room_id = if i % 10 == 9 {
|
||||
// Make 1 in 10 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).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();
|
||||
let event = event_factory.text_msg(format!("Message {j}")).into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(room_id.clone(), joined_room_update);
|
||||
@@ -102,9 +110,11 @@ fn handle_room_updates(c: &mut Criterion) {
|
||||
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()),
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store.clone()),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
@@ -170,8 +180,10 @@ fn find_event_relations(c: &mut Criterion) {
|
||||
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");
|
||||
// Room v1-v11 ID.
|
||||
let room_id = room_id!("!initialtestingroom:ben.ch");
|
||||
// Room v12 ID.
|
||||
let other_room_id = room_id!("!ICMDdumUm6RRX_eWYY2wMb2w0CY0Z_5OvlY2gBR6ELc");
|
||||
|
||||
// Make the state store aware of the room, so that `client.get_room()` works
|
||||
// with it.
|
||||
@@ -193,43 +205,37 @@ fn find_event_relations(c: &mut Criterion) {
|
||||
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();
|
||||
let target_event = event_factory.text_msg("hello world").into_event();
|
||||
let target_event_id =
|
||||
{ &target_event.event_id().expect("generated event has an event ID") };
|
||||
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();
|
||||
let event = event_factory.text_msg(format!("unrelated message {i}")).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();
|
||||
let other_target_event = event_factory.text_msg("hello world").into_event();
|
||||
let other_target_event_id =
|
||||
other_target_event.event_id().expect("generated event has an event ID");
|
||||
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();
|
||||
for _i in 0..NUM_OTHER_EVENTS {
|
||||
let event = event_factory.reaction(&other_target_event_id, "👍").into();
|
||||
joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
|
||||
@@ -239,8 +245,7 @@ fn find_event_relations(c: &mut Criterion) {
|
||||
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();
|
||||
let event = event_factory.text_msg(format!("hi {i}")).into();
|
||||
other_joined_room_update.timeline.events.push(event);
|
||||
}
|
||||
room_updates.joined.insert(other_room_id.to_owned(), other_joined_room_update);
|
||||
@@ -268,9 +273,11 @@ fn find_event_relations(c: &mut Criterion) {
|
||||
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),
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(state_store.clone())
|
||||
.event_cache_store(event_cache_store),
|
||||
)
|
||||
})
|
||||
.build()
|
||||
@@ -319,7 +326,7 @@ fn find_event_relations(c: &mut Criterion) {
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
|
||||
assert_eq!(target.event_id().unwrap(), *target_event_id);
|
||||
assert_eq!(relations.len(), num_related_events as usize);
|
||||
},
|
||||
criterion::BatchSize::PerIteration,
|
||||
|
||||
@@ -10,7 +10,7 @@ use matrix_sdk_base::event_cache::{
|
||||
store::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, IntoEventCacheStore, MemoryStore},
|
||||
};
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::{EventId, room_id};
|
||||
use ruma::room_id;
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
@@ -33,7 +33,7 @@ fn writing(c: &mut Criterion) {
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let room_id = room_id!("!fabricandofitfaber:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
@@ -66,12 +66,7 @@ fn writing(c: &mut Criterion) {
|
||||
|
||||
{
|
||||
let mut events = (0..number_of_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.map(|nth| event_factory.text_msg(format!("foo {nth}")).into_event())
|
||||
.peekable();
|
||||
|
||||
let mut gap_nth = 0;
|
||||
@@ -88,9 +83,8 @@ fn writing(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
{
|
||||
operations.push(Operation::PushGapBack(Gap {
|
||||
prev_token: format!("gap{gap_nth}"),
|
||||
}));
|
||||
operations
|
||||
.push(Operation::PushGapBack(Gap { token: format!("gap{gap_nth}") }));
|
||||
gap_nth += 1;
|
||||
}
|
||||
}
|
||||
@@ -150,7 +144,7 @@ fn reading(c: &mut Criterion) {
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let room_id = room_id!("!fabricandofitfaber:bar.baz");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
@@ -178,12 +172,7 @@ fn reading(c: &mut Criterion) {
|
||||
// Store some events and gap chunks in the store.
|
||||
{
|
||||
let mut events = (0..num_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.map(|nth| event_factory.text_msg(format!("foo {nth}")).into_event())
|
||||
.peekable();
|
||||
|
||||
let mut lc =
|
||||
@@ -198,7 +187,7 @@ fn reading(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
lc.push_items_back(events_chunk);
|
||||
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
|
||||
lc.push_gap_back(Gap { token: format!("gap{num_gaps}") });
|
||||
|
||||
num_gaps += 1;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk::{
|
||||
cross_process_lock::CrossProcessLockConfig, store::RoomLoadSettings,
|
||||
test_utils::mocks::MatrixMockServer,
|
||||
};
|
||||
use matrix_sdk_base::{
|
||||
BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore, ThreadingSupport,
|
||||
store::StoreConfig,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, base64_sha256_hash, 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,
|
||||
mxc_uri, owned_device_id, owned_room_id, owned_user_id,
|
||||
serde::Raw,
|
||||
user_id,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
use wiremock::{Request, ResponseTemplate};
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
let room_id = owned_room_id!("!homohominilupusest:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
@@ -58,16 +58,18 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(sqlite_store),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
runtime
|
||||
.block_on(base_client.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
user_id: owned_user_id!("@somebody:example.com"),
|
||||
device_id: owned_device_id!("DEVICE_ID"),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
@@ -103,32 +105,22 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
const PINNED_EVENTS_COUNT: usize = 100;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
let room_id = owned_room_id!("!homohominilupusest:example.com");
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id).sender(&sender_id);
|
||||
|
||||
let mut joined_room_builder =
|
||||
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
|
||||
JoinedRoomBuilder::new(&room_id).add_state_event(f.room_encryption());
|
||||
|
||||
let pinned_event_ids: Vec<OwnedEventId> = (0..PINNED_EVENTS_COUNT)
|
||||
.map(|i| EventId::parse(format!("${i}")).expect("Invalid event id"))
|
||||
.map(|i| {
|
||||
EventId::new_v2_or_v3(&base64_sha256_hash(format!("${i}").as_bytes()))
|
||||
.expect("Invalid event id")
|
||||
})
|
||||
.collect();
|
||||
joined_room_builder = joined_room_builder.add_state_event(StateTestEvent::Custom(json!(
|
||||
{
|
||||
"content": {
|
||||
"pinned": pinned_event_ids
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 151393755,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.pinned_events",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
}
|
||||
)));
|
||||
joined_room_builder =
|
||||
joined_room_builder.add_state_bulk(vec![f.room_pinned_events(pinned_event_ids).into()]);
|
||||
|
||||
let (server, client, room) = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
|
||||
@@ -2,12 +2,12 @@ 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_test::{JoinedRoomBuilder, base64_sha256_hash, 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 rand::{distr::Uniform, prelude::Distribution};
|
||||
use ruma::{OwnedRoomId, RoomId, owned_user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a room list.
|
||||
@@ -26,19 +26,30 @@ pub fn create(c: &mut Criterion) {
|
||||
});
|
||||
|
||||
let sender_id = owned_user_id!("@mnt_io:matrix.org");
|
||||
let mut rand = rand::thread_rng();
|
||||
let server_ts_range = Uniform::from(100..1000);
|
||||
let mut rand = rand::rng();
|
||||
let server_ts_range = Uniform::try_from(100..1000).unwrap();
|
||||
|
||||
for room_nth in 0..NUMBER_OF_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!r{room_nth}")).unwrap();
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!arsgratiaartis{room_nth:04}:example.com");
|
||||
|
||||
let room_id = if room_nth % 10 == 9 {
|
||||
// Make 1 in 10 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).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()
|
||||
event_factory
|
||||
.text_msg(format!("a {room_nth}_{event_nth}"))
|
||||
.sender(&sender_id)
|
||||
.into_raw_sync()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_m
|
||||
use matrix_sdk::{
|
||||
Client, RoomInfo, RoomState, SessionTokens, StateChanges,
|
||||
authentication::matrix::MatrixSession, config::StoreConfig,
|
||||
cross_process_lock::CrossProcessLockConfig,
|
||||
};
|
||||
use matrix_sdk_base::{SessionMeta, StateStore as _, store::MemoryStore};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use ruma::{RoomId, device_id, user_id};
|
||||
use matrix_sdk_test::base64_sha256_hash;
|
||||
use ruma::{OwnedRoomId, RoomId, owned_device_id, owned_user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Number of joined rooms in the benchmark.
|
||||
@@ -23,19 +25,37 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
let mut changes = StateChanges::default();
|
||||
|
||||
for i in 0..NUM_JOINED_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap().to_owned();
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!joinedchamber{i:05}:example.com");
|
||||
|
||||
let room_id = if i % 20 == 19 {
|
||||
// Make 1 in 20 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).unwrap()
|
||||
};
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
}
|
||||
|
||||
for i in 0..NUM_STRIPPED_JOINED_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!strippedroom{i}:example.com")).unwrap().to_owned();
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
let raw_room_id = format!("!strippedlodge{i:05}:example.com");
|
||||
|
||||
let room_id = if i % 20 == 19 {
|
||||
// Make 1 in 20 rooms use a room v12 ID, which is a base64 hash similar to an
|
||||
// event ID.
|
||||
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
|
||||
} else {
|
||||
OwnedRoomId::try_from(raw_room_id).unwrap()
|
||||
};
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Invited));
|
||||
}
|
||||
|
||||
let session = MatrixSession {
|
||||
meta: SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
user_id: owned_user_id!("@somebody:example.com"),
|
||||
device_id: owned_device_id!("DEVICE_ID"),
|
||||
},
|
||||
tokens: SessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
};
|
||||
@@ -54,8 +74,10 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
@@ -84,8 +106,10 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
let client = Client::builder()
|
||||
.homeserver_url("https://matrix.example.com")
|
||||
.store_config(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(store.clone()),
|
||||
StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
"cross-process-store-locks-holder-name",
|
||||
))
|
||||
.state_store(store.clone()),
|
||||
)
|
||||
.build()
|
||||
.await
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineReadReceiptTracking};
|
||||
use ruma::{
|
||||
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
|
||||
OwnedEventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
|
||||
owned_user_id,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
@@ -18,7 +18,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
const NUM_EVENTS: usize = 10000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
let room_id = owned_room_id!("!fortesfortunajuvat:example.com");
|
||||
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
let other_sender_id = owned_user_id!("@other_sender:example.com");
|
||||
@@ -35,51 +35,49 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
_ => unreachable!("math genius over here"),
|
||||
};
|
||||
|
||||
let event_id = EventId::parse(format!("$event{i}")).unwrap();
|
||||
|
||||
let j = i % 10;
|
||||
if j < 6 {
|
||||
// Messages.
|
||||
events.push(
|
||||
f.text_msg(format!("Message {i}"))
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
events.push(f.text_msg(format!("Message {i}")).sender(sender).into_raw_sync());
|
||||
} else if j < 8 {
|
||||
// Reactions.
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 2)).unwrap();
|
||||
events.push(
|
||||
f.reaction(&prev_event, "👍").sender(sender).event_id(&event_id).into_raw_sync(),
|
||||
);
|
||||
let prev_event_id = events[i - 2]
|
||||
.get_field::<OwnedEventId>("event_id")
|
||||
.expect("invalid event ID")
|
||||
.expect("missing event ID");
|
||||
events.push(f.reaction(&prev_event_id, "👍").sender(sender).into_raw_sync());
|
||||
} else if j == 8 {
|
||||
// Edit.
|
||||
// Note: (i-3)%3 is the same as i%3 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 3)).unwrap();
|
||||
let prev_event_id = events[i - 3]
|
||||
.get_field::<OwnedEventId>("event_id")
|
||||
.expect("invalid event ID")
|
||||
.expect("missing event ID");
|
||||
events.push(
|
||||
f.text_msg(format!("* Message {}v2", i - 3))
|
||||
.edit(
|
||||
&prev_event,
|
||||
&prev_event_id,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!(
|
||||
"Message {}v2",
|
||||
i - 3
|
||||
)),
|
||||
)
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j == 9 {
|
||||
// Redaction.
|
||||
// Note: (i-6)%3 is the same as i%6 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 6)).unwrap();
|
||||
events
|
||||
.push(f.redaction(&prev_event).sender(sender).event_id(&event_id).into_raw_sync());
|
||||
let prev_event_id = events[i - 6]
|
||||
.get_field::<OwnedEventId>("event_id")
|
||||
.expect("invalid event ID")
|
||||
.expect("missing event ID");
|
||||
events.push(f.redaction(&prev_event_id).sender(sender).into_raw_sync());
|
||||
}
|
||||
}
|
||||
|
||||
let builder = JoinedRoomBuilder::new(&room_id)
|
||||
.add_state_event(StateTestEvent::Encryption)
|
||||
.add_state_event(f.room_encryption().sender(&sender_id))
|
||||
.add_timeline_bulk(events);
|
||||
|
||||
let room = runtime.block_on(async move {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:5.6
|
||||
// swift-tools-version:5.7
|
||||
|
||||
// A package manifest for local development. This file will be copied
|
||||
// into the root of the repo when generating an XCFramework.
|
||||
@@ -8,7 +8,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "MatrixRustSDK",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.iOS(.v16),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version: 5.6
|
||||
// swift-tools-version: 5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "MatrixRustSDK",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.iOS(.v16),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "matrix-sdk-crypto-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
description = "Uniffi based bindings for the Rust SDK crypto crate"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
|
||||
@@ -33,15 +33,22 @@ Rust supports many different [targets], you'll have to make sure to pick the
|
||||
right one for your device or emulator.
|
||||
|
||||
After this is done, we'll have to configure [Cargo] to use the correct linker
|
||||
for our target. Cargo is configured using a TOML file that will be found in
|
||||
`%USERPROFILE%\.cargo\config.toml` on Windows or `$HOME/.cargo/config` on Unix
|
||||
platforms. More details and configuration options for Cargo can be found in the
|
||||
official docs over [here](https://doc.rust-lang.org/cargo/reference/config.html).
|
||||
for our target, by providing the Cargo setting of
|
||||
[target.<triple>.linker](https://doc.rust-lang.org/cargo/reference/config.html#targettriplelinker)
|
||||
with a value of the path to an appropriate linker in your NDK installation.
|
||||
|
||||
This may be set through an environment variable:
|
||||
|
||||
```
|
||||
$ export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="<path-to-ndk-installation>/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
|
||||
```
|
||||
|
||||
Alternatively, it may be set in the `.cargo/config.toml` file in the current directory,
|
||||
any parent directory, or your home directory:
|
||||
|
||||
```
|
||||
[target.aarch64-linux-android]
|
||||
ar = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/ar"
|
||||
linker = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
|
||||
linker = "<path-to-ndk-installation>/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -51,7 +58,13 @@ we'll need to set the `ANDROID_NDK` environment variable to the location of our
|
||||
Android NDK installation.
|
||||
|
||||
```
|
||||
$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/22.0.7026061/
|
||||
$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/<some-installed-version>
|
||||
```
|
||||
|
||||
Also, include the NDK tools directory in your `PATH`:
|
||||
|
||||
```
|
||||
$ export PATH="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
|
||||
```
|
||||
|
||||
### Building for a target
|
||||
@@ -62,12 +75,13 @@ The bindings can built for the `aarch64` target with:
|
||||
$ cargo build --target aarch64-linux-android
|
||||
```
|
||||
|
||||
After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory.
|
||||
The library will be called `libmatrix_crypto.so` and needs to be renamed and
|
||||
After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory,
|
||||
under the repository root directory.
|
||||
The library will be called `libmatrix_sdk_crypto_ffi.so` and needs to be renamed and
|
||||
copied into the `jniLibs` directory of your Android project, for Element Android:
|
||||
|
||||
```
|
||||
$ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
|
||||
$ cp ../../target/aarch64-linux-android/debug/libmatrix_sdk_crypto_ffi.so \
|
||||
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
|
||||
```
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
|
||||
use hmac::Hmac;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::DecryptionError,
|
||||
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
store::{CryptoStoreError as InnerStoreError, types::BackupDecryptionKey},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use rand::{RngExt, distr::Alphanumeric, rng};
|
||||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
@@ -75,11 +75,7 @@ impl BackupRecoveryKey {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
inner: BackupDecryptionKey::new()
|
||||
.expect("Can't gather enough randomness to create a recovery key"),
|
||||
passphrase_info: None,
|
||||
})
|
||||
Arc::new(Self { inner: BackupDecryptionKey::new(), passphrase_info: None })
|
||||
}
|
||||
|
||||
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
|
||||
@@ -97,7 +93,7 @@ impl BackupRecoveryKey {
|
||||
/// Create a new [`BackupRecoveryKey`] from the given passphrase.
|
||||
#[uniffi::constructor]
|
||||
pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
let salt: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
|
||||
@@ -2,14 +2,14 @@ use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings,
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
DecryptionSettings,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use ruma::{OwnedDeviceId, api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
@@ -29,8 +29,6 @@ pub enum DehydrationError {
|
||||
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
|
||||
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
|
||||
PickleKeyLength(usize),
|
||||
#[error(transparent)]
|
||||
Rand(#[from] rand::Error),
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
|
||||
@@ -227,13 +225,11 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
|
||||
use crate::{DehydratedDeviceKey, dehydrated_devices::DehydrationError};
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key() {
|
||||
let result = DehydratedDeviceKey::new();
|
||||
assert!(result.is_ok());
|
||||
let dehydrated_device_key = result.unwrap();
|
||||
let dehydrated_device_key = DehydratedDeviceKey::new();
|
||||
let base_64 = dehydrated_device_key.to_base64();
|
||||
let inner_bytes = dehydrated_device_key.inner;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
|
||||
SignatureError as InnerSignatureError,
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
};
|
||||
use matrix_sdk_sqlite::OpenStoreError;
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
|
||||
@@ -31,22 +31,22 @@ pub use error::{
|
||||
CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError,
|
||||
};
|
||||
use js_int::UInt;
|
||||
pub use logger::{set_logger, Logger};
|
||||
pub use logger::{Logger, set_logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
|
||||
use matrix_sdk_crypto::{
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{
|
||||
CryptoStore,
|
||||
types::{
|
||||
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
CryptoStore,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
pub use responses::{
|
||||
@@ -54,9 +54,9 @@ pub use responses::{
|
||||
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
|
||||
RoomId, SecondsSinceUnixEpoch, UserId,
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
@@ -850,9 +850,9 @@ pub struct DehydratedDeviceKey {
|
||||
|
||||
impl DehydratedDeviceKey {
|
||||
/// Generates a new random pickle key.
|
||||
pub fn new() -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::new()?;
|
||||
Ok(inner.into())
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
InnerDehydratedDeviceKey::new().into()
|
||||
}
|
||||
|
||||
/// Creates a new dehydration pickle key from the given slice.
|
||||
@@ -1015,18 +1015,18 @@ impl PkEncryption {
|
||||
}
|
||||
|
||||
/// Encrypt a message using this [`PkEncryption`] object.
|
||||
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
|
||||
pub fn encrypt(&self, plaintext: &str) -> Option<PkMessage> {
|
||||
use vodozemac::base64_encode;
|
||||
|
||||
let message = self.inner.encrypt(plaintext.as_ref());
|
||||
let message = self.inner.encrypt(plaintext.as_ref()).ok()?;
|
||||
|
||||
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
|
||||
|
||||
PkMessage {
|
||||
Some(PkMessage {
|
||||
ciphertext: base64_encode(ciphertext),
|
||||
mac: base64_encode(mac),
|
||||
ephemeral_key: ephemeral_key.to_base64(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,11 +1049,11 @@ uniffi::setup_scaffolding!();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::MigrationData;
|
||||
use crate::{migrate, EventEncryptionAlgorithm, OlmMachine, RoomSettings};
|
||||
use crate::{EventEncryptionAlgorithm, OlmMachine, RoomSettings, migrate};
|
||||
|
||||
#[test]
|
||||
fn android_migration() -> Result<()> {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
use tracing_subscriber::{EnvFilter, fmt::MakeWriter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
|
||||
@@ -10,6 +10,8 @@ use std::{
|
||||
use js_int::UInt;
|
||||
use matrix_sdk_common::deserialized_responses::AlgorithmInfo;
|
||||
use matrix_sdk_crypto::{
|
||||
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
backups::{
|
||||
MegolmV1BackupKey as RustBackupKey, SignatureState,
|
||||
SignatureVerification as RustSignatureCheckResult,
|
||||
@@ -18,11 +20,12 @@ use matrix_sdk_crypto::{
|
||||
olm::ExportedRoomKey,
|
||||
store::types::{BackupDecryptionKey, Changes},
|
||||
types::requests::ToDeviceRequest,
|
||||
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
api::{
|
||||
IncomingResponse,
|
||||
client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
@@ -32,38 +35,35 @@ use ruma::{
|
||||
upload_signatures::v3::Response as SignatureUploadResponse,
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomMessageResponse,
|
||||
sync::sync_events::{v3::ToDevice, DeviceLists as RumaDeviceLists},
|
||||
sync::sync_events::{DeviceLists as RumaDeviceLists, v3::ToDevice},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
events::{
|
||||
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
AnyMessageLikeEvent, AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
key::verification::VerificationMethod, room::message::MessageType,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use serde_json::{Value, value::RawValue};
|
||||
use tokio::runtime::Runtime;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
|
||||
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
|
||||
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
|
||||
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
|
||||
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
|
||||
dehydrated_devices::DehydratedDevices,
|
||||
error::{
|
||||
CryptoStoreError, DecryptionError, SecretImportError, SecretsBundleExportError,
|
||||
SignatureError,
|
||||
},
|
||||
parse_user_id,
|
||||
responses::{response_from_string, OwnedResponse},
|
||||
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
|
||||
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
|
||||
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
|
||||
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
|
||||
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
|
||||
responses::{OwnedResponse, response_from_string},
|
||||
};
|
||||
|
||||
/// The return value for the [`OlmMachine::receive_sync_changes()`] method.
|
||||
@@ -913,22 +913,19 @@ impl OlmMachine {
|
||||
&decryption_settings,
|
||||
))?;
|
||||
|
||||
if handle_verification_events {
|
||||
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype
|
||||
{
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
if handle_verification_events
|
||||
&& let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize()
|
||||
{
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(original_event)) => {
|
||||
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype {
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@ use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
CrossSigningBootstrapRequests,
|
||||
types::requests::{
|
||||
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
},
|
||||
CrossSigningBootstrapRequests,
|
||||
};
|
||||
use ruma::{
|
||||
OwnedTransactionId, UserId,
|
||||
api::client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
@@ -28,7 +29,6 @@ use ruma::{
|
||||
},
|
||||
assign,
|
||||
events::MessageLikeEventContent,
|
||||
OwnedTransactionId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
|
||||
use matrix_sdk_crypto::{UserIdentity as SdkUserIdentity, types::CrossSigningKey};
|
||||
|
||||
use crate::CryptoStoreError;
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ use std::sync::Arc;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
|
||||
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
|
||||
Verification as InnerVerification, VerificationRequest as InnerVerificationRequest,
|
||||
CancelInfo as RustCancelInfo, QrVerification as InnerQr, QrVerificationState, Sas as InnerSas,
|
||||
SasState as RustSasState, Verification as InnerVerification,
|
||||
VerificationRequest as InnerVerificationRequest,
|
||||
VerificationRequestState as RustVerificationRequestState,
|
||||
matrix_sdk_qrcode::QrVerificationData,
|
||||
};
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
description = "Helper macros to write FFI bindings"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "Apache-2.0"
|
||||
@@ -17,9 +17,9 @@ test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2.workspace = true
|
||||
proc-macro2 = { workspace = true , features = ["proc-macro"] }
|
||||
quote.workspace = true
|
||||
syn = { workspace = true, features = ["full", "extra-traits"] }
|
||||
syn = { workspace = true, features = ["full", "extra-traits", "proc-macro"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -27,18 +27,18 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
}
|
||||
} else if let Item::Impl(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let ImplItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
if let ImplItem::Fn(fun) = item
|
||||
&& fun.sig.asyncness.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if let Item::Trait(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let TraitItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
if let TraitItem::Fn(fun) = item
|
||||
&& fun.sig.asyncness.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,19 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add `Client::set_avatar_url` to manually set the avatar URL of the user to a provided MXC one.
|
||||
- Allow setting a custom Sliding Sync connection ID and timeline limit on `RoomListService`.
|
||||
([#6289](https://github.com/matrix-org/matrix-rust-sdk/pull/6289))
|
||||
- Fix devices on Android 11 crashing because the SDK could not be initialized using `libloading`
|
||||
to get a reference to the JVM. Replaced `libloading` with `jvm-getter`, which works like a
|
||||
compatibility layer. ([#6370](https://github.com/matrix-org/matrix-rust-sdk/pull/6370))
|
||||
- Added `android_platform.rs` for fixing the `rustls` integration on Android, which was broken.
|
||||
([#6306](https://github.com/matrix-org/matrix-rust-sdk/pull/6306))
|
||||
- [**breaking**] `OtherState` properly supports redacted events that still have fields in the
|
||||
content. The following fields are no longer optional:
|
||||
- `federate` in `OtherState::RoomCreate`.
|
||||
- `history_visibility` in `OtherState::RoomHistoryVisibility`.
|
||||
- `thresholds` in `OtherState::RoomPowerLevels`.
|
||||
- `omit_checksums` option is now enabled for the Kotlin bindings in all FFI-exporting crates.
|
||||
We enabled them because with JNA direct mapping enabled they result in invalid checks in
|
||||
ARM 32bit devices, preventing the SDK from working altogether (see
|
||||
@@ -31,6 +44,75 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Client::get_dm_rooms` function to get a list with the DMs for the provided user id.
|
||||
([#6487](https://github.com/matrix-org/matrix-rust-sdk/pull/6487))
|
||||
- Expose `ffi::NotificationRoomInfo::service_members` so clients can use the list of service
|
||||
members to calculate if a room is a DM from the notification info.
|
||||
([#6474](https://github.com/matrix-org/matrix-rust-sdk/pull/6474))
|
||||
- Enable `experimental-push-secrets` feature by default.
|
||||
([#6473](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
|
||||
- Add new high-level search helpers `RoomSearchIterator` and `GlobalSearchIterator` to perform
|
||||
searches for messages in a room or across all rooms.
|
||||
([6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
|
||||
- Added the `Client.request_openid_token()` method.
|
||||
([#6458](https://github.com/matrix-org/matrix-rust-sdk/pull/6458))
|
||||
- Added the `Client::import_secrets_bundle` method.
|
||||
([#6212](https://github.com/matrix-org/matrix-rust-sdk/pull/6212))
|
||||
- [**breaking**] Remove support for `native-tls` and remove all feature
|
||||
flags for selecting TLS backend, as `rustls` is the now the only supported
|
||||
TLS backend.
|
||||
([#6409](https://github.com/matrix-org/matrix-rust-sdk/pull/6409))
|
||||
- Expose `event_type_raw` and `latest_json()` on `EventTimelineItem`,
|
||||
allowing clients to access the raw event type string and full event JSON for
|
||||
custom event handling without pattern-matching through nested enums.
|
||||
([#6387](https://github.com/matrix-org/matrix-rust-sdk/pull/6387))
|
||||
([#6424](https://github.com/matrix-org/matrix-rust-sdk/pull/6424))
|
||||
- Expose sync v2 API through FFI via `Client.sync_v2()` and
|
||||
`Client.sync_once_v2()`, enabling mobile clients to sync without
|
||||
requiring Sliding Sync support on the homeserver. `Client.sync_v2()`
|
||||
accepts a `SyncListenerV2` callback that receives a `SyncResponseV2`
|
||||
after each successful sync.
|
||||
([#6359](https://github.com/matrix-org/matrix-rust-sdk/pull/6359))
|
||||
- Added `HomeserverCapabilities` and `Client::homeserver_capabilities()` to get the capabilities
|
||||
of the homeserver. ([#6371](https://github.com/matrix-org/matrix-rust-sdk/pull/6371))
|
||||
- Expose `Room.send_state_event_raw()` for sending arbitrary state events
|
||||
through the FFI layer.
|
||||
([#6350](https://github.com/matrix-org/matrix-rust-sdk/pull/6350))
|
||||
- Introduce a `ThreadListService` which offers reactive interfaces for rendering
|
||||
and managing the list of threads from a particular room.
|
||||
([6311](https://github.com/matrix-org/matrix-rust-sdk/pull/6311))
|
||||
- [**breaking**] Move `LiveLocation` out of `TimelineItemContent` and into `MsgLikeKind`
|
||||
so it has access to `MsgLikeContent` `reactions`.
|
||||
([#6286](https://github.com/matrix-org/matrix-rust-sdk/pull/6286))
|
||||
- Add `HumanQrLoginError::UnsupportedQrCodeType` for when a QR is parseable but cannot be used to
|
||||
complete a login.
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6285)
|
||||
- Add `HumanQrGrantLoginError::UnsupportedQrCodeType` for when a QR is parseable but cannot be used
|
||||
to grant a login.
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6285)
|
||||
- Add the `QrCodeData::base_url` and `QrCodeData::intent` methods.
|
||||
([#6283](https://github.com/matrix-org/matrix-rust-sdk/pull/6283))
|
||||
- Add `Encryption::recover_and_fix_backup` to automatically fix key storage backup if the
|
||||
private backup decryption key is missing, invalid or inconsistent with the public key.
|
||||
([#6252](https://github.com/matrix-org/matrix-rust-sdk/pull/6252))
|
||||
- Add support for [MSC3489](https://github.com/matrix-org/matrix-spec-proposals/pull/3489)
|
||||
live location sharing through a new `TimelineItemContent::LiveLocation` variant.
|
||||
([#6232](https://github.com/matrix-org/matrix-rust-sdk/pull/6232))
|
||||
- Add `HumanQrGrantLoginError::ConnectionInsecure` for errors establishing the secure channel
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::Expired` for when a timeout is encountered during the grant
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::Cancelled` for when the grant is cancelled
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::OtherDeviceAlreadySignedIn` for when the other device is already signed in
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `HumanQrGrantLoginError::DeviceNotFound` for when the requested device was not returned by the homeserver
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- Add `RoomInfo::is_low_priority` for getting the room's `m.lowpriority` tag state
|
||||
([#6183](https://github.com/matrix-org/matrix-rust-sdk/pull/6183))
|
||||
- Add `Client::subscribe_to_duplicate_key_upload_errors` for listening to duplicate key
|
||||
upload errors from `/keys/upload`.
|
||||
([#6135](https://github.com/matrix-org/matrix-rust-sdk/pull/6135/))
|
||||
- Add `NotificationItem::raw_event` to get the raw event content of the event that triggered the notification, which can be useful for debugging and to support clients that want to implement custom handling for certain notifications. ([#6122](https://github.com/matrix-org/matrix-rust-sdk/pull/6122))
|
||||
- [**breaking**] Extend `TimelineFocus::Event` to allow marking the target
|
||||
event as the root of a thread.
|
||||
@@ -71,10 +153,50 @@ All notable changes to this project will be documented in this file.
|
||||
the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
|
||||
key bundle.
|
||||
([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000))
|
||||
- Add `NonFavorite` filter to the Room List API. ([#5991](https://github.com/matrix-org/matrix-rust-sdk/pull/5991))
|
||||
- Add `NonFavorite` filter to the Room List API. ([#5991](https://github.com/matrix-org/matrix-rust-sdk/pull/5991)
|
||||
- Add `call_intent` (either `RtcCallIntent::Audio` or `RtcCallIntent::Video`) field to `RtcNotification` event content. ([#6207](https://github.com/matrix-org/matrix-rust-sdk/pull/6207))
|
||||
- Add `RoomInfo::active_room_call_consensus_intent` method to get the call intent for the current call,
|
||||
based on what members are advertising.
|
||||
([#6274](https://github.com/matrix-org/matrix-rust-sdk/pull/6274))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `Room::observe_live_location_shares` has been replaced by
|
||||
`Room::live_location_shares`. Call [`LiveLocationShares::subscribe`] on it to
|
||||
receive an initial snapshot and a stream of incremental updates.The stream is seeded from the event cache
|
||||
on creation and includes the own user's shares (previously excluded). `LiveLocationShare.is_live`
|
||||
has been removed; instead `ts` (start timestamp) and `timeout` (duration in milliseconds) are now
|
||||
exposed so clients can compute liveness themselves via `current_time < ts + timeout`. Non-live
|
||||
shares are automatically removed from the list. A new `LiveLocationShareListener` callback
|
||||
interface must be implemented and passed to the method.
|
||||
([#6385](https://github.com/matrix-org/matrix-rust-sdk/pull/6385))
|
||||
- [**breaking**] The `RoomAliases` variants of `StateEventContent`, `StateEventType` and
|
||||
`OtherState` was removed. This state event type was removed from the Matrix specification a while
|
||||
ago, and support for it has been removed in Ruma.
|
||||
([#6414](https://github.com/matrix-org/matrix-rust-sdk/pull/6414))
|
||||
- `Client::new` no longer unnecessarily instantiates an `OAuth` component if `CrossProcessLockConfig::SingleProcess`
|
||||
is used. ([#6293](https://github.com/matrix-org/matrix-rust-sdk/pull/6293))
|
||||
- [**breaking**] `Room::report_content()` no longer takes a `score` argument, because it was
|
||||
removed from the Matrix specification.
|
||||
([#6256](https://github.com/matrix-org/matrix-rust-sdk/pull/6256))
|
||||
- [**breaking**] The `current_version` field of `ErrorKind::WrongRoomKeysVersion`
|
||||
is no longer optional.
|
||||
([#6241](https://github.com/matrix-org/matrix-rust-sdk/pull/6241))
|
||||
- [**breaking**] The following variants of `AccountManagementAction` were
|
||||
renamed to match their new names after being merge in the Matrix specification:
|
||||
- `SessionsList` is renamed to `DevicesList`
|
||||
- `SessionView` is renamed to `DeviceView`
|
||||
- `SessionEnd` is renamed to `DeviceDelete`
|
||||
([#6217](https://github.com/matrix-org/matrix-rust-sdk/pull/6217))
|
||||
- [**breaking**] `HumanQrGrantLoginError::UnableToCreateDevice` has been removed
|
||||
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
|
||||
- [**breaking**] Removed `ClientBuilder::enable_oidc_refresh_lock` in favour of using `ClientBuilder::cross_process_lock_config`
|
||||
to configure that lock when a `MultiProcess` configuration is supplied. ([#6204](https://github.com/matrix-org/matrix-rust-sdk/pull/6204))
|
||||
- `RoomPaginationStatus` is renamed to `PaginationStatus`.
|
||||
([#6174](https://github.com/matrix-org/matrix-rust-sdk/pull/6174/))
|
||||
- [**breaking**] Replaced `ClientBuilder::cross_process_store_locks_holder_name` with `ClientBuilder::cross_process_lock_config`,
|
||||
which accepts a `CrossProcessLockConfig` value to specify whether the resulting `Client` will be used in a single
|
||||
process or multiple processes. ([#6160](https://github.com/matrix-org/matrix-rust-sdk/pull/6160))
|
||||
- [**breaking**] Refactored `is_last_admin` to `is_last_owner` the check will now
|
||||
account also for v12 rooms, where creators and users with PL 150 matter.
|
||||
([#6036](https://github.com/matrix-org/matrix-rust-sdk/pull/6036))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.16.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
license = "Apache-2.0"
|
||||
@@ -24,7 +24,7 @@ crate-type = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"]
|
||||
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "experimental-push-secrets"]
|
||||
# Use SQLite for the session storage.
|
||||
sqlite = ["matrix-sdk/sqlite"]
|
||||
# Use an embedded version of SQLite.
|
||||
@@ -34,17 +34,15 @@ 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"]
|
||||
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
|
||||
# Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
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"]
|
||||
experimental-push-secrets = ["matrix-sdk/experimental-push-secrets"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
extension-trait = "1.0.2"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
@@ -58,13 +56,13 @@ matrix-sdk = { workspace = true, features = [
|
||||
"socks",
|
||||
"uniffi",
|
||||
"federation-api",
|
||||
"experimental-search"
|
||||
] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = { version = "0.3.17", default-features = false }
|
||||
once_cell.workspace = true
|
||||
ruma = { workspace = true, features = [
|
||||
"html",
|
||||
"unstable-msc3488",
|
||||
@@ -124,9 +122,18 @@ uniffi = { workspace = true, features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = { version = "0.2.2", default-features = false }
|
||||
# Needed for `rustls-platform-verifier`. Newer versions aren't compatible with it.
|
||||
jni = "0.21.1"
|
||||
# Used to access the credential storage on Android
|
||||
rustls-platform-verifier = "0.6.2"
|
||||
# Needed for intializing and keeping the JavaVM reference around
|
||||
once_cell = "1.21.4"
|
||||
# Gobley's jvm-getter is used to get a JVM pointer from all Android versions
|
||||
jvm-getter = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
similar-asserts.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -6,11 +6,6 @@ This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build
|
||||
|
||||
Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
### 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.
|
||||
@@ -24,11 +19,11 @@ Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
## 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 build the relevant 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: `"indexeddb,unstable-msc4274,native-tls"`
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,sentry"`
|
||||
- JavaScript/Wasm: `"indexeddb,unstable-msc4274"`
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
@@ -5,12 +19,12 @@ use std::{
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
Error,
|
||||
authentication::oauth::{
|
||||
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
|
||||
error::OAuthAuthorizationCodeError,
|
||||
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
|
||||
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
use ruma::serde::Raw;
|
||||
use url::Url;
|
||||
@@ -85,8 +99,9 @@ impl SsoHandler {
|
||||
let auth = self.client.inner.matrix_auth();
|
||||
let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
let builder =
|
||||
auth.login_with_sso_callback(url).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
auth.login_with_sso_callback(url.into()).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! A generic `ChunkIterator` that operates over a `Vec`.
|
||||
//!
|
||||
//! This type is not designed to work over FFI, but it can be embedded inside an
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
@@ -6,46 +20,48 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
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;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
|
||||
authentication::oauth::{ClientId, OAuthAuthorizationData, OAuthError, OAuthSession},
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
executor::AbortOnDrop,
|
||||
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
|
||||
ruma::{
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
api::client::{
|
||||
account::request_openid_token,
|
||||
discovery::{
|
||||
discover_homeserver::RtcFocusInfo,
|
||||
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
|
||||
},
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
room::{Visibility, create_room},
|
||||
session::get_login_types,
|
||||
user_directory::search_users,
|
||||
},
|
||||
events::{
|
||||
AnyInitialStateEvent, InitialStateEvent,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
|
||||
message::MessageType,
|
||||
},
|
||||
AnyInitialStateEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
sync::Notification,
|
||||
task_monitor::BackgroundTaskFailureReason,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_common::{
|
||||
SendOutsideWasm, SyncOutsideWasm, cross_process_lock::CrossProcessLockConfig, stream::StreamExt,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
@@ -57,14 +73,23 @@ use matrix_sdk_ui::{
|
||||
use mime::Mime;
|
||||
use oauth2::Scope;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
alias::get_alias,
|
||||
OwnedDeviceId, OwnedMxcUri, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
api::{
|
||||
client::{
|
||||
alias::get_alias,
|
||||
discovery::get_authorization_server_metadata::v1::{
|
||||
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
|
||||
},
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
room::create_room::{RoomPowerLevelsContentOverride, v3::CreationContent},
|
||||
uiaa::{EmailUserIdentifier, UserIdentifier},
|
||||
},
|
||||
error::ErrorKind,
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
events::{
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
@@ -83,25 +108,22 @@ use ruma::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
},
|
||||
tag::TagEventContent,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
room::RoomType,
|
||||
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
|
||||
use super::{
|
||||
room::{room_info::RoomInfo, Room},
|
||||
room::{Room, room_info::RoomInfo},
|
||||
session_verification::SessionVerificationController,
|
||||
};
|
||||
use crate::{
|
||||
ClientError,
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
@@ -121,10 +143,10 @@ use crate::{
|
||||
runtime::get_runtime_handle,
|
||||
spaces::SpaceService,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
sync_v2::{SyncListenerV2, SyncResponseV2, SyncSettingsV2},
|
||||
task_handle::TaskHandle,
|
||||
utd::{UnableToDecryptDelegate, UtdHook},
|
||||
utils::AsyncRuntimeDropped,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -237,6 +259,32 @@ pub trait AccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_change(&self, event: AccountDataEvent);
|
||||
}
|
||||
|
||||
/// A listener for duplicate key upload errors triggered by requests to
|
||||
/// /keys/upload.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait DuplicateKeyUploadErrorListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called once when uploading keys fails.
|
||||
fn on_duplicate_key_upload_error(&self, message: Option<DuplicateOneTimeKeyErrorMessage>);
|
||||
}
|
||||
|
||||
/// Information about the old and new key that caused a duplicate key upload
|
||||
/// error in /keys/upload.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct DuplicateOneTimeKeyErrorMessage {
|
||||
/// The previously uploaded one-time key, encoded as unpadded base64.
|
||||
pub old_key: String,
|
||||
/// The one-time key we attempted to upload, encoded as unpadded base64
|
||||
pub new_key: String,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::DuplicateOneTimeKeyErrorMessage>
|
||||
for DuplicateOneTimeKeyErrorMessage
|
||||
{
|
||||
fn from(value: matrix_sdk::encryption::DuplicateOneTimeKeyErrorMessage) -> Self {
|
||||
Self { old_key: value.old_key.to_base64(), new_key: value.new_key.to_base64() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for changes of room account data events.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
@@ -269,6 +317,28 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct OpenIdToken {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
pub matrix_server_name: String,
|
||||
pub expires_in_seconds: u64,
|
||||
}
|
||||
|
||||
impl From<request_openid_token::v3::Response> for OpenIdToken {
|
||||
fn from(value: request_openid_token::v3::Response) -> Self {
|
||||
Self {
|
||||
access_token: value.access_token,
|
||||
token_type: serde_json::to_value(&value.token_type)
|
||||
.ok()
|
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_else(|| format!("{:?}", value.token_type)),
|
||||
matrix_server_name: value.matrix_server_name.to_string(),
|
||||
expires_in_seconds: value.expires_in.as_secs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegateData {
|
||||
/// The delegate itself, that will receive the callbacks.
|
||||
delegate: Arc<dyn ClientDelegate>,
|
||||
@@ -299,7 +369,6 @@ pub struct Client {
|
||||
impl Client {
|
||||
pub async fn new(
|
||||
sdk_client: MatrixClient,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
store_path: Option<PathBuf>,
|
||||
) -> Result<Self, ClientError> {
|
||||
@@ -322,17 +391,16 @@ impl Client {
|
||||
|
||||
let controller = session_verification_controller.clone();
|
||||
sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
|
||||
if let MessageType::VerificationRequest(_) = &event.content.msgtype {
|
||||
if let Some(session_verification_controller) = &*controller.clone().read().await {
|
||||
session_verification_controller
|
||||
.process_incoming_verification_request(&event.sender, event.event_id)
|
||||
.await;
|
||||
}
|
||||
if let MessageType::VerificationRequest(_) = &event.content.msgtype
|
||||
&& let Some(session_verification_controller) = &*controller.clone().read().await
|
||||
{
|
||||
session_verification_controller
|
||||
.process_incoming_verification_request(&event.sender, event.event_id)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let cross_process_store_locks_holder_name =
|
||||
sdk_client.cross_process_store_locks_holder_name().to_owned();
|
||||
let store_mode = sdk_client.cross_process_lock_config();
|
||||
|
||||
let client = Client {
|
||||
inner: AsyncRuntimeDropped::new(sdk_client.clone()),
|
||||
@@ -342,18 +410,16 @@ impl Client {
|
||||
store_path,
|
||||
};
|
||||
|
||||
if enable_oidc_refresh_lock {
|
||||
if session_delegate.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"missing session delegates when enabling the cross-process lock"
|
||||
))?;
|
||||
match store_mode {
|
||||
CrossProcessLockConfig::MultiProcess { holder_name } => {
|
||||
if session_delegate.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"missing session delegates with multi-process lock configuration"
|
||||
))?;
|
||||
}
|
||||
client.inner.oauth().enable_cross_process_refresh_lock(holder_name.clone()).await?;
|
||||
}
|
||||
|
||||
client
|
||||
.inner
|
||||
.oauth()
|
||||
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
|
||||
.await?;
|
||||
CrossProcessLockConfig::SingleProcess => {}
|
||||
}
|
||||
|
||||
if let Some(session_delegate) = session_delegate {
|
||||
@@ -448,13 +514,17 @@ impl Client {
|
||||
device_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
|
||||
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
}
|
||||
|
||||
if let Some(device_id) = device_id.as_ref() {
|
||||
builder = builder.device_id(device_id);
|
||||
}
|
||||
|
||||
builder.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -480,6 +550,7 @@ impl Client {
|
||||
}
|
||||
|
||||
builder.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -494,7 +565,7 @@ impl Client {
|
||||
let mut builder = self
|
||||
.inner
|
||||
.matrix_auth()
|
||||
.login_identifier(UserIdentifier::Email { address: email }, &password);
|
||||
.login_identifier(UserIdentifier::Email(EmailUserIdentifier::new(email)), &password);
|
||||
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
@@ -748,6 +819,28 @@ impl Client {
|
||||
})))
|
||||
}
|
||||
|
||||
/// Subscribe to duplicate key upload errors triggered by requests to
|
||||
/// /keys/upload.
|
||||
pub fn subscribe_to_duplicate_key_upload_errors(
|
||||
&self,
|
||||
listener: Box<dyn DuplicateKeyUploadErrorListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_to_duplicate_key_upload_errors();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(message) => {
|
||||
listener.on_duplicate_key_upload_error(message.map(|m| m.into()))
|
||||
}
|
||||
Err(err) => {
|
||||
error!("error when listening to key upload errors: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Subscribe to updates of global account data events.
|
||||
///
|
||||
/// Be careful that only the most recent value can be observed. Subscribers
|
||||
@@ -889,139 +982,7 @@ impl Client {
|
||||
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, raw_event) = match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
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, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to deserialize timeline event: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
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, raw_event)
|
||||
}
|
||||
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,
|
||||
raw_event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
has_mention: Some(has_mention),
|
||||
thread_id,
|
||||
actions: Some(actions),
|
||||
},
|
||||
room_id,
|
||||
);
|
||||
}
|
||||
notification_handler(notification, room, listener.clone())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
@@ -1063,10 +1024,7 @@ impl Client {
|
||||
pub async fn reset_well_known(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_well_known().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Retrieves a media file from the media source
|
||||
///
|
||||
/// Not available on Wasm platforms, due to lack of accessible file system.
|
||||
@@ -1133,10 +1091,7 @@ impl Client {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// The sliding sync version.
|
||||
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
|
||||
self.inner.sliding_sync_version().into()
|
||||
@@ -1250,20 +1205,20 @@ impl Client {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut url_builder = match self.inner.oauth().account_management_url().await {
|
||||
Ok(Some(url_builder)) => url_builder,
|
||||
Ok(None) => return Ok(None),
|
||||
let server_metadata = match self.inner.oauth().cached_server_metadata().await {
|
||||
Ok(server_metadata) => server_metadata,
|
||||
Err(e) => {
|
||||
error!("Failed retrieving account management URL: {e}");
|
||||
return Err(e.into());
|
||||
error!("Failed retrieving cached server metadata: {e}");
|
||||
return Err(OAuthError::from(e).into());
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(action) = action {
|
||||
url_builder = url_builder.action(action.into());
|
||||
Ok(if let Some(action) = &action {
|
||||
server_metadata.account_management_url_with_action(action.into())
|
||||
} else {
|
||||
server_metadata.account_management_uri
|
||||
}
|
||||
|
||||
Ok(Some(url_builder.build().to_string()))
|
||||
.map(Into::into))
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> Result<String, ClientError> {
|
||||
@@ -1283,12 +1238,28 @@ impl Client {
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
pub async fn request_openid_token(&self) -> Result<OpenIdToken, ClientError> {
|
||||
Ok(self.inner.account().request_openid_token().await?.into())
|
||||
}
|
||||
|
||||
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner.account().upload_avatar(&mime, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the user's avatar using the provided MXC url.
|
||||
pub async fn set_avatar_url(&self, url: String) -> Result<(), ClientError> {
|
||||
// MxcUri can't just be instantiated, serde deserialization seems to be the only
|
||||
// way
|
||||
let mxc = serde_json::from_str::<OwnedMxcUri>(&url)?;
|
||||
// Validate the newly generated MxcUri
|
||||
mxc.validate().map_err(ClientError::from_err)?;
|
||||
|
||||
self.inner.account().set_avatar_url(Some(&mxc)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
|
||||
self.inner.account().set_avatar_url(None).await?;
|
||||
Ok(())
|
||||
@@ -1512,6 +1483,7 @@ impl Client {
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// Get the first existing DM room with the given user, if any.
|
||||
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let sdk_room = self.inner.get_dm_room(&user_id);
|
||||
@@ -1520,6 +1492,16 @@ impl Client {
|
||||
Ok(dm)
|
||||
}
|
||||
|
||||
/// Get an iterator with the existing DM rooms for the given user.
|
||||
pub fn get_dm_rooms(&self, user_id: String) -> Result<Vec<Arc<Room>>, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let sdk_rooms = self.inner.get_dm_rooms(&user_id);
|
||||
let dms = sdk_rooms
|
||||
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
.collect();
|
||||
Ok(dms)
|
||||
}
|
||||
|
||||
pub async fn search_users(
|
||||
&self,
|
||||
search_term: String,
|
||||
@@ -1549,6 +1531,55 @@ impl Client {
|
||||
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
|
||||
}
|
||||
|
||||
/// Start a sync v2 loop.
|
||||
///
|
||||
/// This is an alternative to [`Client::sync_service`] (which uses Sliding
|
||||
/// Sync / MSC4186). It works with any homeserver, including older
|
||||
/// Synapse versions that do not support Sliding Sync.
|
||||
///
|
||||
/// Returns a `TaskHandle` that can be used to cancel the sync loop.
|
||||
/// The listener is called after each successful sync response.
|
||||
pub fn sync_v2(
|
||||
&self,
|
||||
settings: SyncSettingsV2,
|
||||
listener: Box<dyn SyncListenerV2>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let client = (*self.inner).clone();
|
||||
let sdk_settings: matrix_sdk::config::SyncSettings = settings.into();
|
||||
let listener: Arc<dyn SyncListenerV2> = Arc::from(listener);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let result = client
|
||||
.sync_with_result_callback(sdk_settings, |result| {
|
||||
let listener = listener.clone();
|
||||
async move {
|
||||
let response = result?;
|
||||
let ffi_response: SyncResponseV2 = response.into();
|
||||
listener.on_update(ffi_response);
|
||||
Ok(matrix_sdk::LoopCtrl::Continue)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Sync loop ended with error: {e}");
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Perform a single sync v2 call.
|
||||
///
|
||||
/// This is useful for performing an initial sync or a one-shot sync
|
||||
/// without entering a continuous loop.
|
||||
pub async fn sync_once_v2(
|
||||
&self,
|
||||
settings: SyncSettingsV2,
|
||||
) -> Result<SyncResponseV2, ClientError> {
|
||||
let sdk_settings: matrix_sdk::config::SyncSettings = settings.into();
|
||||
let response = self.inner.sync_once(sdk_settings).await?;
|
||||
Ok(response.into())
|
||||
}
|
||||
|
||||
pub async fn space_service(&self) -> Arc<SpaceService> {
|
||||
let inner = UISpaceService::new((*self.inner).clone()).await;
|
||||
Arc::new(SpaceService::new(inner))
|
||||
@@ -2019,10 +2050,10 @@ impl Client {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
// Emit the initial event, if present
|
||||
if let Some(room) = self.inner.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
if let Some(room) = self.inner.get_room(&room_id)
|
||||
&& let Ok(room_info) = RoomInfo::new(&room).await
|
||||
{
|
||||
listener.call(room_info);
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
|
||||
@@ -2034,15 +2065,150 @@ impl Client {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room) = client.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
if let Some(room) = client.get_room(&room_id)
|
||||
&& let Ok(room_info) = RoomInfo::new(&room).await
|
||||
{
|
||||
listener.call(room_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Whether to enable automatic backpagination under certain conditions
|
||||
/// (e.g. when processing read receipts).
|
||||
///
|
||||
/// This is an experimental feature, and might cause performance issues on
|
||||
/// large accounts. Use with caution.
|
||||
///
|
||||
/// This must be called after creating a client, but before subscribing to
|
||||
/// the event cache (so, before spawning a sync service or a timeline).
|
||||
pub fn enable_automatic_backpagination(&self) {
|
||||
self.inner.event_cache().config_mut().experimental_auto_backpagination = true;
|
||||
}
|
||||
|
||||
pub fn homeserver_capabilities(&self) -> HomeserverCapabilities {
|
||||
HomeserverCapabilities::new(self.inner.homeserver_capabilities())
|
||||
}
|
||||
}
|
||||
|
||||
async fn notification_handler(
|
||||
notification: Notification,
|
||||
room: matrix_sdk::Room,
|
||||
listener: Arc<Box<dyn SyncNotificationListener>>,
|
||||
) {
|
||||
let room_id = room.room_id().to_string();
|
||||
|
||||
// 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, raw_event) = match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let thread_id = if let AnySyncTimelineEvent::MessageLike(event) = &deserialized
|
||||
&& let Some(AnyMessageLikeEventContent::RoomMessage(content)) =
|
||||
event.original_content()
|
||||
&& let Some(Relation::Thread(thread)) = content.relates_to
|
||||
{
|
||||
Some(thread.event_id.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let event = NotificationEvent::Timeline {
|
||||
event: Arc::new(crate::event::TimelineEvent(Box::new(deserialized))),
|
||||
};
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to deserialize timeline event: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
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, raw_event)
|
||||
}
|
||||
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(),
|
||||
service_members: room
|
||||
.service_members()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
is_encrypted: Some(room.encryption_state().is_encrypted()),
|
||||
is_direct,
|
||||
is_space: room.is_space(),
|
||||
};
|
||||
|
||||
listener.on_notification(
|
||||
NotificationItem {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
has_mention: Some(has_mention),
|
||||
thread_id,
|
||||
actions: Some(actions),
|
||||
},
|
||||
room_id,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
@@ -2178,8 +2344,8 @@ impl Client {
|
||||
debug!("Applying session change: {session_change:?}");
|
||||
let delegate = delegate_data.delegate.clone();
|
||||
get_runtime_handle().spawn_blocking(move || match session_change {
|
||||
SessionChange::UnknownToken { soft_logout } => {
|
||||
delegate.did_receive_auth_error(soft_logout);
|
||||
SessionChange::UnknownToken(unknown_token) => {
|
||||
delegate.did_receive_auth_error(unknown_token.soft_logout);
|
||||
}
|
||||
SessionChange::TokensRefreshed => {}
|
||||
});
|
||||
@@ -2599,23 +2765,23 @@ pub(crate) struct OidcSessionData {
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AccountManagementAction {
|
||||
Profile,
|
||||
SessionsList,
|
||||
SessionView { device_id: String },
|
||||
SessionEnd { device_id: String },
|
||||
DevicesList,
|
||||
DeviceView { device_id: String },
|
||||
DeviceDelete { device_id: String },
|
||||
AccountDeactivate,
|
||||
CrossSigningReset,
|
||||
}
|
||||
|
||||
impl From<AccountManagementAction> for AccountManagementActionFull {
|
||||
fn from(value: AccountManagementAction) -> Self {
|
||||
impl<'a> From<&'a AccountManagementAction> for AccountManagementActionData<'a> {
|
||||
fn from(value: &'a AccountManagementAction) -> Self {
|
||||
match value {
|
||||
AccountManagementAction::Profile => Self::Profile,
|
||||
AccountManagementAction::SessionsList => Self::SessionsList,
|
||||
AccountManagementAction::SessionView { device_id } => {
|
||||
Self::SessionView { device_id: device_id.into() }
|
||||
AccountManagementAction::DevicesList => Self::DevicesList,
|
||||
AccountManagementAction::DeviceView { device_id } => {
|
||||
Self::DeviceView(DeviceViewData::new(device_id.as_str().into()))
|
||||
}
|
||||
AccountManagementAction::SessionEnd { device_id } => {
|
||||
Self::SessionEnd { device_id: device_id.into() }
|
||||
AccountManagementAction::DeviceDelete { device_id } => {
|
||||
Self::DeviceDelete(DeviceDeleteData::new(device_id.as_str().into()))
|
||||
}
|
||||
AccountManagementAction::AccountDeactivate => Self::AccountDeactivate,
|
||||
AccountManagementAction::CrossSigningReset => Self::CrossSigningReset,
|
||||
@@ -2928,16 +3094,88 @@ impl From<matrix_sdk::StoreSizes> for StoreSizes {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverCapabilities {
|
||||
inner: matrix_sdk::HomeserverCapabilities,
|
||||
}
|
||||
|
||||
impl HomeserverCapabilities {
|
||||
pub(crate) fn new(capabilities: matrix_sdk::HomeserverCapabilities) -> Self {
|
||||
Self { inner: capabilities }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl HomeserverCapabilities {
|
||||
pub async fn refresh(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.refresh().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_password(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_password().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_displayname(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_displayname().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_avatar(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_avatar().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_thirdparty_ids(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_thirdparty_ids().await?)
|
||||
}
|
||||
|
||||
pub async fn can_get_login_token(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_get_login_token().await?)
|
||||
}
|
||||
|
||||
pub async fn extended_profile_fields(&self) -> Result<ExtendedProfileFields, ClientError> {
|
||||
let profile_fields = self.inner.extended_profile_fields().await?;
|
||||
Ok(ExtendedProfileFields {
|
||||
enabled: profile_fields.enabled,
|
||||
allowed: profile_fields
|
||||
.allowed
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
disallowed: profile_fields
|
||||
.disallowed
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn forgets_room_when_leaving(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.forgets_room_when_leaving().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ExtendedProfileFields {
|
||||
pub enabled: bool,
|
||||
pub allowed: Vec<String>,
|
||||
pub disallowed: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use ruma::{
|
||||
api::client::room::{create_room, Visibility},
|
||||
ServerName,
|
||||
api::client::room::{Visibility, create_room},
|
||||
authentication::TokenType,
|
||||
events::StateEventType,
|
||||
room::RoomType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{CreateRoomParameters, JoinRule, RoomPreset, RoomVisibility},
|
||||
client::{CreateRoomParameters, JoinRule, OpenIdToken, RoomPreset, RoomVisibility},
|
||||
room::RoomHistoryVisibility,
|
||||
};
|
||||
|
||||
@@ -2976,9 +3214,9 @@ mod tests {
|
||||
assert_eq!(request.invite.len(), 1);
|
||||
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomAvatar));
|
||||
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomJoinRules));
|
||||
assert!(initial_state
|
||||
.iter()
|
||||
.any(|e| e.event_type() == StateEventType::RoomHistoryVisibility));
|
||||
assert!(
|
||||
initial_state.iter().any(|e| e.event_type() == StateEventType::RoomHistoryVisibility)
|
||||
);
|
||||
assert_eq!(request.room_alias_name, Some("#a-room:example.com".to_owned()));
|
||||
|
||||
let room_type = request
|
||||
@@ -2989,4 +3227,21 @@ mod tests {
|
||||
.room_type;
|
||||
assert_eq!(room_type, Some(RoomType::Space));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openid_token_mapping() {
|
||||
let response = ruma::api::client::account::request_openid_token::v3::Response::new(
|
||||
"open-id-token".to_owned(),
|
||||
TokenType::Bearer,
|
||||
ServerName::parse("example.com").expect("valid server name"),
|
||||
Duration::from_secs(3_600),
|
||||
);
|
||||
|
||||
let token: OpenIdToken = response.into();
|
||||
|
||||
assert_eq!(token.access_token, "open-id-token");
|
||||
assert_eq!(token.token_type, "Bearer");
|
||||
assert_eq!(token.matrix_server_name, "example.com");
|
||||
assert_eq!(token.expires_in_seconds, 3_600);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Allow UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[cfg(not(any(target_family = "wasm", target_os = "android")))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, ThreadingSupport,
|
||||
cross_process_lock::CrossProcessLockConfig as SdkCrossProcessLockConfig,
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
ruma::{ServerName, UserId},
|
||||
search_index::SearchIndexStoreKind,
|
||||
sliding_sync::{
|
||||
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
@@ -115,14 +131,14 @@ pub struct ClientBuilder {
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder,
|
||||
disable_automatic_token_refresh: bool,
|
||||
cross_process_store_locks_holder_name: Option<String>,
|
||||
enable_oidc_refresh_lock: bool,
|
||||
cross_process_lock_config: CrossProcessLockConfig,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
encryption_settings: EncryptionSettings,
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_settings: DecryptionSettings,
|
||||
enable_share_history_on_invite: bool,
|
||||
request_config: Option<RequestConfig>,
|
||||
search_index_store: Option<SearchIndexStoreKind>,
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
user_agent: Option<String>,
|
||||
@@ -160,8 +176,7 @@ impl ClientBuilder {
|
||||
#[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,
|
||||
cross_process_lock_config: CrossProcessLockConfig::SingleProcess,
|
||||
session_delegate: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Default::default(),
|
||||
@@ -180,21 +195,16 @@ impl ClientBuilder {
|
||||
enable_share_history_on_invite: false,
|
||||
request_config: Default::default(),
|
||||
threading_support: ThreadingSupport::Disabled,
|
||||
search_index_store: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
pub fn cross_process_lock_config(
|
||||
self: Arc<Self>,
|
||||
holder_name: String,
|
||||
cross_process_lock_config: CrossProcessLockConfig,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.cross_process_store_locks_holder_name = Some(holder_name);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.enable_oidc_refresh_lock = true;
|
||||
builder.cross_process_lock_config = cross_process_lock_config;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -350,14 +360,41 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set up the search index store for this client, which is used to store
|
||||
/// the message search index locally.
|
||||
///
|
||||
/// As soon as this is enabled, messages will start to be indexed, and can
|
||||
/// be later queried for search.
|
||||
///
|
||||
/// `path` is the directory where the search index will be stored. It must
|
||||
/// be unique per session.
|
||||
///
|
||||
/// `password` is an optional password to encrypt the search index at rest.
|
||||
/// If `None`, the search index will be stored unencrypted.
|
||||
pub fn with_search_index_store(
|
||||
self: Arc<Self>,
|
||||
path: String,
|
||||
password: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
|
||||
// Note: creation of the path is deferred to later.
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
let kind = if let Some(password) = password {
|
||||
SearchIndexStoreKind::EncryptedDirectory(path, password)
|
||||
} else {
|
||||
SearchIndexStoreKind::UnencryptedDirectory(path)
|
||||
};
|
||||
|
||||
builder.search_index_store = Some(kind);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
|
||||
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
|
||||
inner_builder =
|
||||
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
|
||||
}
|
||||
let mut inner_builder = MatrixClient::builder()
|
||||
.cross_process_store_config(builder.cross_process_lock_config.into());
|
||||
|
||||
let store_path = if let Some(store) = &builder.store {
|
||||
match store.build()? {
|
||||
@@ -382,6 +419,20 @@ impl ClientBuilder {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(search_index_store) = builder.search_index_store {
|
||||
// Create the search index directory.
|
||||
match search_index_store {
|
||||
SearchIndexStoreKind::UnencryptedDirectory(ref path)
|
||||
| SearchIndexStoreKind::EncryptedDirectory(ref path, _) => {
|
||||
fs::create_dir_all(path)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Configure the inner builder to use the search index store.
|
||||
inner_builder = inner_builder.search_index_store(search_index_store);
|
||||
}
|
||||
|
||||
// Determine server either from URL, server name or user ID.
|
||||
inner_builder = match builder.homeserver_cfg {
|
||||
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
|
||||
@@ -406,27 +457,34 @@ impl ClientBuilder {
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut certificates = Vec::new();
|
||||
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
inner_builder =
|
||||
inner_builder.add_raw_root_certificates(builder.additional_root_certificates)
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let mut certificates = Vec::new();
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
match Certificate::from_der(&certificate) {
|
||||
Ok(cert) => {
|
||||
certificates.push(cert);
|
||||
}
|
||||
Err(der_error) => {
|
||||
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
|
||||
ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
|
||||
}
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
}
|
||||
|
||||
if builder.disable_built_in_root_certificates {
|
||||
inner_builder = inner_builder.disable_built_in_root_certificates();
|
||||
@@ -480,12 +538,11 @@ impl ClientBuilder {
|
||||
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(
|
||||
max_concurrent_requests as usize,
|
||||
));
|
||||
}
|
||||
if let Some(max_concurrent_requests) = config.max_concurrent_requests
|
||||
&& max_concurrent_requests > 0
|
||||
{
|
||||
updated_config = updated_config
|
||||
.max_concurrent_requests(NonZeroUsize::new(max_concurrent_requests as usize));
|
||||
}
|
||||
if let Some(max_retry_time) = config.max_retry_time {
|
||||
updated_config =
|
||||
@@ -498,15 +555,7 @@ impl ClientBuilder {
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(
|
||||
sdk_client,
|
||||
builder.enable_oidc_refresh_lock,
|
||||
builder.session_delegate,
|
||||
store_path,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
Ok(Arc::new(Client::new(sdk_client, builder.session_delegate, store_path).await?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,3 +673,27 @@ pub enum SlidingSyncVersionBuilder {
|
||||
Native,
|
||||
DiscoverNative,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, uniffi::Enum)]
|
||||
/// The cross-process lock config to use.
|
||||
pub enum CrossProcessLockConfig {
|
||||
/// The client will run using multiple processes.
|
||||
MultiProcess {
|
||||
/// The holder name to use for the lock.
|
||||
holder_name: String,
|
||||
},
|
||||
/// The client will run in a single process, there is no need for a
|
||||
/// cross-process lock.
|
||||
SingleProcess,
|
||||
}
|
||||
|
||||
impl From<CrossProcessLockConfig> for SdkCrossProcessLockConfig {
|
||||
fn from(lock_config: CrossProcessLockConfig) -> Self {
|
||||
match lock_config {
|
||||
CrossProcessLockConfig::MultiProcess { holder_name } => {
|
||||
SdkCrossProcessLockConfig::MultiProcess { holder_name }
|
||||
}
|
||||
CrossProcessLockConfig::SingleProcess => SdkCrossProcessLockConfig::SingleProcess,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
use std::sync::Arc;
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption,
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use matrix_sdk::encryption::{self, backups, recovery};
|
||||
use matrix_sdk_base::crypto::types::{BackupSecrets, RoomKeyBackupInfo};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::OwnedUserId;
|
||||
use serde::de::Error;
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
@@ -224,6 +238,225 @@ impl From<encryption::VerificationState> for VerificationState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct containing the bundle of secrets to fully activate a new device for
|
||||
/// end-to-end encryption.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SecretsBundleWithUserId {
|
||||
user_id: OwnedUserId,
|
||||
inner: matrix_sdk_base::crypto::types::SecretsBundle,
|
||||
}
|
||||
|
||||
/// Result for the check if a store has a valid secrets bundle.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DetectedSecretsBundle {
|
||||
/// The store doesn't contain a secrets bundle at all.
|
||||
None,
|
||||
/// The store contains a bundle without a backup.
|
||||
WithoutBackup,
|
||||
/// The store contains a bundle with an unused backup, the backup key in the
|
||||
/// bundle isn't used on the homeserver.
|
||||
UnusedBackup,
|
||||
/// The store contains a complete secrets bundle.
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// Error type describing failures that can happen while exporting a
|
||||
/// [`SecretsBundle`] from a SQLite store.
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum BundleExportError {
|
||||
/// The SQLite store couldn't be opened.
|
||||
#[error("the store couldn't be opened: {msg}")]
|
||||
OpenStoreError { msg: String },
|
||||
/// Data from the SQLite store couldn't be exported.
|
||||
#[error("the bundle couldn't be exported due to a storage error: {msg}")]
|
||||
StoreError { msg: String },
|
||||
/// The store doesn't contain a secrets bundle or it couldn't be read from
|
||||
/// the store.
|
||||
#[error("the bundle couldn't be exported: {msg}")]
|
||||
SecretError { msg: String },
|
||||
/// The store is empty and doesn't contain a secrets bundle.
|
||||
#[error("the store is completely empty")]
|
||||
StoreEmpty,
|
||||
/// A JSON object couldn't be deserialized while the secrets bundle was
|
||||
/// exported.
|
||||
#[error("Couldn't deserialize a JSON value: {msg}")]
|
||||
Json { msg: String },
|
||||
/// Error returned when the secrets bundle is missing a backup key or
|
||||
/// includes one that doesn’t match the key configured for the active backup
|
||||
/// version.
|
||||
#[error(
|
||||
"The bundle is missing a backup key or has one that isn't the one that's currently used"
|
||||
)]
|
||||
InvalidBackup,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
impl From<matrix_sdk::encryption::BundleExportError> for BundleExportError {
|
||||
fn from(value: matrix_sdk::encryption::BundleExportError) -> Self {
|
||||
match value {
|
||||
matrix_sdk::encryption::BundleExportError::OpenStoreError(e) => {
|
||||
BundleExportError::OpenStoreError { msg: e.to_string() }
|
||||
}
|
||||
matrix_sdk::encryption::BundleExportError::StoreError(e) => {
|
||||
BundleExportError::StoreError { msg: e.to_string() }
|
||||
}
|
||||
matrix_sdk::encryption::BundleExportError::SecretExport(e) => {
|
||||
BundleExportError::SecretError { msg: e.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for BundleExportError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
Self::Json { msg: value.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SecretsBundleWithUserId {
|
||||
/// Attempt to export a [`SecretsBundle`] from a crypto store.
|
||||
///
|
||||
/// This method can be used to retrieve a [`SecretsBundle`] from an existing
|
||||
/// `matrix-sdk`-based client in order to import the [`SecretsBundle`] in
|
||||
/// another [`Client`] instance.
|
||||
///
|
||||
/// This can be useful for migration purposes or to allow existing client
|
||||
/// instances create new ones that will be fully verified.
|
||||
#[uniffi::constructor]
|
||||
pub async fn from_database(
|
||||
database_path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
backup_info: &str,
|
||||
) -> Result<Arc<Self>, BundleExportError> {
|
||||
let backup_info = serde_json::from_str(backup_info)?;
|
||||
|
||||
let ret = if let Some((user_id, bundle)) =
|
||||
matrix_sdk::encryption::export_secrets_bundle_from_store(
|
||||
database_path,
|
||||
passphrase.as_deref(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let is_backup_ok =
|
||||
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
|
||||
|
||||
if is_backup_ok {
|
||||
Ok(SecretsBundleWithUserId { user_id, inner: bundle }.into())
|
||||
} else {
|
||||
Err(BundleExportError::InvalidBackup)
|
||||
}
|
||||
} else {
|
||||
Err(BundleExportError::StoreEmpty)
|
||||
};
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SecretsBundleWithUserId {
|
||||
/// Attempt to create a [`SecretsBundle`] from a previously JSON serialized
|
||||
/// bundle.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_str(
|
||||
user_id: &str,
|
||||
bundle: &str,
|
||||
backup_info: &str,
|
||||
) -> Result<Arc<Self>, BundleExportError> {
|
||||
let user_id =
|
||||
OwnedUserId::from_str(user_id).map_err(|e| serde_json::Error::custom(e.to_string()))?;
|
||||
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
|
||||
let backup_info = serde_json::from_str(backup_info)?;
|
||||
|
||||
let is_backup_ok =
|
||||
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
|
||||
|
||||
if is_backup_ok {
|
||||
Ok(Self { user_id, inner: bundle }.into())
|
||||
} else {
|
||||
Err(BundleExportError::InvalidBackup)
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the bundle contain a backup key.
|
||||
///
|
||||
/// Since enabling a backup is optional, the backup key might be missing
|
||||
/// from the bundle. Returns `false` if the backup key is missing,
|
||||
/// otherwise `true`.
|
||||
pub fn contains_backup_key(&self) -> bool {
|
||||
self.inner.backup.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_backup(secrets: &BackupSecrets, info: &RoomKeyBackupInfo) -> bool {
|
||||
match secrets {
|
||||
BackupSecrets::MegolmBackupV1Curve25519AesSha2(secrets) => {
|
||||
secrets.key.backup_key_matches(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_bundle_and_info(
|
||||
bundle: &matrix_sdk_base::crypto::types::SecretsBundle,
|
||||
info: Option<&RoomKeyBackupInfo>,
|
||||
) -> DetectedSecretsBundle {
|
||||
match (&bundle.backup, info) {
|
||||
(None, None) => DetectedSecretsBundle::WithoutBackup,
|
||||
(None, Some(_)) => DetectedSecretsBundle::WithoutBackup,
|
||||
(Some(_), None) => DetectedSecretsBundle::UnusedBackup,
|
||||
(Some(backup), Some(info)) => {
|
||||
if is_valid_backup(backup, info) {
|
||||
DetectedSecretsBundle::Complete
|
||||
} else {
|
||||
DetectedSecretsBundle::UnusedBackup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a JSON encoded string contains a valid [`SecretsBundle`].
|
||||
#[uniffi::export]
|
||||
pub fn json_string_contains_secrets_bundle(
|
||||
bundle: &str,
|
||||
backup_info: Option<String>,
|
||||
) -> Result<DetectedSecretsBundle, ClientError> {
|
||||
let info: Option<RoomKeyBackupInfo> =
|
||||
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
|
||||
|
||||
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
|
||||
|
||||
Ok(check_bundle_and_info(&bundle, info.as_ref()))
|
||||
}
|
||||
|
||||
/// Check if a crypto store contains a valid [`SecretsBundle`].
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub async fn database_contains_secrets_bundle(
|
||||
database_path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
backup_info: Option<String>,
|
||||
) -> Result<DetectedSecretsBundle, BundleExportError> {
|
||||
let info: Option<RoomKeyBackupInfo> =
|
||||
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
|
||||
|
||||
let maybe_bundle = matrix_sdk::encryption::export_secrets_bundle_from_store(
|
||||
database_path,
|
||||
passphrase.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
Ok(match maybe_bundle {
|
||||
Some((_, bundle)) => check_bundle_and_info(&bundle, info.as_ref()),
|
||||
None => DetectedSecretsBundle::None,
|
||||
})
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Encryption {
|
||||
/// Get the public ed25519 key of our own device. This is usually what is
|
||||
@@ -398,6 +631,7 @@ impl Encryption {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Download identity and key backup information from Recovery
|
||||
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
|
||||
let result = self.inner.recovery().recover(&recovery_key).await;
|
||||
|
||||
@@ -406,6 +640,23 @@ impl Encryption {
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
/// Download identity and key backup information from Recovery, and, if the
|
||||
/// key backup information is inconsistent, create a new key backup.
|
||||
///
|
||||
/// This will create a new key backup if:
|
||||
///
|
||||
/// * Key backup is enabled and the backup decryption key is missing from
|
||||
/// Recovery, or
|
||||
/// * Key backup is enabled and the backup decryption key does not match the
|
||||
/// public key
|
||||
pub async fn recover_and_fix_backup(&self, mut recovery_key: String) -> Result<()> {
|
||||
let result = self.inner.recovery().recover_and_fix_backup(&recovery_key).await;
|
||||
|
||||
recovery_key.zeroize();
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub fn verification_state(&self) -> VerificationState {
|
||||
self.inner.verification_state().get().into()
|
||||
}
|
||||
@@ -473,6 +724,43 @@ impl Encryption {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// This method will import all the private cross-signing keys and
|
||||
/// the private part of a backup key and its accompanying version into the
|
||||
/// store.
|
||||
///
|
||||
/// Importing all the secrets will mark the device as verified and enable
|
||||
/// backups.
|
||||
///
|
||||
/// **Warning**: Only import this from a trusted source, i.e. if an existing
|
||||
/// device is sharing this with a new device.
|
||||
///
|
||||
/// **Warning*: Only call this method right after logging in and before the
|
||||
/// initial sync has been started.
|
||||
pub async fn import_secrets_bundle(
|
||||
&self,
|
||||
secrets_bundle: &SecretsBundleWithUserId,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = self._client.inner.user_id().expect(
|
||||
"We should have a user ID available now, this is only called once we're logged in",
|
||||
);
|
||||
|
||||
if user_id == secrets_bundle.user_id {
|
||||
self.inner
|
||||
.import_secrets_bundle(&secrets_bundle.inner)
|
||||
.await
|
||||
.map_err(ClientError::from_err)?;
|
||||
|
||||
self.inner.wait_for_e2ee_initialization_tasks().await;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ClientError::Generic {
|
||||
msg: "Secrets bundle does not belong to the user which was logged in".to_owned(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The E2EE identity of a user.
|
||||
@@ -563,11 +851,7 @@ impl IdentityResetHandle {
|
||||
/// 3. Go through the cross-signing key reset flow
|
||||
/// 4. Finally, re-enable key backups only if they were enabled before
|
||||
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
|
||||
if let Some(auth) = auth {
|
||||
self.inner.reset(Some(auth.into())).await.map_err(ClientError::from_err)
|
||||
} else {
|
||||
self.inner.reset(None).await.map_err(ClientError::from_err)
|
||||
}
|
||||
self.inner.reset(auth.map(Into::into)).await.map_err(ClientError::from_err)
|
||||
}
|
||||
|
||||
pub async fn cancel(&self) {
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
authentication::oauth::OAuthError,
|
||||
encryption::{identities::RequestVerificationError, CryptoStoreError},
|
||||
encryption::{CryptoStoreError, identities::RequestVerificationError},
|
||||
event_cache::EventCacheError,
|
||||
reqwest,
|
||||
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, spaces, sync_service, timeline};
|
||||
use ruma::{
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
api::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
|
||||
};
|
||||
use tracing::warn;
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
@@ -30,7 +43,6 @@ pub enum ClientError {
|
||||
|
||||
impl ClientError {
|
||||
pub(crate) fn from_str<E: Display>(error: E, details: Option<String>) -> Self {
|
||||
warn!("Error: {error}");
|
||||
Self::Generic { msg: error.to_string(), details }
|
||||
}
|
||||
|
||||
@@ -63,22 +75,21 @@ impl From<matrix_sdk::Error> for ClientError {
|
||||
fn from(e: matrix_sdk::Error) -> Self {
|
||||
match e {
|
||||
matrix_sdk::Error::Http(http_error) => {
|
||||
if let Some(api_error) = http_error.as_client_api_error() {
|
||||
if let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
|
||||
if let Some(api_error) = http_error.as_client_api_error()
|
||||
&& 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
|
||||
return (*http_error).into();
|
||||
};
|
||||
return Self::MatrixApi {
|
||||
kind,
|
||||
code,
|
||||
msg: message.to_owned(),
|
||||
details: Some(format!("{api_error:?}")),
|
||||
};
|
||||
}
|
||||
{
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return (*http_error).into();
|
||||
};
|
||||
return Self::MatrixApi {
|
||||
kind,
|
||||
code,
|
||||
msg: message.to_owned(),
|
||||
details: Some(format!("{api_error:?}")),
|
||||
};
|
||||
}
|
||||
(*http_error).into()
|
||||
}
|
||||
@@ -334,6 +345,39 @@ pub enum RoomError {
|
||||
FailedSendingAttachment,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum LiveLocationError {
|
||||
#[error("Network error")]
|
||||
Network,
|
||||
#[error("Existing beacon information not found")]
|
||||
NotFound,
|
||||
#[error("Beacon event is redacted and cannot be processed")]
|
||||
Redacted,
|
||||
#[error("Must join the room to access beacon information")]
|
||||
Stripped,
|
||||
#[error("The beacon event has expired")]
|
||||
NotLive,
|
||||
#[error("Deserialization error")]
|
||||
Deserialization,
|
||||
#[error("Other error: {msg}")]
|
||||
Other { msg: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::BeaconError> for LiveLocationError {
|
||||
fn from(value: matrix_sdk::BeaconError) -> Self {
|
||||
match value {
|
||||
matrix_sdk::BeaconError::Network(_) => Self::Network,
|
||||
matrix_sdk::BeaconError::NotFound => Self::NotFound,
|
||||
matrix_sdk::BeaconError::Redacted => Self::Redacted,
|
||||
matrix_sdk::BeaconError::Stripped => Self::Stripped,
|
||||
matrix_sdk::BeaconError::Deserialization(_) => Self::Deserialization,
|
||||
matrix_sdk::BeaconError::NotLive => Self::NotLive,
|
||||
matrix_sdk::BeaconError::Other(err) => Self::Other { msg: err.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum MediaInfoError {
|
||||
@@ -736,7 +780,7 @@ pub enum ErrorKind {
|
||||
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
|
||||
WrongRoomKeysVersion {
|
||||
/// The currently active backup version.
|
||||
current_version: Option<String>,
|
||||
current_version: String,
|
||||
},
|
||||
|
||||
/// A custom API error.
|
||||
@@ -750,9 +794,9 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
|
||||
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
|
||||
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
|
||||
RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
|
||||
status: status.map(|code| code.clone().as_u16()),
|
||||
body: body.clone(),
|
||||
RumaApiErrorKind::BadStatus(bad_status) => Ok(ErrorKind::BadStatus {
|
||||
status: bad_status.status.map(|code| code.as_u16()),
|
||||
body: bad_status.body.clone(),
|
||||
}),
|
||||
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
|
||||
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
|
||||
@@ -764,16 +808,18 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
|
||||
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
|
||||
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
|
||||
RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
|
||||
RumaApiErrorKind::Forbidden => Ok(ErrorKind::Forbidden),
|
||||
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
|
||||
RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
|
||||
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
|
||||
RumaApiErrorKind::IncompatibleRoomVersion(incompatible_room_version) => {
|
||||
Ok(ErrorKind::IncompatibleRoomVersion {
|
||||
room_version: incompatible_room_version.room_version.to_string(),
|
||||
})
|
||||
}
|
||||
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
|
||||
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
|
||||
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
|
||||
RumaApiErrorKind::LimitExceeded { retry_after } => {
|
||||
let retry_after_ms = match retry_after {
|
||||
RumaApiErrorKind::LimitExceeded(limit_exceeded) => {
|
||||
let retry_after_ms = match &limit_exceeded.retry_after {
|
||||
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
|
||||
Some(RetryAfter::DateTime(system_time)) => {
|
||||
let duration = MilliSecondsSinceUnixEpoch::now()
|
||||
@@ -790,8 +836,10 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
|
||||
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
|
||||
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
|
||||
RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
|
||||
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
|
||||
RumaApiErrorKind::ResourceLimitExceeded(resource_limit_exceeded) => {
|
||||
Ok(ErrorKind::ResourceLimitExceeded {
|
||||
admin_contact: resource_limit_exceeded.admin_contact.clone(),
|
||||
})
|
||||
}
|
||||
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
|
||||
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
|
||||
@@ -807,8 +855,8 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
|
||||
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
|
||||
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
|
||||
RumaApiErrorKind::UnknownToken { soft_logout } => {
|
||||
Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
|
||||
RumaApiErrorKind::UnknownToken(unknown_token) => {
|
||||
Ok(ErrorKind::UnknownToken { soft_logout: unknown_token.soft_logout.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
|
||||
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
|
||||
@@ -818,10 +866,12 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
|
||||
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
|
||||
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
|
||||
RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
|
||||
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
|
||||
RumaApiErrorKind::WrongRoomKeysVersion(wrong_version) => {
|
||||
Ok(ErrorKind::WrongRoomKeysVersion {
|
||||
current_version: wrong_version.current_version.clone(),
|
||||
})
|
||||
}
|
||||
RumaApiErrorKind::_Custom { .. } => {
|
||||
RumaApiErrorKind::_Custom(_) => {
|
||||
// There is no way to map the extra values since they're private, so we omit
|
||||
// them
|
||||
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
use anyhow::{bail, Context};
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
EventId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
TimelineEventType as RumaTimelineEventType,
|
||||
room::{
|
||||
encrypted,
|
||||
message::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
TimelineEventType as RumaTimelineEventType,
|
||||
},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, RtcNotificationType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, RtcCallIntent, RtcNotificationType},
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -75,6 +89,7 @@ impl From<AnyTimelineEvent> for TimelineEvent {
|
||||
|
||||
/// The timeline event type.
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
#[uniffi::export(Eq, Hash)]
|
||||
pub enum TimelineEventType {
|
||||
/// The event is a message-like one and should be displayed as such.
|
||||
MessageLike { value: MessageLikeEventType },
|
||||
@@ -210,9 +225,6 @@ impl From<RumaTimelineEventType> for TimelineEventType {
|
||||
RumaTimelineEventType::PolicyRuleUser => {
|
||||
Self::State { value: StateEventType::PolicyRuleUser }
|
||||
}
|
||||
RumaTimelineEventType::RoomAliases => {
|
||||
Self::State { value: StateEventType::RoomAliases }
|
||||
}
|
||||
RumaTimelineEventType::RoomAvatar => Self::State { value: StateEventType::RoomAvatar },
|
||||
RumaTimelineEventType::RoomCanonicalAlias => {
|
||||
Self::State { value: StateEventType::RoomCanonicalAlias }
|
||||
@@ -292,7 +304,6 @@ pub enum StateEventContent {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
@@ -320,7 +331,6 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
AnySyncStateEvent::PolicyRuleRoom(_) => StateEventContent::PolicyRuleRoom,
|
||||
AnySyncStateEvent::PolicyRuleServer(_) => StateEventContent::PolicyRuleServer,
|
||||
AnySyncStateEvent::PolicyRuleUser(_) => StateEventContent::PolicyRuleUser,
|
||||
AnySyncStateEvent::RoomAliases(_) => StateEventContent::RoomAliases,
|
||||
AnySyncStateEvent::RoomAvatar(_) => StateEventContent::RoomAvatar,
|
||||
AnySyncStateEvent::RoomCanonicalAlias(_) => StateEventContent::RoomCanonicalAlias,
|
||||
AnySyncStateEvent::RoomCreate(_) => StateEventContent::RoomCreate,
|
||||
@@ -371,6 +381,8 @@ pub enum MessageLikeEventContent {
|
||||
notification_type: RtcNotificationType,
|
||||
/// The timestamp at which this notification is considered invalid.
|
||||
expiration_ts: Timestamp,
|
||||
/// Soft indication of whether it is an audio or video call.
|
||||
call_intent: Option<RtcCallIntent>,
|
||||
},
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
@@ -413,6 +425,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
MessageLikeEventContent::RtcNotification {
|
||||
notification_type: original_content.notification_type.into(),
|
||||
expiration_ts,
|
||||
call_intent: original_content.call_intent.map(|intent| intent.into()),
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
|
||||
@@ -455,7 +468,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
let original_content = get_message_like_event_original_content(content)?;
|
||||
let in_reply_to_event_id =
|
||||
original_content.relates_to.and_then(|relation| match relation {
|
||||
Relation::Reply { in_reply_to } => Some(in_reply_to.event_id.to_string()),
|
||||
Relation::Reply(reply) => Some(reply.in_reply_to.event_id.to_string()),
|
||||
_ => None,
|
||||
});
|
||||
MessageLikeEventContent::RoomMessage {
|
||||
@@ -509,7 +522,6 @@ pub enum StateEventType {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
@@ -541,7 +553,6 @@ impl From<StateEventType> for ruma::events::StateEventType {
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAliases => Self::RoomAliases,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) fn unwrap_or_clone_arc<T: Clone>(arc: Arc<T>) -> T {
|
||||
|
||||
@@ -24,13 +24,14 @@ mod room_member;
|
||||
mod room_preview;
|
||||
mod ruma;
|
||||
mod runtime;
|
||||
mod search;
|
||||
mod session_verification;
|
||||
mod spaces;
|
||||
mod store;
|
||||
mod sync_service;
|
||||
mod sync_v2;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod tracing;
|
||||
mod utd;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
@@ -9,24 +9,156 @@
|
||||
// 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
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use crate::ruma::LocationContent;
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt as _;
|
||||
use matrix_sdk::live_location_share::{
|
||||
LiveLocationShare as SdkLiveLocationShare, LiveLocationShares as SdkLiveLocationShares,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::{ruma::LocationContent, runtime::get_runtime_handle, task_handle::TaskHandle};
|
||||
|
||||
/// Details of the last known location beacon.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
/// The most recent location content of the user.
|
||||
/// The most recent location content shared for this asset.
|
||||
pub location: LocationContent,
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
/// The timestamp of when the location was updated.
|
||||
pub ts: u64,
|
||||
}
|
||||
/// Details of a users live location share.
|
||||
|
||||
/// Details of a user's live location share.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LiveLocationShare {
|
||||
/// The user's last known location.
|
||||
pub last_location: LastLocation,
|
||||
/// The live status of the live location share.
|
||||
pub(crate) is_live: bool,
|
||||
/// The asset's last known location.
|
||||
pub last_location: Option<LastLocation>,
|
||||
/// The user ID of the person sharing their live location.
|
||||
pub user_id: String,
|
||||
/// The time when location sharing started.
|
||||
pub start_ts: u64,
|
||||
/// The duration that the location sharing will be live.
|
||||
/// Meaning that the location will stop being shared at ts + timeout.
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
/// An update to the list of active live location shares.
|
||||
///
|
||||
/// Corresponds to a [`VectorDiff`] on the underlying [`ObservableVector`].
|
||||
///
|
||||
/// [`ObservableVector`]: eyeball_im::ObservableVector
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum LiveLocationShareUpdate {
|
||||
Append { values: Vec<LiveLocationShare> },
|
||||
Clear,
|
||||
PushFront { value: LiveLocationShare },
|
||||
PushBack { value: LiveLocationShare },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: LiveLocationShare },
|
||||
Set { index: u32, value: LiveLocationShare },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<LiveLocationShare> },
|
||||
}
|
||||
|
||||
/// Listener for live location share updates.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
/// Called with a batch of [`LiveLocationShareUpdate`]s whenever the list
|
||||
/// of active shares changes.
|
||||
fn on_update(&self, updates: Vec<LiveLocationShareUpdate>);
|
||||
}
|
||||
|
||||
/// Tracks active live location shares in a room.
|
||||
///
|
||||
/// Holds the SDK [`SdkLiveLocationShares`] which keeps the beacon and
|
||||
/// beacon_info event handlers registered for as long as this object is alive.
|
||||
/// Call [`LiveLocationShares::subscribe`] to start receiving updates.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LiveLocationShares {
|
||||
inner: SdkLiveLocationShares,
|
||||
}
|
||||
|
||||
impl LiveLocationShares {
|
||||
pub fn new(inner: SdkLiveLocationShares) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LiveLocationShares {
|
||||
/// Subscribe to changes in the list of active live location shares.
|
||||
///
|
||||
/// Immediately calls `listener` with a `Reset` update containing the
|
||||
/// current snapshot (if non-empty), then calls it again for every
|
||||
/// subsequent change that arrives from sync.
|
||||
///
|
||||
/// Returns a [`TaskHandle`] that, when dropped, stops the listener.
|
||||
/// The event handlers remain registered for as long as this
|
||||
/// [`LiveLocationShares`] object is alive.
|
||||
pub fn subscribe(&self, listener: Box<dyn LiveLocationShareListener>) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe();
|
||||
|
||||
if !initial_values.is_empty() {
|
||||
listener.on_update(vec![LiveLocationShareUpdate::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());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SdkLiveLocationShare> for LiveLocationShare {
|
||||
fn from(share: SdkLiveLocationShare) -> Self {
|
||||
let start_ts = share.beacon_info.ts.0.into();
|
||||
let timeout = share.beacon_info.timeout.as_millis() as u64;
|
||||
let asset = share.beacon_info.asset.type_.into();
|
||||
let last_location = share.last_location.map(|l| LastLocation {
|
||||
location: LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: l.location.uri.to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset,
|
||||
},
|
||||
ts: l.ts.0.into(),
|
||||
});
|
||||
LiveLocationShare { user_id: share.user_id.to_string(), last_location, start_ts, timeout }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VectorDiff<SdkLiveLocationShare>> for LiveLocationShareUpdate {
|
||||
fn from(diff: VectorDiff<SdkLiveLocationShare>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(Into::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(Into::into).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
@@ -35,6 +49,7 @@ pub struct NotificationRoomInfo {
|
||||
pub topic: Option<String>,
|
||||
pub join_rule: Option<JoinRule>,
|
||||
pub joined_members_count: u64,
|
||||
pub service_members: Vec<String>,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_space: bool,
|
||||
@@ -92,6 +107,7 @@ impl NotificationItem {
|
||||
topic: item.room_topic,
|
||||
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
|
||||
joined_members_count: item.joined_members_count,
|
||||
service_members: item.service_members,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_space: item.is_space,
|
||||
@@ -117,6 +133,8 @@ pub enum NotificationStatus {
|
||||
/// rules, or because the user which triggered it is ignored by the
|
||||
/// current user.
|
||||
EventFilteredOut,
|
||||
/// The event has been redacted.
|
||||
EventRedacted,
|
||||
}
|
||||
|
||||
impl From<SdkNotificationStatus> for NotificationStatus {
|
||||
@@ -127,6 +145,7 @@ impl From<SdkNotificationStatus> for NotificationStatus {
|
||||
}
|
||||
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
|
||||
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
|
||||
SdkNotificationStatus::EventRedacted => NotificationStatus::EventRedacted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use matrix_sdk::{
|
||||
Client as MatrixClient,
|
||||
event_handler::EventHandlerHandle,
|
||||
notification_settings::{
|
||||
NotificationSettings as SdkNotificationSettings,
|
||||
RoomNotificationMode as SdkRoomNotificationMode,
|
||||
},
|
||||
ruma::events::push_rules::PushRulesEvent,
|
||||
Client as MatrixClient,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
Int, RoomId, UInt,
|
||||
events::push_rules::PushRulesEventContent,
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
|
||||
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, EventMatchConditionData,
|
||||
EventPropertyContainsConditionData, EventPropertyIsConditionData, HighlightTweakValue,
|
||||
PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition as SdkPushCondition,
|
||||
RoomMemberCountConditionData, RoomMemberCountIs, RuleKind as SdkRuleKind,
|
||||
ScalarJsonValue as SdkJsonValue, SenderNotificationPermissionConditionData,
|
||||
Tweak as SdkTweak,
|
||||
},
|
||||
Int, RoomId, UInt,
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
|
||||
@@ -167,20 +184,22 @@ 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 },
|
||||
SdkPushCondition::EventMatch(data) => {
|
||||
Self::EventMatch { key: data.key, pattern: data.pattern }
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
SdkPushCondition::RoomMemberCount { is } => {
|
||||
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
|
||||
SdkPushCondition::RoomMemberCount(data) => {
|
||||
Self::RoomMemberCount { prefix: data.is.prefix.into(), count: data.is.count.into() }
|
||||
}
|
||||
SdkPushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key: key.to_string() }
|
||||
SdkPushCondition::SenderNotificationPermission(data) => {
|
||||
Self::SenderNotificationPermission { key: data.key.to_string() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
SdkPushCondition::EventPropertyIs(data) => {
|
||||
Self::EventPropertyIs { key: data.key, value: data.value.into() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
SdkPushCondition::EventPropertyContains(data) => {
|
||||
Self::EventPropertyContains { key: data.key, value: data.value.into() }
|
||||
}
|
||||
_ => return Err("Unsupported condition type".to_owned()),
|
||||
})
|
||||
@@ -190,24 +209,28 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
impl From<PushCondition> for SdkPushCondition {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
PushCondition::EventMatch { key, pattern } => {
|
||||
Self::EventMatch(EventMatchConditionData::new(key, pattern))
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
|
||||
is: RoomMemberCountIs {
|
||||
PushCondition::RoomMemberCount { prefix, count } => {
|
||||
Self::RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs {
|
||||
prefix: prefix.into(),
|
||||
count: UInt::new(count).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key: key.into() }
|
||||
Self::SenderNotificationPermission(SenderNotificationPermissionConditionData::new(
|
||||
key.into(),
|
||||
))
|
||||
}
|
||||
PushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
}
|
||||
PushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
Self::EventPropertyIs(EventPropertyIsConditionData::new(key, value.into()))
|
||||
}
|
||||
PushCondition::EventPropertyContains { key, value } => Self::EventPropertyContains(
|
||||
EventPropertyContainsConditionData::new(key, value.into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,16 +312,19 @@ impl TryFrom<SdkTweak> for Tweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkTweak::Sound(sound) => Self::Sound { value: sound },
|
||||
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
|
||||
SdkTweak::Custom { name, value } => {
|
||||
let json_string = serde_json::to_string(&value)
|
||||
.map_err(|e| format!("Failed to serialize custom tweak value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value: json_string }
|
||||
Ok(match &value {
|
||||
SdkTweak::Sound(sound) => Self::Sound { value: sound.to_string() },
|
||||
SdkTweak::Highlight(highlight) => {
|
||||
Self::Highlight { value: matches!(highlight, HighlightTweakValue::Yes) }
|
||||
}
|
||||
_ => {
|
||||
let json_string = value
|
||||
.custom_value()
|
||||
.ok_or_else(|| "Unsupported tweak type".to_owned())?
|
||||
.to_string();
|
||||
|
||||
Self::Custom { name: value.set_tweak().to_owned(), value: json_string }
|
||||
}
|
||||
_ => return Err("Unsupported tweak type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -308,16 +334,16 @@ impl TryFrom<Tweak> for SdkTweak {
|
||||
|
||||
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Tweak::Sound { value } => Self::Sound(value),
|
||||
Tweak::Highlight { value } => Self::Highlight(value),
|
||||
Tweak::Custom { name, value } => {
|
||||
let json_value: serde_json::Value = serde_json::from_str(&value)
|
||||
.map_err(|e| format!("Failed to deserialize custom tweak value: {e}"))?;
|
||||
let value = serde_json::from_value(json_value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value }
|
||||
}
|
||||
Tweak::Sound { value } => Self::Sound(value.into()),
|
||||
Tweak::Highlight { value } => Self::Highlight(value.into()),
|
||||
Tweak::Custom { name, value } => Self::new(
|
||||
name,
|
||||
Some(
|
||||
serde_json::value::RawValue::from_string(value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?,
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("Failed to convert custom tweak: {e}"))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use std::{error::Error, mem::MaybeUninit};
|
||||
|
||||
use jni::{
|
||||
errors::JniError,
|
||||
sys::{JNI_OK, JavaVM as RawJavaVM},
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
static ANDROID_JVM: once_cell::sync::OnceCell<jni::JavaVM> = once_cell::sync::OnceCell::new();
|
||||
|
||||
/// Initialize the platform support for Android targets.
|
||||
///
|
||||
/// This includes setting up `rustls-platform-verifier`.
|
||||
pub(crate) fn init() {
|
||||
debug!("Initializing Android platform support");
|
||||
|
||||
ANDROID_JVM.get_or_init(|| {
|
||||
match get_java_vm() {
|
||||
Ok(jvm) => {
|
||||
// Initialize rustls platform verifier
|
||||
let mut env =
|
||||
jvm.attach_current_thread_permanently().expect("Failed to attach thread");
|
||||
init_rustls_platform_verifier(&mut env)
|
||||
.expect("Failed to initialize rustls platform verifier");
|
||||
|
||||
debug!("Android platform support initialized successfully");
|
||||
|
||||
jvm
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Failed to initialize Android platform support: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn get_java_vm() -> Result<jni::JavaVM, Box<dyn Error>> {
|
||||
debug!("Getting a JVM pointer");
|
||||
#[allow(non_snake_case)]
|
||||
let JNI_GetCreatedJavaVMs = unsafe {
|
||||
jvm_getter::find_jni_get_created_java_vms().expect("Failed to find JNI_GetCreatedJavaVMs")
|
||||
};
|
||||
|
||||
let mut vm: MaybeUninit<*mut RawJavaVM> = MaybeUninit::uninit();
|
||||
let status = unsafe { JNI_GetCreatedJavaVMs(vm.as_mut_ptr(), 1, &mut 0) };
|
||||
if status != JNI_OK {
|
||||
panic!("no JavaVM was found by JNI_GetCreatedJavaVMs");
|
||||
}
|
||||
|
||||
unsafe { jni::JavaVM::from_raw(vm.assume_init()).map_err(|e| e.into()) }
|
||||
}
|
||||
|
||||
fn init_rustls_platform_verifier(env: &mut jni::JNIEnv<'_>) -> jni::errors::Result<()> {
|
||||
// Get the current activity thread
|
||||
let activity_thread = env
|
||||
.call_static_method(
|
||||
"android/app/ActivityThread",
|
||||
"currentActivityThread",
|
||||
"()Landroid/app/ActivityThread;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
|
||||
// Then get the application context
|
||||
let context = env
|
||||
.call_method(activity_thread, "getApplication", "()Landroid/app/Application;", &[])?
|
||||
.l()?;
|
||||
|
||||
Ok(rustls_platform_verifier::android::init_hosted(env, context)?)
|
||||
}
|
||||
|
||||
/// Attach the current thread to a JVM one.
|
||||
pub(crate) fn android_attach_current_thread_permanently()
|
||||
-> jni::errors::Result<jni::JNIEnv<'static>> {
|
||||
ANDROID_JVM
|
||||
.get()
|
||||
.ok_or_else(|| jni::errors::Error::JniCall(JniError::Unknown))?
|
||||
.attach_current_thread_permanently()
|
||||
}
|
||||
+109
-45
@@ -1,31 +1,61 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use ::tracing::info;
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing::warn;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use ::tracing::warn;
|
||||
use tracing_appender::rolling::Rotation;
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing_core::Level;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
EnvFilter, Layer, Registry,
|
||||
field::RecordFields,
|
||||
fmt::{
|
||||
self,
|
||||
self, FormatEvent, FormatFields, FormattedFields,
|
||||
format::{DefaultFields, Writer},
|
||||
time::FormatTime,
|
||||
FormatEvent, FormatFields, FormattedFields,
|
||||
},
|
||||
layer::{Layered, SubscriberExt as _},
|
||||
registry::LookupSpan,
|
||||
reload::{self, Handle},
|
||||
util::SubscriberInitExt as _,
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
use crate::error::ClientError;
|
||||
|
||||
/// Default maximum total size of all log files combined (10MB).
|
||||
const DEFAULT_MAX_TOTAL_SIZE_BYTES: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// Default maximum age of log files in seconds (1 week).
|
||||
const DEFAULT_MAX_AGE_SECONDS: u64 = 7 * 24 * 60 * 60;
|
||||
|
||||
mod rolling_writer;
|
||||
pub mod tracing;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android_platform;
|
||||
|
||||
use rolling_writer::SizeAndDateRollingWriter;
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use crate::tracing::BRIDGE_SPAN_NAME;
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
use self::tracing::BRIDGE_SPAN_NAME;
|
||||
use self::tracing::LogLevel;
|
||||
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
@@ -119,10 +149,10 @@ where
|
||||
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>()
|
||||
&& !fields.is_empty()
|
||||
{
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +183,7 @@ type ReloadHandle = Handle<
|
||||
Layered<EnvFilter, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
SizeAndDateRollingWriter,
|
||||
>,
|
||||
Layered<EnvFilter, Registry>,
|
||||
>;
|
||||
@@ -218,21 +248,17 @@ fn make_file_layer(
|
||||
Layered<EnvFilter, Registry, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
SizeAndDateRollingWriter,
|
||||
> {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&file_configuration.file_prefix);
|
||||
|
||||
if let Some(max_files) = file_configuration.max_files {
|
||||
builder = builder.max_log_files(max_files as usize)
|
||||
}
|
||||
if let Some(file_suffix) = file_configuration.file_suffix {
|
||||
builder = builder.filename_suffix(file_suffix)
|
||||
}
|
||||
|
||||
let writer =
|
||||
builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
&file_configuration.path,
|
||||
file_configuration.file_prefix,
|
||||
file_configuration.file_suffix.unwrap_or_else(|| String::from(".log")),
|
||||
Rotation::HOURLY,
|
||||
file_configuration.max_total_size_bytes.unwrap_or(DEFAULT_MAX_TOTAL_SIZE_BYTES),
|
||||
file_configuration.max_age_seconds.unwrap_or(DEFAULT_MAX_AGE_SECONDS),
|
||||
)
|
||||
.expect("Failed to create a rolling file appender.");
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
@@ -254,13 +280,30 @@ pub struct TracingFileConfiguration {
|
||||
file_prefix: String,
|
||||
|
||||
/// Optional suffix for the log file's names.
|
||||
///
|
||||
/// Default is ".log" if not specified.
|
||||
file_suffix: Option<String>,
|
||||
|
||||
/// Maximum number of rotated files.
|
||||
/// Maximum total size of all log files combined in bytes.
|
||||
///
|
||||
/// If not set, there's no max limit, i.e. the number of log files is
|
||||
/// unlimited.
|
||||
max_files: Option<u64>,
|
||||
/// When the total size of all log files with the configured prefix and
|
||||
/// suffix exceeds this limit, the oldest files will be removed until
|
||||
/// the total is below the limit.
|
||||
///
|
||||
/// This is useful to prevent log files from consuming too much disk space
|
||||
/// over time, even with multiple rotated files.
|
||||
///
|
||||
/// Default: 10MB (10 * 1024 * 1024 bytes) if not specified.
|
||||
max_total_size_bytes: Option<u64>,
|
||||
|
||||
/// Maximum age of log files in seconds.
|
||||
///
|
||||
/// Log files older than this age will be automatically removed during
|
||||
/// cleanup. This is checked when the writer is created and during
|
||||
/// rotation operations.
|
||||
///
|
||||
/// Default: 1 week (7 * 24 * 60 * 60 seconds) if not specified.
|
||||
max_age_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, PartialOrd)]
|
||||
@@ -451,9 +494,17 @@ pub struct TracingConfiguration {
|
||||
/// If set, configures rotated log files where to write additional logs.
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
|
||||
/// If set, the Sentry DSN to use for error reporting.
|
||||
/// If set, the Sentry configuration to use for error reporting.
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: Option<String>,
|
||||
sentry_config: Option<SentryConfig>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SentryConfig {
|
||||
dsn: String,
|
||||
app_version: String,
|
||||
app_platform: String,
|
||||
}
|
||||
|
||||
impl TracingConfiguration {
|
||||
@@ -462,7 +513,12 @@ impl TracingConfiguration {
|
||||
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
|
||||
fn build(mut self) -> LoggingCtx {
|
||||
// Show full backtraces, if we run into panics.
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
//
|
||||
// FIXME: Use safe API for this once stable. Tracking issue:
|
||||
// https://github.com/rust-lang/rust/issues/93346
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
// Log panics.
|
||||
log_panics::init();
|
||||
@@ -474,18 +530,14 @@ impl TracingConfiguration {
|
||||
{
|
||||
// Prepare the Sentry layer, if a DSN is provided.
|
||||
let (sentry_layer, sentry_logging_ctx) =
|
||||
if let Some(sentry_dsn) = self.sentry_dsn.take() {
|
||||
if let Some(sentry_config) = self.sentry_config.take() {
|
||||
// Initialize the Sentry client with the given options.
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_dsn,
|
||||
sentry_config.dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sampler: Some(Arc::new(|ctx| {
|
||||
// Make sure bridge spans are always uploaded
|
||||
if ctx.name() == BRIDGE_SPAN_NAME {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if ctx.name() == BRIDGE_SPAN_NAME { 1.0 } else { 0.0 }
|
||||
})),
|
||||
attach_stacktrace: true,
|
||||
release: Some(env!("VERGEN_GIT_SHA").into()),
|
||||
@@ -493,6 +545,11 @@ impl TracingConfiguration {
|
||||
},
|
||||
));
|
||||
|
||||
sentry::configure_scope(|scope| {
|
||||
scope.set_tag("app_version", sentry_config.app_version);
|
||||
scope.set_tag("app_platform", sentry_config.app_platform);
|
||||
});
|
||||
|
||||
let sentry_enabled = Arc::new(AtomicBool::new(true));
|
||||
|
||||
// Add a Sentry layer to the tracing subscriber.
|
||||
@@ -558,7 +615,7 @@ impl TracingConfiguration {
|
||||
}
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
info!(env_filter, "Logging has been set up");
|
||||
|
||||
logging_ctx
|
||||
}
|
||||
@@ -628,6 +685,9 @@ pub fn init_platform(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
android_platform::init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -685,6 +745,10 @@ fn setup_multithreaded_tokio_runtime() {
|
||||
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
builder.enable_all();
|
||||
#[cfg(target_os = "android")]
|
||||
builder.on_thread_start(|| {
|
||||
_ = android_platform::android_attach_current_thread_permanently();
|
||||
});
|
||||
builder
|
||||
}));
|
||||
}
|
||||
@@ -735,7 +799,7 @@ mod tests {
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
sentry_config: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
@@ -781,7 +845,7 @@ mod tests {
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
sentry_config: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
@@ -828,7 +892,7 @@ mod tests {
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
#[cfg(feature = "sentry")]
|
||||
sentry_dsn: None,
|
||||
sentry_config: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
@@ -0,0 +1,914 @@
|
||||
// Copyright 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Mutex,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use tracing_appender::rolling::Rotation;
|
||||
|
||||
/// Custom rolling file appender that supports time-based rotation and
|
||||
/// size-based cleanup.
|
||||
///
|
||||
/// This writer automatically manages log files with the following behavior:
|
||||
///
|
||||
/// # File Naming
|
||||
///
|
||||
/// Log files are named using the pattern: `{prefix}.{timestamp}.{suffix}`
|
||||
/// where the timestamp format depends on the rotation period:
|
||||
/// - `MINUTELY`: `YYYY-MM-DD-HH-MM`
|
||||
/// - `HOURLY`: `YYYY-MM-DD-HH`
|
||||
/// - `DAILY`: `YYYY-MM-DD`
|
||||
/// - `WEEKLY` or `NEVER`: `YYYY-Www` (ISO week number, e.g., `2024-W03`)
|
||||
///
|
||||
/// # Automatic Rotation
|
||||
///
|
||||
/// Files are rotated (a new file is created) when the configured time period
|
||||
/// changes. For example, with hourly rotation, a new file is created when the
|
||||
/// hour changes. Rotation is checked:
|
||||
/// - During writer initialization (creates/opens file for current period)
|
||||
/// - Before each write operation (only rotates if time period has changed)
|
||||
///
|
||||
/// If a log file already exists for the current time period, it will be
|
||||
/// reopened and appended to rather than creating a new file.
|
||||
///
|
||||
/// # Automatic Cleanup
|
||||
///
|
||||
/// The writer performs cleanup operations during initialization and rotation:
|
||||
/// - **Size limit enforcement**: When total size of all log files exceeds
|
||||
/// `max_total_size_bytes`, the oldest files are removed until under the limit
|
||||
/// - **Age-based cleanup**: Files older than `max_age_seconds` (based on
|
||||
/// filesystem modification time) are automatically removed
|
||||
/// - **File filtering**: Only files matching both the configured prefix and
|
||||
/// suffix are managed; other files in the directory are left untouched
|
||||
///
|
||||
/// # Side Effects on Creation
|
||||
///
|
||||
/// When `new()` is called, the following side effects occur:
|
||||
/// 1. Creates the log directory if it doesn't exist (including parent
|
||||
/// directories)
|
||||
/// 2. Creates or opens a log file for the current time period (appends if
|
||||
/// exists)
|
||||
/// 3. Performs cleanup of old files based on age (by filesystem mtime)
|
||||
/// 4. Enforces the total size limit by removing oldest files if needed
|
||||
///
|
||||
/// # Thread Safety
|
||||
///
|
||||
/// This writer is safe to use from multiple threads. Internal state is
|
||||
/// protected by a mutex, ensuring that file operations and rotations are
|
||||
/// properly synchronized.
|
||||
pub(super) struct SizeAndDateRollingWriter {
|
||||
config: WriterConfig,
|
||||
state: Mutex<Option<WriterState>>,
|
||||
}
|
||||
|
||||
/// Immutable configuration for the writer - shared without locks.
|
||||
///
|
||||
/// This struct contains all configuration parameters that remain constant
|
||||
/// throughout the writer's lifetime. Since these values never change, they
|
||||
/// can be safely shared across threads without synchronization.
|
||||
struct WriterConfig {
|
||||
/// Directory where log files are created
|
||||
base_path: PathBuf,
|
||||
/// Prefix for log file names (e.g., "app" results in "app.2024-01-15.log")
|
||||
file_prefix: String,
|
||||
/// Suffix for log file names (typically ".log")
|
||||
file_suffix: String,
|
||||
/// Time period for automatic rotation (MINUTELY, HOURLY, DAILY, WEEKLY, or
|
||||
/// NEVER which is treated as WEEKLY)
|
||||
rotation: Rotation,
|
||||
/// Maximum total size in bytes of all log files before cleanup
|
||||
max_total_size_bytes: u64,
|
||||
/// Maximum age in seconds before a log file is removed during cleanup
|
||||
max_age_seconds: u64,
|
||||
}
|
||||
|
||||
/// Mutable state that requires synchronization.
|
||||
///
|
||||
/// This struct contains the current log file handle and its path. These values
|
||||
/// change when rotation occurs, so access must be protected by a mutex to
|
||||
/// ensure thread safety.
|
||||
///
|
||||
/// The state is wrapped in `Option` because it needs to be temporarily taken
|
||||
/// during rotation operations to allow mutation while holding the lock.
|
||||
struct WriterState {
|
||||
/// The currently open log file handle for writing
|
||||
current_file: File,
|
||||
/// Path to the current log file (used to identify it during cleanup)
|
||||
current_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SizeAndDateRollingWriter {
|
||||
/// Creates a new rolling writer with the specified configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Directory where log files will be created. Will be created if
|
||||
/// it doesn't exist.
|
||||
/// * `file_prefix` - Prefix for log file names (e.g., "app")
|
||||
/// * `file_suffix` - Suffix for log file names (e.g., ".log")
|
||||
/// * `rotation` - Time period for rotation (MINUTELY, HOURLY, DAILY,
|
||||
/// WEEKLY, or NEVER which is treated as WEEKLY)
|
||||
/// * `max_total_size_bytes` - Maximum total size of all log files. When
|
||||
/// exceeded, oldest files are removed.
|
||||
/// * `max_age_seconds` - Maximum age of log files in seconds. Files older
|
||||
/// than this (by filesystem mtime) are removed during cleanup.
|
||||
///
|
||||
/// # Side Effects
|
||||
///
|
||||
/// This method performs several file system operations in order:
|
||||
/// 1. Creates the directory at `path` if it doesn't exist
|
||||
/// 2. Creates or reopens a log file for the current time period (appends if
|
||||
/// exists)
|
||||
/// 3. Scans the directory for existing log files matching the prefix/suffix
|
||||
/// 4. Removes files older than `max_age_seconds` (by filesystem mtime)
|
||||
/// 5. Removes oldest files if total size exceeds `max_total_size_bytes`
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The directory cannot be created
|
||||
/// - The directory cannot be read
|
||||
/// - The log file cannot be created or opened
|
||||
/// - File metadata cannot be read during cleanup
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let writer = SizeAndDateRollingWriter::new(
|
||||
/// "/var/log/myapp",
|
||||
/// "app".to_owned(),
|
||||
/// ".log".to_owned(),
|
||||
/// Rotation::HOURLY,
|
||||
/// 100 * 1024 * 1024, // 100 MB
|
||||
/// 7 * 24 * 60 * 60, // 7 days
|
||||
/// )?;
|
||||
/// ```
|
||||
pub(super) fn new(
|
||||
path: impl AsRef<Path>,
|
||||
file_prefix: String,
|
||||
file_suffix: String,
|
||||
rotation: Rotation,
|
||||
max_total_size_bytes: u64,
|
||||
max_age_seconds: u64,
|
||||
) -> io::Result<Self> {
|
||||
let base_path = path.as_ref().to_path_buf();
|
||||
fs::create_dir_all(&base_path)?;
|
||||
|
||||
let config = WriterConfig {
|
||||
base_path,
|
||||
file_prefix,
|
||||
file_suffix,
|
||||
rotation,
|
||||
max_total_size_bytes,
|
||||
max_age_seconds,
|
||||
};
|
||||
|
||||
// Create initial state with first rotation
|
||||
let mut state = None;
|
||||
Self::rotate_internal(&config, &mut state, false)?;
|
||||
|
||||
Ok(Self { config, state: Mutex::new(state) })
|
||||
}
|
||||
|
||||
/// Extract the timestamp from the current filename.
|
||||
fn extract_timestamp_from_path(config: &WriterConfig, current_path: &Path) -> Option<String> {
|
||||
let filename = current_path.file_name()?.to_str()?;
|
||||
|
||||
// Strip prefix and suffix to get the timestamp
|
||||
// Format: "prefix.timestamp.suffix"
|
||||
let without_prefix = filename.strip_prefix(&format!("{}.", config.file_prefix))?;
|
||||
let timestamp = without_prefix.strip_suffix(&config.file_suffix)?;
|
||||
|
||||
Some(timestamp.to_owned())
|
||||
}
|
||||
|
||||
/// Check if rotation is needed based on time period change.
|
||||
fn should_rotate_by_time(config: &WriterConfig, current_path: &Path) -> bool {
|
||||
let current_time = Self::format_rotation_timestamp(config);
|
||||
let last_rotation_time = Self::extract_timestamp_from_path(config, current_path);
|
||||
|
||||
// If we can't extract the timestamp, assume rotation is needed
|
||||
match last_rotation_time {
|
||||
Some(last_time) => current_time != last_time,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the current time as a timestamp string for the rotation period.
|
||||
///
|
||||
/// Returns a timestamp string based on the configured rotation period
|
||||
/// (e.g., "2024-01-15-14" for hourly rotation).
|
||||
fn format_rotation_timestamp(config: &WriterConfig) -> String {
|
||||
let now = chrono::Local::now();
|
||||
match config.rotation {
|
||||
Rotation::MINUTELY => now.format("%Y-%m-%d-%H-%M").to_string(),
|
||||
Rotation::HOURLY => now.format("%Y-%m-%d-%H").to_string(),
|
||||
Rotation::DAILY => now.format("%Y-%m-%d").to_string(),
|
||||
Rotation::WEEKLY | Rotation::NEVER => now.format("%Y-W%W").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate the log file, creating a new file with a timestamp-based name.
|
||||
///
|
||||
/// If `check_conditions` is true, rotation only happens if time or size
|
||||
/// thresholds are met. Otherwise, rotation is forced.
|
||||
///
|
||||
/// This method also handles initial state creation when called with None
|
||||
/// state.
|
||||
fn rotate_internal(
|
||||
config: &WriterConfig,
|
||||
state: &mut Option<WriterState>,
|
||||
check_conditions: bool,
|
||||
) -> io::Result<()> {
|
||||
// Check if rotation is needed (skip for uninitialized state)
|
||||
if check_conditions
|
||||
&& let Some(state) = state.as_ref()
|
||||
&& !Self::should_rotate_by_time(config, &state.current_path)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let time_str = Self::format_rotation_timestamp(config);
|
||||
|
||||
// Generate filename with timestamp
|
||||
let filename = format!("{}.{}{}", config.file_prefix, time_str, config.file_suffix);
|
||||
let new_path = config.base_path.join(filename);
|
||||
|
||||
// Open or create file in append mode
|
||||
let new_file = OpenOptions::new().create(true).append(true).open(&new_path)?;
|
||||
|
||||
let new_state = WriterState { current_file: new_file, current_path: new_path };
|
||||
|
||||
// Clean up logs older than configured max age
|
||||
Self::trim_old_logs_internal(config, &new_state)?;
|
||||
|
||||
// Enforce total size limit by removing oldest files if needed
|
||||
Self::enforce_total_size_limit_internal(config, &new_state)?;
|
||||
|
||||
*state = Some(new_state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all log files matching our prefix and suffix, sorted by modification
|
||||
/// time.
|
||||
///
|
||||
/// Returns a vector of (path, modification_time) tuples, oldest first.
|
||||
fn get_matching_log_files(config: &WriterConfig) -> io::Result<Vec<(PathBuf, SystemTime)>> {
|
||||
let mut files: Vec<_> = fs::read_dir(&config.base_path)?
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let filename = path.file_name()?.to_str()?;
|
||||
// Only process files matching our prefix and suffix
|
||||
if filename.starts_with(&config.file_prefix)
|
||||
&& filename.ends_with(&config.file_suffix)
|
||||
{
|
||||
let metadata = fs::metadata(&path).ok()?;
|
||||
let modified = metadata.modified().ok()?;
|
||||
return Some((path, modified));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
files.sort_by_key(|(_, modified)| *modified);
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Remove all log files older than the configured max age.
|
||||
///
|
||||
/// Only files matching the configured prefix and suffix are removed.
|
||||
/// Other files in the directory are left alone.
|
||||
fn trim_old_logs_internal(config: &WriterConfig, state: &WriterState) -> io::Result<()> {
|
||||
let now = SystemTime::now();
|
||||
let files = Self::get_matching_log_files(config)?;
|
||||
|
||||
for (path, modified) in files {
|
||||
// Skip the current file
|
||||
if path == state.current_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if file is older than max age
|
||||
if let Ok(duration) = now.duration_since(modified)
|
||||
&& duration.as_secs() > config.max_age_seconds
|
||||
{
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enforce total size limit across all log files.
|
||||
///
|
||||
/// If the total size of all matching log files exceeds
|
||||
/// max_total_size_bytes, remove the oldest files until the total is
|
||||
/// below the limit.
|
||||
fn enforce_total_size_limit_internal(
|
||||
config: &WriterConfig,
|
||||
state: &WriterState,
|
||||
) -> io::Result<()> {
|
||||
let max_total = config.max_total_size_bytes;
|
||||
|
||||
let files = Self::get_matching_log_files(config)?;
|
||||
|
||||
// Calculate total size of all log files
|
||||
let mut total_size: u64 = 0;
|
||||
for (path, _) in &files {
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
total_size += metadata.len();
|
||||
}
|
||||
}
|
||||
|
||||
// If under limit, nothing to do
|
||||
if total_size <= max_total {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Remove oldest files until we're under the limit
|
||||
// Files are already sorted by modification time (oldest first)
|
||||
for (path, _) in files {
|
||||
// Don't remove the current file
|
||||
if path == state.current_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(metadata) = fs::metadata(&path) {
|
||||
let file_size = metadata.len();
|
||||
let _ = fs::remove_file(&path);
|
||||
total_size = total_size.saturating_sub(file_size);
|
||||
|
||||
// Check if we're now under the limit
|
||||
if total_size <= max_total {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SizeAndDateRollingWriter {
|
||||
type Writer = SizeAndDateRollingWriterHandle<'a>;
|
||||
|
||||
fn make_writer(&'a self) -> Self::Writer {
|
||||
SizeAndDateRollingWriterHandle {
|
||||
config: &self.config,
|
||||
state: &self.state,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SizeAndDateRollingWriterHandle<'a> {
|
||||
config: &'a WriterConfig,
|
||||
state: &'a Mutex<Option<WriterState>>,
|
||||
_phantom: std::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a> Write for SizeAndDateRollingWriterHandle<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
// Check if rotation is needed
|
||||
SizeAndDateRollingWriter::rotate_internal(self.config, &mut state, true)?;
|
||||
|
||||
// Write to file (state must be initialized after rotation)
|
||||
state.as_mut().unwrap().current_file.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let Some(s) = state.as_mut() { s.current_file.flush() } else { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SizeAndDateRollingWriter {
|
||||
/// Manually trigger log rotation if conditions are met.
|
||||
///
|
||||
/// This will rotate the current log file if the time period has changed.
|
||||
fn roll(&self) -> io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
Self::rotate_internal(&self.config, &mut state, true)
|
||||
}
|
||||
|
||||
/// Manually trigger cleanup of old log files.
|
||||
///
|
||||
/// This removes all log files older than the configured max age, keeping
|
||||
/// only files that match the configured prefix and suffix.
|
||||
fn trim(&self) -> io::Result<()> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if let Some(ref state) = *state {
|
||||
Self::trim_old_logs_internal(&self.config, state)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use tempfile::tempdir;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rotation_file_naming() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"app".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check file naming pattern
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let filename = path.file_name()?.to_str()?.to_owned();
|
||||
if filename.starts_with("app") && filename.ends_with(".log") {
|
||||
Some(filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Should have one file with proper naming
|
||||
assert_eq!(log_files.len(), 1, "Expected exactly one log file");
|
||||
|
||||
// Check that files follow the expected naming pattern
|
||||
for filename in &log_files {
|
||||
assert!(
|
||||
filename.starts_with("app."),
|
||||
"Filename should start with prefix: {}",
|
||||
filename
|
||||
);
|
||||
assert!(filename.ends_with(".log"), "Filename should end with suffix: {}", filename);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix_suffix_filtering() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create some unrelated files
|
||||
std::fs::write(log_path.join("other.txt"), "unrelated").unwrap();
|
||||
std::fs::write(log_path.join("app.txt"), "wrong suffix").unwrap();
|
||||
std::fs::write(log_path.join("test.log"), "wrong prefix").unwrap();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"app".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"test message\n").unwrap();
|
||||
handle.flush().unwrap();
|
||||
|
||||
// Trigger manual trim
|
||||
writer.trim().unwrap();
|
||||
|
||||
// Check that unrelated files still exist
|
||||
assert!(log_path.join("other.txt").exists(), "Unrelated file should not be removed");
|
||||
assert!(log_path.join("app.txt").exists(), "File with wrong suffix should not be removed");
|
||||
assert!(log_path.join("test.log").exists(), "File with wrong prefix should not be removed");
|
||||
|
||||
// App log file should still exist
|
||||
let app_logs: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let filename = entry.file_name().to_str()?.to_owned();
|
||||
if filename.starts_with("app.") && filename.ends_with(".log") {
|
||||
Some(filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(app_logs.len(), 1, "Expected one app log file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_roll_and_trim() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"manual".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Manual roll should work
|
||||
assert!(writer.roll().is_ok(), "Manual roll should succeed");
|
||||
|
||||
// Manual trim should work
|
||||
assert!(writer.trim().is_ok(), "Manual trim should succeed");
|
||||
|
||||
// Should still have log file
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("manual") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert!(!log_files.is_empty(), "Should have at least one log file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_size_limit_removes_oldest_files() {
|
||||
// This test verifies that when the total size of all log files exceeds
|
||||
// max_total_size_bytes, the oldest files are removed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Manually create several log files with different timestamps (alphabetically
|
||||
// sorted = oldest first) Total of 240 bytes
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-00.log"), "x".repeat(80)).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-01.log"), "y".repeat(80)).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-02.log"), "z".repeat(80)).unwrap();
|
||||
|
||||
let count_files = || {
|
||||
std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.count()
|
||||
};
|
||||
|
||||
assert_eq!(count_files(), 3, "Should have 3 log files");
|
||||
|
||||
// Now create a new writer with 200 byte total limit
|
||||
// Current total is 240 bytes. The writer will:
|
||||
// 1. Create a new file with current timestamp
|
||||
// 2. See total exceeds 200 bytes
|
||||
// 3. Remove oldest file(s) until under limit
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"total".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::MINUTELY,
|
||||
200, // 200 byte total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// After writer creation, we should still have 3 files:
|
||||
// - Oldest file (10-00) was removed
|
||||
// - Two middle files (10-01, 10-02) remain
|
||||
// - New current file was created
|
||||
let remaining_files = count_files();
|
||||
assert_eq!(
|
||||
remaining_files, 3,
|
||||
"Should have 3 files: 2 old files + 1 new current file, but have {}",
|
||||
remaining_files
|
||||
);
|
||||
|
||||
// Calculate total size of remaining files
|
||||
let total_size: u64 = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
|
||||
Some(std::fs::metadata(&path).ok()?.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
// Total should be 160 bytes (80 + 80 + 0 for new file)
|
||||
assert!(total_size <= 200, "Total size should be under 200 bytes, but is {}", total_size);
|
||||
|
||||
// Verify the oldest file was removed by checking filenames
|
||||
let filenames: Vec<String> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() { path.file_name()?.to_str().map(|s| s.to_owned()) } else { None }
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!filenames.contains(&"total.2024-01-01-10-00.log".to_owned()),
|
||||
"Oldest file should have been removed"
|
||||
);
|
||||
assert!(
|
||||
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-01")),
|
||||
"Second file should still exist"
|
||||
);
|
||||
assert!(
|
||||
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-02")),
|
||||
"Third file should still exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_based_rotation_logic() {
|
||||
// Test that the rotation logic correctly identifies when rotation is needed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"time".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write some data
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"initial log entry\n").unwrap();
|
||||
handle.flush().unwrap();
|
||||
|
||||
// Verify initial state - should have created one file
|
||||
let initial_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("time") {
|
||||
Some(path.file_name()?.to_str()?.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(initial_files.len(), 1, "Should have one log file initially");
|
||||
|
||||
// Verify the file contains our data
|
||||
let file_content = std::fs::read_to_string(log_path.join(&initial_files[0])).unwrap();
|
||||
assert!(file_content.contains("initial log entry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_old_files_cleanup() {
|
||||
// Test that files older than one week are removed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create files with timestamps more than a week old
|
||||
// We can't easily manipulate file mtimes without external crates,
|
||||
// but we can verify the cleanup logic doesn't fail
|
||||
std::fs::write(log_path.join("old.2020-01-01-10-00.log"), "old data").unwrap();
|
||||
|
||||
// Create a writer which will trigger cleanup
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"old".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The old file should still exist because we can't manipulate mtime easily
|
||||
// But we verify that cleanup doesn't crash
|
||||
writer.trim().unwrap();
|
||||
|
||||
// Verify we can still write
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"new data").unwrap();
|
||||
handle.flush().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_operations() {
|
||||
// Test that writing works correctly across multiple writes
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"write".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write multiple times
|
||||
for i in 0..5 {
|
||||
let mut handle = writer.make_writer();
|
||||
let message = format!("Log entry {}\n", i);
|
||||
handle.write_all(message.as_bytes()).unwrap();
|
||||
handle.flush().unwrap();
|
||||
}
|
||||
|
||||
// Verify all writes went to the same file (same hour)
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("write") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(log_files.len(), 1, "Should have one log file for same time period");
|
||||
|
||||
// Verify content
|
||||
let content = std::fs::read_to_string(&log_files[0]).unwrap();
|
||||
for i in 0..5 {
|
||||
assert!(content.contains(&format!("Log entry {}", i)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reopening_existing_file() {
|
||||
// Test that reopening appends to existing file within same time period
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create first writer and write
|
||||
let writer1 = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"reopen".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle1 = writer1.make_writer();
|
||||
handle1.write_all(b"first write\n").unwrap();
|
||||
handle1.flush().unwrap();
|
||||
drop(writer1);
|
||||
|
||||
// Create second writer (simulating restart within same day)
|
||||
let writer2 = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"reopen".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle2 = writer2.make_writer();
|
||||
handle2.write_all(b"second write\n").unwrap();
|
||||
handle2.flush().unwrap();
|
||||
|
||||
// Should still have only one file
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("reopen") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(log_files.len(), 1, "Should have one log file");
|
||||
|
||||
// Verify both writes are in the file
|
||||
let content = std::fs::read_to_string(&log_files[0]).unwrap();
|
||||
assert!(content.contains("first write"));
|
||||
assert!(content.contains("second write"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_size_limit_with_multiple_old_files() {
|
||||
// Test that multiple old files are removed until under limit
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create 5 files, each 50 bytes (total 250 bytes)
|
||||
for i in 0..5 {
|
||||
let filename = format!("multi.2024-01-01-10-0{}.log", i);
|
||||
std::fs::write(log_path.join(filename), "x".repeat(50)).unwrap();
|
||||
}
|
||||
|
||||
let count_files = || {
|
||||
std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter(|entry| {
|
||||
if let Ok(entry) = entry
|
||||
&& let Some(name) = entry.file_name().to_str()
|
||||
{
|
||||
return name.starts_with("multi");
|
||||
}
|
||||
false
|
||||
})
|
||||
.count()
|
||||
};
|
||||
|
||||
assert_eq!(count_files(), 5, "Should start with 5 files");
|
||||
|
||||
// Create writer with 100 byte limit (should keep only 2 old files + new
|
||||
// current)
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"multi".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::MINUTELY,
|
||||
100, // 100 byte total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Should have removed oldest files
|
||||
let remaining = count_files();
|
||||
assert!(remaining <= 3, "Should have removed multiple old files to stay under limit");
|
||||
|
||||
// Verify total size is under limit
|
||||
let total_size: u64 = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("multi") {
|
||||
Some(std::fs::metadata(&path).ok()?.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
assert!(total_size <= 100, "Total size should be under 100 bytes, but is {}", total_size);
|
||||
}
|
||||
}
|
||||
+19
-6
@@ -1,12 +1,25 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::borrow::ToOwned;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use tracing::{callsite::DefaultCallsite, debug, error, field::FieldSet, Callsite};
|
||||
use tracing::{Callsite, callsite::DefaultCallsite, debug, error, field::FieldSet};
|
||||
use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
|
||||
/// Log an event.
|
||||
@@ -54,7 +67,7 @@ fn get_or_init_metadata(
|
||||
meta_kind: MetadataKind,
|
||||
) -> &'static DefaultCallsite {
|
||||
mutex.lock().unwrap().entry(id).or_insert_with_key(|id| {
|
||||
let callsite = Box::leak(Box::new(LateInitCallsite(OnceCell::new())));
|
||||
let callsite = Box::leak(Box::new(LateInitCallsite(OnceLock::new())));
|
||||
let metadata = Box::leak(Box::new(tracing::Metadata::new(
|
||||
Box::leak(
|
||||
id.name
|
||||
@@ -73,7 +86,7 @@ fn get_or_init_metadata(
|
||||
FieldSet::new(field_names, identify_callsite!(callsite)),
|
||||
meta_kind,
|
||||
)));
|
||||
callsite.0.try_insert(DefaultCallsite::new(metadata)).expect("callsite was not set before")
|
||||
callsite.0.get_or_init(|| DefaultCallsite::new(metadata))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -254,7 +267,7 @@ struct MetadataId {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
struct LateInitCallsite(OnceCell<DefaultCallsite>);
|
||||
struct LateInitCallsite(OnceLock<DefaultCallsite>);
|
||||
|
||||
impl Callsite for LateInitCallsite {
|
||||
fn set_interest(&self, interest: tracing_core::Interest) {
|
||||
@@ -1,14 +1,28 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
OAuth,
|
||||
qrcode::{
|
||||
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
|
||||
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
|
||||
},
|
||||
OAuth,
|
||||
};
|
||||
use matrix_sdk_base::crypto::types::qr_login;
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm, stream::StreamExt};
|
||||
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
@@ -259,6 +273,26 @@ impl QrCodeData {
|
||||
qr_login::QrCodeIntentData::Msc4388 { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The base URL of the homeserver contained within the scanned QR code
|
||||
/// data.
|
||||
///
|
||||
/// Note: This value is only present when scanning a QR code conforming to
|
||||
/// MSC4388.
|
||||
pub fn base_url(&self) -> Option<String> {
|
||||
match self.inner.intent_data() {
|
||||
qrcode::QrCodeIntentData::Msc4108 { .. } => None,
|
||||
qrcode::QrCodeIntentData::Msc4388 { base_url, .. } => Some(base_url.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [`QrCodeIntent`] of this [`QrCodeData`] object.
|
||||
///
|
||||
/// This tells us if the creator of the QR code wants to log in or if they
|
||||
/// want to log another device in.
|
||||
pub fn intent(&self) -> QrCodeIntent {
|
||||
self.inner.intent()
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for the decoding of the [`QrCodeData`].
|
||||
@@ -298,6 +332,8 @@ pub enum HumanQrLoginError {
|
||||
CheckCodeCannotBeSent,
|
||||
#[error("The rendezvous session was not found and might have expired")]
|
||||
NotFound,
|
||||
#[error("The QR code specifies an unsupported protocol version")]
|
||||
UnsupportedQrCodeType,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
@@ -328,8 +364,10 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_)
|
||||
| SecureChannelError::UnsupportedQrCodeType => HumanQrLoginError::Unknown,
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::UnsupportedQrCodeType => {
|
||||
HumanQrLoginError::UnsupportedQrCodeType
|
||||
}
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
@@ -384,23 +422,44 @@ pub enum HumanQrGrantLoginError {
|
||||
#[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),
|
||||
|
||||
/// The requested device was not returned by the homeserver.
|
||||
#[error("The requested device was not returned by the homeserver.")]
|
||||
DeviceNotFound,
|
||||
|
||||
/// The other device is already signed in and so does not need to sign in.
|
||||
#[error("The other device is already signed and so does not need to sign in.")]
|
||||
OtherDeviceAlreadySignedIn,
|
||||
|
||||
/// The sign in was cancelled.
|
||||
#[error("The sign in was cancelled.")]
|
||||
Cancelled,
|
||||
|
||||
/// The sign in was not completed in the required time.
|
||||
#[error("The sign in was not completed in the required time.")]
|
||||
Expired,
|
||||
|
||||
/// A secure connection could not have been established between the two
|
||||
/// devices.
|
||||
#[error("A secure connection could not have been established between the two devices.")]
|
||||
ConnectionInsecure,
|
||||
|
||||
/// The QR code specifies an unsupported protocol version.
|
||||
#[error("The QR code specifies an unsupported protocol version")]
|
||||
UnsupportedQrCodeType,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
|
||||
fn from(value: qrcode::QRCodeGrantLoginError) -> Self {
|
||||
use qrcode::QRCodeGrantLoginError;
|
||||
use qrcode::{QRCodeGrantLoginError, SecureChannelError};
|
||||
|
||||
match value {
|
||||
QRCodeGrantLoginError::DeviceIDAlreadyInUse => Self::DeviceIDAlreadyInUse,
|
||||
QRCodeGrantLoginError::DeviceNotFound => Self::DeviceNotFound,
|
||||
QRCodeGrantLoginError::InvalidCheckCode => Self::InvalidCheckCode,
|
||||
QRCodeGrantLoginError::UnableToCreateDevice => Self::UnableToCreateDevice,
|
||||
QRCodeGrantLoginError::UnsupportedProtocol(protocol) => {
|
||||
Self::UnsupportedProtocol(protocol.to_string())
|
||||
}
|
||||
@@ -408,7 +467,28 @@ impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
|
||||
Self::MissingSecretsBackup(error.map_or("other".to_owned(), |e| e.to_string()))
|
||||
}
|
||||
QRCodeGrantLoginError::NotFound => Self::NotFound,
|
||||
QRCodeGrantLoginError::SecureChannel(e) => match e {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => Self::Unknown(e.to_string()),
|
||||
SecureChannelError::UnsupportedQrCodeType => Self::UnsupportedQrCodeType,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
| SecureChannelError::CannotReceiveCheckCode => Self::ConnectionInsecure,
|
||||
SecureChannelError::InvalidIntent => Self::OtherDeviceAlreadySignedIn,
|
||||
},
|
||||
QRCodeGrantLoginError::UnexpectedMessage { .. } => Self::Unknown(value.to_string()),
|
||||
QRCodeGrantLoginError::Unknown(string) => Self::Unknown(string),
|
||||
QRCodeGrantLoginError::LoginFailure { reason, .. } => match reason {
|
||||
LoginFailureReason::UnsupportedProtocol => Self::UnsupportedProtocol(
|
||||
"Other device does not support any of our protocols".to_owned(),
|
||||
),
|
||||
LoginFailureReason::AuthorizationExpired => Self::Expired,
|
||||
LoginFailureReason::UserCancelled => Self::Cancelled,
|
||||
_ => Self::Unknown(reason.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,75 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{StreamExt, pin_mut};
|
||||
use matrix_sdk::{
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges,
|
||||
ListThreadsOptions as SdkListThreadsOptions, Room as SdkRoom, RoomMemberRole,
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
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,
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
Room as SdkRoom, RoomMemberRole, edit::EditedContent, power_levels::RoomPowerLevelChanges,
|
||||
},
|
||||
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
timeline::{default_event_filter, RoomExt, TimelineBuilder},
|
||||
timeline::{RoomExt, TimelineBuilder, default_event_filter},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads,
|
||||
assign,
|
||||
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId, assign,
|
||||
events::{
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
receipt::ReceiptThread,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
MediaSource as RumaMediaSource, avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
MediaSource as RumaMediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId,
|
||||
};
|
||||
use tracing::{error, warn};
|
||||
use tracing::error;
|
||||
|
||||
use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
|
||||
error::{
|
||||
ClientError, LiveLocationError, MediaInfoError, NotYetImplemented, QueueWedgeError,
|
||||
RoomError,
|
||||
},
|
||||
event::TimelineEvent,
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
live_location_share::LiveLocationShares,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{
|
||||
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
ruma::{AudioInfo, FileInfo, ImageInfo, MediaSource, ThumbnailInfo, VideoInfo},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
threads::{ThreadListService, ThreadSubscription},
|
||||
},
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
utils::{AsyncRuntimeDropped, u64_to_uint},
|
||||
};
|
||||
|
||||
mod power_levels;
|
||||
@@ -427,6 +438,37 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a raw state event to the room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_type` - The type of the state event to send (e.g.
|
||||
/// `"m.room.name"` or a custom type).
|
||||
///
|
||||
/// * `state_key` - A unique key which defines the overwriting semantics for
|
||||
/// this piece of room state. This is often an empty string.
|
||||
///
|
||||
/// * `content` - The content of the state event encoded as a JSON string.
|
||||
///
|
||||
/// Returns the event ID of the newly created state event.
|
||||
pub async fn send_state_event_raw(
|
||||
&self,
|
||||
event_type: String,
|
||||
state_key: String,
|
||||
content: String,
|
||||
) -> Result<String, ClientError> {
|
||||
let content_json: serde_json::Value =
|
||||
serde_json::from_str(&content).map_err(|e| ClientError::Generic {
|
||||
msg: format!("Failed to parse JSON: {e}"),
|
||||
details: Some(format!("{e:?}")),
|
||||
})?;
|
||||
|
||||
let response =
|
||||
self.inner.send_state_event_raw(&event_type, &state_key, content_json).await?;
|
||||
|
||||
Ok(response.event_id.to_string())
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -470,18 +512,9 @@ impl Room {
|
||||
pub async fn report_content(
|
||||
&self,
|
||||
event_id: String,
|
||||
score: Option<i32>,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.report_content(
|
||||
EventId::parse(event_id)?,
|
||||
score.map(TryFrom::try_from).transpose().map_err(
|
||||
|error: TryFromReportedContentScoreError| ClientError::from_err(error),
|
||||
)?,
|
||||
reason,
|
||||
)
|
||||
.await?;
|
||||
self.inner.report_content(EventId::parse(event_id)?, reason).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1054,17 +1087,14 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Stop the current users live location share in the room.
|
||||
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
|
||||
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
|
||||
pub async fn stop_live_location_share(&self) -> Result<(), LiveLocationError> {
|
||||
self.inner.stop_live_location_share().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the current users live location beacon in the room.
|
||||
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_location_beacon(geo_uri)
|
||||
.await
|
||||
.expect("Unable to send live location beacon");
|
||||
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), LiveLocationError> {
|
||||
self.inner.send_location_beacon(geo_uri).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1106,46 +1136,16 @@ impl Room {
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Subscribes to live location shares in this room, using a `listener` to
|
||||
/// be notified of the changes.
|
||||
/// Returns the active live location shares for this room.
|
||||
///
|
||||
/// The current live location shares will be emitted immediately when
|
||||
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
|
||||
pub fn subscribe_to_live_location_shares(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn LiveLocationShareListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let subscription = room.observe_live_location_shares();
|
||||
let stream = subscription.subscribe();
|
||||
let mut pinned_stream = pin!(stream);
|
||||
|
||||
while let Some(event) = pinned_stream.next().await {
|
||||
let last_location = LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: event.last_location.location.uri.clone().to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset: None,
|
||||
};
|
||||
|
||||
let Some(beacon_info) = event.beacon_info else {
|
||||
warn!("Live location share is missing the associated beacon_info state, skipping event.");
|
||||
continue;
|
||||
};
|
||||
|
||||
listener.call(vec![LiveLocationShare {
|
||||
last_location: LastLocation {
|
||||
location: last_location,
|
||||
ts: event.last_location.ts.0.into(),
|
||||
},
|
||||
is_live: beacon_info.is_live(),
|
||||
user_id: event.user_id.to_string(),
|
||||
}])
|
||||
}
|
||||
})))
|
||||
/// The returned [`LiveLocationShares`] object tracks which users are
|
||||
/// currently sharing their live location. It keeps the underlying event
|
||||
/// handlers registered — and therefore the share list up-to-date — for as
|
||||
/// long as it is alive. Call [`LiveLocationShares::subscribe`] on it to
|
||||
/// receive an initial snapshot and a stream of incremental updates.
|
||||
pub async fn live_location_shares(&self) -> Arc<LiveLocationShares> {
|
||||
let inner = self.inner.live_location_shares().await;
|
||||
Arc::new(LiveLocationShares::new(inner))
|
||||
}
|
||||
|
||||
/// Forget this room.
|
||||
@@ -1180,12 +1180,11 @@ impl Room {
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
if server_names.is_empty()
|
||||
&& let Ok(invite_details) = self.inner.invite_details().await
|
||||
&& let Some(inviter) = invite_details.inviter
|
||||
{
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
@@ -1238,39 +1237,18 @@ impl Room {
|
||||
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
|
||||
}
|
||||
|
||||
/// Retrieve a list of all the threads for the current room.
|
||||
/// Creates a new [`ThreadListService`] for this room.
|
||||
///
|
||||
/// Since this client-server API is paginated, the return type may include a
|
||||
/// token used to resuming back-pagination into the list of results, in
|
||||
/// [`ThreadRoots::prev_batch_token`]. This token can be passed to the next
|
||||
/// call to this function, through the `from` field of
|
||||
/// [`ListThreadsOptions`].
|
||||
pub async fn list_threads(&self, opts: ListThreadsOptions) -> Result<ThreadRoots, ClientError> {
|
||||
let inner_opts = SdkListThreadsOptions {
|
||||
include_threads: match opts.include_threads {
|
||||
IncludeThreads::All => SdkIncludeThreads::All,
|
||||
IncludeThreads::Participated => SdkIncludeThreads::Participated,
|
||||
},
|
||||
from: opts.from,
|
||||
limit: opts.limit.and_then(ruma::UInt::new),
|
||||
};
|
||||
/// The returned service provides a reactive, paginated list of thread roots
|
||||
/// for the room. Use [`ThreadListService::paginate`] to load pages and
|
||||
/// [`ThreadListService::subscribe_to_items_updates`] /
|
||||
/// [`ThreadListService::subscribe_to_pagination_state_updates`] to observe
|
||||
/// changes.
|
||||
pub fn thread_list_service(&self) -> Arc<ThreadListService> {
|
||||
// `no reactor running` panics
|
||||
let _guard = get_runtime_handle().enter();
|
||||
|
||||
let roots = self.inner.list_threads(inner_opts).await?;
|
||||
|
||||
Ok(ThreadRoots {
|
||||
chunk: roots
|
||||
.chunk
|
||||
.into_iter()
|
||||
.filter_map(|timeline_event| {
|
||||
timeline_event
|
||||
.raw()
|
||||
.deserialize()
|
||||
.ok()
|
||||
.map(|any_timeline_event| TimelineEvent(Box::new(any_timeline_event)))
|
||||
})
|
||||
.collect(),
|
||||
prev_batch_token: roots.prev_batch_token,
|
||||
})
|
||||
Arc::new(ThreadListService::new(&self.inner))
|
||||
}
|
||||
|
||||
/// Either loads the event associated with the `event_id` from the event
|
||||
@@ -1290,74 +1268,6 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Options for [Room::list_threads].
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct ListThreadsOptions {
|
||||
/// An extra filter to select which threads should be returned.
|
||||
pub include_threads: IncludeThreads,
|
||||
|
||||
/// The token to start returning events from.
|
||||
///
|
||||
/// This token can be obtained from a [`ThreadRoots::prev_batch_token`]
|
||||
/// returned by a previous call to [`Room::list_threads()`].
|
||||
///
|
||||
/// If `from` isn't provided the homeserver shall return a list of thread
|
||||
/// roots from end of the timeline history.
|
||||
pub from: Option<String>,
|
||||
|
||||
/// The maximum number of events to return.
|
||||
///
|
||||
/// Default: 10.
|
||||
pub limit: Option<u64>,
|
||||
}
|
||||
|
||||
/// Which threads to include in the response.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum IncludeThreads {
|
||||
/// `all`
|
||||
///
|
||||
/// Include all thread roots found in the room.
|
||||
///
|
||||
/// This is the default.
|
||||
All,
|
||||
|
||||
/// `participated`
|
||||
///
|
||||
/// Only include thread roots for threads where
|
||||
/// [`current_user_participated`] is `true`.
|
||||
///
|
||||
/// [`current_user_participated`]: https://spec.matrix.org/latest/client-server-api/#server-side-aggregation-of-mthread-relationships
|
||||
Participated,
|
||||
}
|
||||
|
||||
/// The result of a [`Room::list_threads`] query.
|
||||
///
|
||||
/// This is a wrapper around the Ruma equivalent, with events decrypted if needs
|
||||
/// be.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct ThreadRoots {
|
||||
/// The events that are thread roots in the current batch.
|
||||
pub chunk: Vec<TimelineEvent>,
|
||||
|
||||
/// Token to paginate backwards in a subsequent query to
|
||||
/// [`Room::list_threads`].
|
||||
pub prev_batch_token: Option<String>,
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
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 {
|
||||
@@ -1724,23 +1634,30 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
|
||||
fn draft_thumbnail(
|
||||
thumbnail_info: Option<ThumbnailInfo>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
) -> Result<Option<DraftThumbnail>, ClientError> {
|
||||
if let Some(info) = thumbnail_info
|
||||
&& let Some(source) = thumbnail_source
|
||||
{
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Ok(Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -1750,26 +1667,12 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
width: image_info.width,
|
||||
height: image_info.height,
|
||||
blurhash: image_info.blurhash,
|
||||
thumbnail,
|
||||
thumbnail: draft_thumbnail(image_info.thumbnail_info, thumbnail_source)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
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 {
|
||||
@@ -1780,7 +1683,7 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
height: video_info.height,
|
||||
duration: video_info.duration,
|
||||
blurhash: video_info.blurhash,
|
||||
thumbnail,
|
||||
thumbnail: draft_thumbnail(video_info.thumbnail_info, thumbnail_source)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use ruma::{
|
||||
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
|
||||
OwnedUserId, UserId,
|
||||
events::{TimelineEventType, room::power_levels::RoomPowerLevels as RumaPowerLevels},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{EncryptionState, RoomState};
|
||||
use matrix_sdk::{CallIntentConsensus, EncryptionState, RoomState};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
@@ -8,11 +22,35 @@ use crate::{
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{
|
||||
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
|
||||
Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom, power_levels::RoomPowerLevels,
|
||||
},
|
||||
room_member::RoomMember,
|
||||
ruma::RtcCallIntent,
|
||||
};
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RtcCallIntentConsensus {
|
||||
Full(RtcCallIntent),
|
||||
Partial { intent: RtcCallIntent, agreeing_count: u64, total_count: u64 },
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<CallIntentConsensus> for RtcCallIntentConsensus {
|
||||
fn from(value: CallIntentConsensus) -> Self {
|
||||
match value {
|
||||
CallIntentConsensus::Full(intent) => RtcCallIntentConsensus::Full(intent.into()),
|
||||
CallIntentConsensus::Partial { intent, agreeing_count, total_count } => {
|
||||
RtcCallIntentConsensus::Partial {
|
||||
intent: intent.into(),
|
||||
agreeing_count,
|
||||
total_count,
|
||||
}
|
||||
}
|
||||
CallIntentConsensus::None => RtcCallIntentConsensus::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
@@ -35,6 +73,7 @@ pub struct RoomInfo {
|
||||
/// If present, it means the room has been archived/upgraded.
|
||||
successor_room: Option<SuccessorRoom>,
|
||||
is_favourite: bool,
|
||||
is_low_priority: bool,
|
||||
canonical_alias: Option<String>,
|
||||
alternative_aliases: Vec<String>,
|
||||
membership: Membership,
|
||||
@@ -48,11 +87,13 @@ pub struct RoomInfo {
|
||||
active_members_count: u64,
|
||||
invited_members_count: u64,
|
||||
joined_members_count: u64,
|
||||
service_members: Vec<String>,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
has_room_call: bool,
|
||||
active_room_call_participants: Vec<String>,
|
||||
active_room_call_consensus_intent: RtcCallIntentConsensus,
|
||||
/// Whether this room has been explicitly marked as unread
|
||||
is_marked_unread: bool,
|
||||
/// "Interesting" messages received in that room, independently of the
|
||||
@@ -119,6 +160,7 @@ impl RoomInfo {
|
||||
is_space: room.is_space(),
|
||||
successor_room: room.successor_room().map(Into::into),
|
||||
is_favourite: room.is_favourite(),
|
||||
is_low_priority: room.is_low_priority(),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
|
||||
membership: room.state().into(),
|
||||
@@ -138,6 +180,12 @@ impl RoomInfo {
|
||||
active_members_count: room.active_members_count(),
|
||||
invited_members_count: room.invited_members_count(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
service_members: room
|
||||
.service_members()
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|m| m.to_string())
|
||||
.collect(),
|
||||
highlight_count: unread_notification_counts.highlight_count,
|
||||
notification_count: unread_notification_counts.notification_count,
|
||||
cached_user_defined_notification_mode: room
|
||||
@@ -149,6 +197,7 @@ impl RoomInfo {
|
||||
.iter()
|
||||
.map(|u| u.to_string())
|
||||
.collect(),
|
||||
active_room_call_consensus_intent: room.active_room_call_consensus_intent().into(),
|
||||
is_marked_unread: room.is_marked_unread(),
|
||||
num_unread_messages: room.num_unread_messages(),
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::RoomDisplayName;
|
||||
|
||||
/// Verifies the passed `String` matches the expected room alias format:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Copyright 2024 Mauro Romito
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -10,7 +9,7 @@
|
||||
// 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
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
@@ -1,32 +1,46 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{StreamExt, pin_mut};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
},
|
||||
Room as SdkRoom,
|
||||
ruma::{
|
||||
RoomId,
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
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_identifiers,
|
||||
new_filter_invite, 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,
|
||||
BoxedFilterFn, RoomCategory, new_filter_all, new_filter_any, new_filter_category,
|
||||
new_filter_deduplicate_versions, new_filter_favourite, new_filter_fuzzy_match_room_name,
|
||||
new_filter_identifiers, new_filter_invite, 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,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
room::{Membership, Room},
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -168,15 +182,6 @@ 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;
|
||||
|
||||
@@ -214,7 +219,7 @@ impl RoomList {
|
||||
|
||||
// Get a reference to `this`. It is only borrowed, it's not moved.
|
||||
let this =
|
||||
// SAFETY: `ptr` is correct aligned, the `this` field is correctly aligned,
|
||||
// SAFETY: `ptr` is correctly aligned, the `this` field is correctly aligned,
|
||||
// is dereferenceable and points to a correctly initialized value as done
|
||||
// in the previous line.
|
||||
unsafe { addr_of_mut!((*ptr).this).as_ref() }
|
||||
@@ -225,10 +230,7 @@ impl RoomList {
|
||||
// borrowing `this`, which is going to live long enough since it will live as
|
||||
// long as `entries_stream` and `dynamic_entries_controller`.
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
this.inner.entries_with_dynamic_adapters_with(
|
||||
page_size.try_into().unwrap(),
|
||||
enable_latest_event_sorter,
|
||||
);
|
||||
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
|
||||
// FFI dance to make those values consumable by foreign language, nothing fancy
|
||||
// here, that's the real code for this method.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
|
||||
use ruma::{UserId, events::room::power_levels::UserPowerLevel};
|
||||
|
||||
use crate::error::{ClientError, NotYetImplemented};
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use matrix_sdk::{Client, room_preview::RoomPreview as SdkRoomPreview};
|
||||
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -21,8 +21,13 @@ use std::{
|
||||
use extension_trait::extension_trait;
|
||||
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
|
||||
use ruma::{
|
||||
assign,
|
||||
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
|
||||
OwnedRoomId, OwnedUserId, UInt, UserId, assign,
|
||||
events::{
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
RoomAccountDataEventType as RumaRoomAccountDataEventType,
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
@@ -36,6 +41,8 @@ use ruma::{
|
||||
poll::start::PollKind as RumaPollKind,
|
||||
push_rules::PushRulesEventContent,
|
||||
room::{
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
message::{
|
||||
AudioInfo as RumaAudioInfo,
|
||||
AudioMessageEventContent as RumaAudioMessageEventContent,
|
||||
@@ -53,10 +60,10 @@ use ruma::{
|
||||
VideoInfo as RumaVideoInfo,
|
||||
VideoMessageEventContent as RumaVideoMessageEventContent,
|
||||
},
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
rtc::notification::NotificationType as RumaNotificationType,
|
||||
rtc::notification::{
|
||||
CallIntent as RumaCallIntent, NotificationType as RumaNotificationType,
|
||||
},
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent,
|
||||
key::{
|
||||
@@ -70,10 +77,6 @@ use ruma::{
|
||||
TagEventContent, TagInfo as RumaTagInfo, TagName as RumaTagName,
|
||||
UserTagName as RumaUserTagName,
|
||||
},
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
RoomAccountDataEventType as RumaRoomAccountDataEventType,
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
push::{
|
||||
@@ -81,8 +84,6 @@ use ruma::{
|
||||
Ruleset as RumaRuleset, SimplePushRule as RumaSimplePushRule,
|
||||
},
|
||||
serde::JsonObject,
|
||||
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
|
||||
OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
@@ -389,11 +390,7 @@ pub enum MessageType {
|
||||
/// is its own field.
|
||||
/// - if a media only has a filename, then body is the filename.
|
||||
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
|
||||
if let Some(caption) = caption {
|
||||
(caption, Some(filename))
|
||||
} else {
|
||||
(filename, None)
|
||||
}
|
||||
if let Some(caption) = caption { (caption, Some(filename)) } else { (filename, None) }
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
@@ -470,11 +467,7 @@ impl TryFrom<RumaMessageType> for MessageType {
|
||||
geo_uri: c.geo_uri,
|
||||
description,
|
||||
zoom_level: zoom_level.and_then(|z| z.get().try_into().ok()),
|
||||
asset: c.asset.and_then(|a| match a.type_ {
|
||||
RumaAssetType::Self_ => Some(AssetType::Sender),
|
||||
RumaAssetType::Pin => Some(AssetType::Pin),
|
||||
_ => None,
|
||||
}),
|
||||
asset: c.asset.map(|a| a.type_).into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -510,6 +503,31 @@ impl From<RtcNotificationType> for RumaNotificationType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RtcCallIntent {
|
||||
Video,
|
||||
Audio,
|
||||
}
|
||||
|
||||
impl From<RumaCallIntent> for RtcCallIntent {
|
||||
fn from(val: RumaCallIntent) -> Self {
|
||||
match val {
|
||||
RumaCallIntent::Audio => Self::Audio,
|
||||
// No support for custom intents, so we can just use video as default
|
||||
_ => Self::Video,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RtcCallIntent> for RumaCallIntent {
|
||||
fn from(value: RtcCallIntent) -> Self {
|
||||
match value {
|
||||
RtcCallIntent::Video => RumaCallIntent::Video,
|
||||
RtcCallIntent::Audio => RumaCallIntent::Audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct EmoteMessageContent {
|
||||
pub body: String,
|
||||
@@ -543,7 +561,7 @@ impl TryFrom<RumaImageMessageEventContent> for ImageMessageContent {
|
||||
fn try_from(value: RumaImageMessageEventContent) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
filename: value.filename().to_owned(),
|
||||
caption: value.caption().map(ToString::to_string),
|
||||
caption: value.caption().map(str::to_owned),
|
||||
formatted_caption: value.formatted_caption().map(Into::into),
|
||||
source: Arc::new(value.source.try_into()?),
|
||||
info: value.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
@@ -582,7 +600,7 @@ impl TryFrom<RumaAudioMessageEventContent> for AudioMessageContent {
|
||||
fn try_from(value: RumaAudioMessageEventContent) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
filename: value.filename().to_owned(),
|
||||
caption: value.caption().map(ToString::to_string),
|
||||
caption: value.caption().map(str::to_owned),
|
||||
formatted_caption: value.formatted_caption().map(Into::into),
|
||||
source: Arc::new(value.source.try_into()?),
|
||||
info: value.info.as_deref().map(Into::into),
|
||||
@@ -619,7 +637,7 @@ impl TryFrom<RumaVideoMessageEventContent> for VideoMessageContent {
|
||||
fn try_from(value: RumaVideoMessageEventContent) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
filename: value.filename().to_owned(),
|
||||
caption: value.caption().map(ToString::to_string),
|
||||
caption: value.caption().map(str::to_owned),
|
||||
formatted_caption: value.formatted_caption().map(Into::into),
|
||||
source: Arc::new(value.source.try_into()?),
|
||||
info: value.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
@@ -654,7 +672,7 @@ impl TryFrom<RumaFileMessageEventContent> for FileMessageContent {
|
||||
fn try_from(value: RumaFileMessageEventContent) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
filename: value.filename().to_owned(),
|
||||
caption: value.caption().map(ToString::to_string),
|
||||
caption: value.caption().map(str::to_owned),
|
||||
formatted_caption: value.formatted_caption().map(Into::into),
|
||||
source: Arc::new(value.source.try_into()?),
|
||||
info: value.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
@@ -900,13 +918,14 @@ pub struct LocationContent {
|
||||
pub geo_uri: String,
|
||||
pub description: Option<String>,
|
||||
pub zoom_level: Option<u8>,
|
||||
pub asset: Option<AssetType>,
|
||||
pub asset: AssetType,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum AssetType {
|
||||
Sender,
|
||||
Pin,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<AssetType> for RumaAssetType {
|
||||
@@ -914,6 +933,26 @@ impl From<AssetType> for RumaAssetType {
|
||||
match value {
|
||||
AssetType::Sender => Self::Self_,
|
||||
AssetType::Pin => Self::Pin,
|
||||
_ => panic!("Invalid asset type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaAssetType> for AssetType {
|
||||
fn from(value: RumaAssetType) -> Self {
|
||||
match value {
|
||||
RumaAssetType::Self_ => Self::Sender,
|
||||
RumaAssetType::Pin => Self::Pin,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<RumaAssetType>> for AssetType {
|
||||
fn from(value: Option<RumaAssetType>) -> Self {
|
||||
match value {
|
||||
None => Self::Sender,
|
||||
Some(asset_type) => asset_type.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1670,6 +1709,7 @@ pub enum RoomAccountDataEvent {
|
||||
|
||||
/// The name of a tag.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, uniffi::Enum)]
|
||||
#[uniffi::export(Eq, Hash)]
|
||||
pub enum TagName {
|
||||
/// `m.favourite`: The user's favorite rooms.
|
||||
Favorite,
|
||||
|
||||
@@ -39,7 +39,7 @@ mod sys {
|
||||
mod sys {
|
||||
use std::future::Future;
|
||||
|
||||
use matrix_sdk_common::executor::{spawn, JoinHandle};
|
||||
use matrix_sdk_common::executor::{JoinHandle, spawn};
|
||||
|
||||
/// A dummy guard that does nothing when dropped.
|
||||
/// This is used for the Wasm implementation to match
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::TimelineEvent,
|
||||
message_search::{
|
||||
GlobalSearchIterator as SdkGlobalSearchIterator,
|
||||
RoomSearchIterator as SdkRoomSearchIterator, SearchError as SdkSearchError,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_ui::timeline::TimelineDetails;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
client::Client,
|
||||
error::ClientError,
|
||||
room::Room,
|
||||
timeline::{ProfileDetails, TimelineItemContent},
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Error, thiserror::Error, Debug)]
|
||||
pub enum SearchError {
|
||||
#[error("Failed to search through the index: {0}")]
|
||||
IndexError(String),
|
||||
#[error("Failed to load event content for search result: {0}")]
|
||||
EventLoadError(String),
|
||||
}
|
||||
|
||||
impl From<SdkSearchError> for SearchError {
|
||||
fn from(err: SdkSearchError) -> Self {
|
||||
match err {
|
||||
SdkSearchError::IndexError(err) => SearchError::IndexError(err.to_string()),
|
||||
SdkSearchError::EventLoadError(err) => SearchError::EventLoadError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Room {
|
||||
/// Search for messages in this room matching the given query, returning an
|
||||
/// iterator over the results that yields `num_results_per_batch` results at
|
||||
/// a time.
|
||||
pub fn search_messages(&self, query: String, num_results_per_batch: u32) -> RoomSearchIterator {
|
||||
RoomSearchIterator {
|
||||
sdk_room: self.inner.clone(),
|
||||
inner: Mutex::new(self.inner.search_messages(query, num_results_per_batch as usize)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomSearchIterator {
|
||||
sdk_room: matrix_sdk::room::Room,
|
||||
inner: Mutex<SdkRoomSearchIterator>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomSearchIterator {
|
||||
/// Return a list of events for the next batch of search results, or `None`
|
||||
/// if there are no more results.
|
||||
pub async fn next_events(&self) -> Result<Option<Vec<RoomSearchResult>>, SearchError> {
|
||||
let Some(events) = self.inner.lock().await.next_events().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut results = Vec::with_capacity(events.len());
|
||||
|
||||
for event in events {
|
||||
if let Some(result) = RoomSearchResult::from(&self.sdk_room, event).await {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
results.shrink_to_fit();
|
||||
|
||||
Ok(Some(results))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomSearchResult {
|
||||
event_id: String,
|
||||
sender: String,
|
||||
sender_profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl RoomSearchResult {
|
||||
async fn from(room: &matrix_sdk::room::Room, event: TimelineEvent) -> Option<Self> {
|
||||
let sender = event.sender()?;
|
||||
|
||||
let event_id = event.event_id().unwrap().to_string();
|
||||
let timestamp =
|
||||
event.timestamp().unwrap_or_else(ruma::MilliSecondsSinceUnixEpoch::now).into();
|
||||
|
||||
let content = matrix_sdk_ui::timeline::TimelineItemContent::from_event(room, event).await?;
|
||||
let profile = TimelineDetails::from_initial_value(
|
||||
matrix_sdk_ui::timeline::Profile::load(room, &sender).await,
|
||||
);
|
||||
|
||||
Some(Self {
|
||||
event_id,
|
||||
sender: sender.to_string(),
|
||||
sender_profile: ProfileDetails::from(profile),
|
||||
content: TimelineItemContent::from(content),
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SearchRoomFilter {
|
||||
/// All the joined rooms (= DMs + non-DMs).
|
||||
Rooms,
|
||||
/// Only joined DM rooms.
|
||||
Dms,
|
||||
/// Only joined non-DM (group) rooms.
|
||||
NonDms,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Search across all all rooms for the given query, returning an iterator
|
||||
/// over the results.
|
||||
pub async fn search_messages(
|
||||
&self,
|
||||
query: String,
|
||||
filter: SearchRoomFilter,
|
||||
num_results_per_batch: u32,
|
||||
) -> Result<GlobalSearchIterator, ClientError> {
|
||||
let sdk_client = (*self.inner).clone();
|
||||
let mut search = sdk_client.search_messages(query, num_results_per_batch as usize);
|
||||
|
||||
match filter {
|
||||
SearchRoomFilter::Rooms => {}
|
||||
SearchRoomFilter::Dms => search = search.only_dm_rooms().await?,
|
||||
SearchRoomFilter::NonDms => search = search.no_dms().await?,
|
||||
}
|
||||
|
||||
Ok(GlobalSearchIterator { sdk_client, inner: Mutex::new(search.build()) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct GlobalSearchResult {
|
||||
room_id: String,
|
||||
result: RoomSearchResult,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct GlobalSearchIterator {
|
||||
sdk_client: matrix_sdk::Client,
|
||||
inner: Mutex<SdkGlobalSearchIterator>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl GlobalSearchIterator {
|
||||
/// Return a list of events for the next batch of search results, or `None`
|
||||
/// if there are no more results.
|
||||
pub async fn next_events(&self) -> Result<Option<Vec<GlobalSearchResult>>, SearchError> {
|
||||
let Some(events) = self.inner.lock().await.next_events().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut results = Vec::with_capacity(events.len());
|
||||
|
||||
for (room_id, event) in events {
|
||||
let Some(room) = self.sdk_client.get_room(&room_id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(result) = RoomSearchResult::from(&room, event).await {
|
||||
results.push(GlobalSearchResult { room_id: room_id.to_string(), result });
|
||||
}
|
||||
}
|
||||
|
||||
results.shrink_to_fit();
|
||||
|
||||
Ok(Some(results))
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
Account,
|
||||
encryption::{
|
||||
Encryption,
|
||||
identities::UserIdentity,
|
||||
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
|
||||
Encryption,
|
||||
},
|
||||
ruma::events::key::verification::VerificationMethod,
|
||||
Account,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::UserId;
|
||||
@@ -232,16 +246,15 @@ impl SessionVerificationController {
|
||||
sender: &UserId,
|
||||
flow_id: impl AsRef<str>,
|
||||
) {
|
||||
if sender != self.user_identity.user_id() {
|
||||
if let Some(status) = self.encryption.cross_signing_status().await {
|
||||
if !status.is_complete() {
|
||||
warn!(
|
||||
"Cannot verify other users until our own device's cross-signing status \
|
||||
is complete: {status:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if sender != self.user_identity.user_id()
|
||||
&& let Some(status) = self.encryption.cross_signing_status().await
|
||||
&& !status.is_complete()
|
||||
{
|
||||
warn!(
|
||||
"Cannot verify other users until our own device's cross-signing status \
|
||||
is complete: {status:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
|
||||
@@ -264,7 +277,7 @@ impl SessionVerificationController {
|
||||
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),
|
||||
device_display_name: other_device_data.display_name().map(str::to_owned),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
|
||||
});
|
||||
}
|
||||
@@ -276,15 +289,10 @@ impl SessionVerificationController {
|
||||
) -> Result<(), ClientError> {
|
||||
if let Some(ongoing_verification_request) =
|
||||
self.verification_request.read().unwrap().clone()
|
||||
&& !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
if !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
return Err(ClientError::from_str(
|
||||
"There is another verification flow ongoing.",
|
||||
None,
|
||||
));
|
||||
}
|
||||
return Err(ClientError::from_str("There is another verification flow ongoing.", None));
|
||||
}
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
@@ -15,23 +15,23 @@
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{StreamExt, pin_mut};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
|
||||
SpaceService as UISpaceService,
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_preview::RoomType,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
/// The main entry point into the Spaces facilities.
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ use matrix_sdk_ui::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
|
||||
runtime::get_runtime_handle, TaskHandle,
|
||||
TaskHandle, error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
|
||||
runtime::get_runtime_handle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -115,12 +115,6 @@ impl SyncServiceBuilder {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SyncServiceBuilder {
|
||||
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_cross_process_lock();
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
/// Enable the "offline" mode for the [`SyncService`].
|
||||
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
@@ -134,6 +128,28 @@ impl SyncServiceBuilder {
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
/// Set a custom Sliding Sync connection ID for the room list service.
|
||||
///
|
||||
/// By default [`matrix_sdk_ui::room_list_service::DEFAULT_CONNECTION_ID`]
|
||||
/// is used. Set a different value for secondary processes such as iOS
|
||||
/// Share Extensions that are not meant to reuse the main app's
|
||||
/// connection.
|
||||
pub fn with_room_list_connection_id(self: Arc<Self>, connection_id: String) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_room_list_conn_id(connection_id);
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
/// Set a custom timeline limit for the room list service.
|
||||
///
|
||||
/// When set, overrides the default timeline limit of
|
||||
/// [`matrix_sdk_ui::room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`].
|
||||
pub fn with_room_list_timeline_limit(self: Arc<Self>, limit: u32) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_room_list_timeline_limit(limit);
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Ok(Arc::new(SyncService {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2026 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::time::Duration;
|
||||
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
/// A listener for the sync loop.
|
||||
///
|
||||
/// Called after each successful sync response when using
|
||||
/// [`Client::sync_v2`](crate::client::Client::sync_v2).
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SyncListenerV2: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called after each successful sync response.
|
||||
fn on_update(&self, response: SyncResponseV2);
|
||||
}
|
||||
|
||||
/// Settings for a sync v2 call.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SyncSettingsV2 {
|
||||
/// Timeout in milliseconds for the server long-poll.
|
||||
/// If not set, defaults to 30 seconds.
|
||||
#[uniffi(default = None)]
|
||||
pub timeout_ms: Option<u64>,
|
||||
/// Whether to request full state on the first sync.
|
||||
#[uniffi(default = false)]
|
||||
pub full_state: bool,
|
||||
}
|
||||
|
||||
impl From<SyncSettingsV2> for matrix_sdk::config::SyncSettings {
|
||||
fn from(value: SyncSettingsV2) -> Self {
|
||||
let mut settings = matrix_sdk::config::SyncSettings::new();
|
||||
if let Some(timeout_ms) = value.timeout_ms {
|
||||
settings = settings.timeout(Duration::from_millis(timeout_ms));
|
||||
}
|
||||
if value.full_state {
|
||||
settings = settings.full_state(true);
|
||||
}
|
||||
settings
|
||||
}
|
||||
}
|
||||
|
||||
/// The response from a sync v2 call.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SyncResponseV2 {
|
||||
/// The batch token to supply in the `since` param of the next `/sync`
|
||||
/// request.
|
||||
pub next_batch: String,
|
||||
/// Updates to rooms.
|
||||
pub rooms: SyncResponseRoomsV2,
|
||||
}
|
||||
|
||||
/// Room updates from a sync v2 response.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SyncResponseRoomsV2 {
|
||||
/// Room IDs of rooms the user has been invited to.
|
||||
pub invited: Vec<String>,
|
||||
/// Room IDs of joined rooms that had updates.
|
||||
pub joined: Vec<String>,
|
||||
/// Room IDs of rooms the user has left.
|
||||
pub left: Vec<String>,
|
||||
/// Room IDs of rooms the user has knocked on.
|
||||
pub knocked: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::sync::SyncResponse> for SyncResponseV2 {
|
||||
fn from(value: matrix_sdk::sync::SyncResponse) -> Self {
|
||||
Self {
|
||||
next_batch: value.next_batch,
|
||||
rooms: SyncResponseRoomsV2 {
|
||||
invited: value.rooms.invited.keys().map(ToString::to_string).collect(),
|
||||
joined: value.rooms.joined.keys().map(ToString::to_string).collect(),
|
||||
left: value.rooms.left.keys().map(ToString::to_string).collect(),
|
||||
knocked: value.rooms.knocked.keys().map(ToString::to_string).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::executor::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::{
|
||||
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
|
||||
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
|
||||
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
|
||||
};
|
||||
use ruma::{
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
EventId,
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
};
|
||||
|
||||
use super::FocusEventError;
|
||||
|
||||
@@ -17,12 +17,12 @@ use std::collections::HashMap;
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
|
||||
use ruma::events::{
|
||||
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, FullStateEventContent,
|
||||
StateEventContentChange, room::history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::JoinRule, event::TimelineEventType, timeline::msg_like::MsgLikeContent,
|
||||
utils::Timestamp,
|
||||
client::JoinRule, event::TimelineEventType, ruma::AssetType,
|
||||
timeline::msg_like::MsgLikeContent, utils::Timestamp,
|
||||
};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
@@ -40,11 +40,13 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::RtcNotification => TimelineItemContent::RtcNotification,
|
||||
Content::RtcNotification { call_intent } => TimelineItemContent::RtcNotification {
|
||||
call_intent: call_intent.map(|s| s.to_string()),
|
||||
},
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
FullStateEventContent::Original { content, .. } => content.reason.clone(),
|
||||
StateEventContentChange::Original { content, .. } => content.reason.clone(),
|
||||
_ => None,
|
||||
};
|
||||
TimelineItemContent::RoomMembership {
|
||||
@@ -133,8 +135,8 @@ pub enum HistoryVisibility {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: RumaHistoryVisibility) -> Self {
|
||||
impl From<&RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: &RumaHistoryVisibility) -> Self {
|
||||
match value {
|
||||
RumaHistoryVisibility::Invited => Self::Invited,
|
||||
RumaHistoryVisibility::Joined => Self::Joined,
|
||||
@@ -159,7 +161,9 @@ pub enum TimelineItemContent {
|
||||
content: MsgLikeContent,
|
||||
},
|
||||
CallInvite,
|
||||
RtcNotification,
|
||||
RtcNotification {
|
||||
call_intent: Option<String>,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
@@ -265,18 +269,17 @@ pub enum OtherState {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar {
|
||||
url: Option<String>,
|
||||
},
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate {
|
||||
federate: Option<bool>,
|
||||
federate: bool,
|
||||
},
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility {
|
||||
history_visibility: Option<HistoryVisibility>,
|
||||
history_visibility: HistoryVisibility,
|
||||
},
|
||||
RoomJoinRules {
|
||||
join_rule: Option<JoinRule>,
|
||||
@@ -292,7 +295,7 @@ pub enum OtherState {
|
||||
previous_events: Option<HashMap<TimelineEventType, i64>>,
|
||||
users: HashMap<String, i64>,
|
||||
previous_users: Option<HashMap<String, i64>>,
|
||||
thresholds: Option<PowerLevelChanges>,
|
||||
thresholds: PowerLevelChanges,
|
||||
previous_thresholds: Option<PowerLevelChanges>,
|
||||
},
|
||||
RoomServerAcl,
|
||||
@@ -310,16 +313,59 @@ pub enum OtherState {
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState {
|
||||
fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self {
|
||||
use matrix_sdk::ruma::events::FullStateEventContent as FullContent;
|
||||
use matrix_sdk_ui::timeline::AnyOtherFullStateEventContent as Content;
|
||||
/// FFI representation of a single location update from a beacon event.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct BeaconInfo {
|
||||
/// The geo URI carrying the user's coordinates
|
||||
/// (e.g. `"geo:51.5008,0.1247;u=35"`).
|
||||
pub geo_uri: String,
|
||||
|
||||
/// Timestamp (ms since Unix Epoch) of this location update.
|
||||
pub ts: Timestamp,
|
||||
|
||||
/// An optional human-readable description of the location.
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// FFI representation of a live location sharing session (MSC3489).
|
||||
///
|
||||
/// Corresponds to a `org.matrix.msc3672.beacon_info` state event in the
|
||||
/// timeline. Location updates are aggregated here as they arrive.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct LiveLocationContent {
|
||||
/// Whether this sharing session is currently active.
|
||||
pub is_live: bool,
|
||||
|
||||
/// The timestamp when this live location sharing session started
|
||||
/// (from the `org.matrix.msc3488.ts` field of the originating
|
||||
/// `beacon_info` state event).
|
||||
///
|
||||
/// This marks the *beginning* of the session. The session expires at
|
||||
/// `ts + timeout_ms`.
|
||||
pub ts: Timestamp,
|
||||
|
||||
/// An optional human-readable label for this sharing session.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Duration of the session in milliseconds.
|
||||
pub timeout_ms: u64,
|
||||
|
||||
/// The asset type of the beacon (e.g. `Sender` for the user's own
|
||||
/// location, `Pin` for a fixed point of interest).
|
||||
pub asset_type: AssetType,
|
||||
|
||||
/// All location updates received so far, sorted oldest-first.
|
||||
pub locations: Vec<BeaconInfo>,
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk_ui::timeline::AnyOtherStateEventContentChange> for OtherState {
|
||||
fn from(content: &matrix_sdk_ui::timeline::AnyOtherStateEventContentChange) -> Self {
|
||||
use matrix_sdk::ruma::events::StateEventContentChange as FullContent;
|
||||
use matrix_sdk_ui::timeline::AnyOtherStateEventContentChange as Content;
|
||||
match content {
|
||||
Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
|
||||
Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
|
||||
Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
|
||||
Content::RoomAliases(_) => Self::RoomAliases,
|
||||
Content::RoomAvatar(c) => {
|
||||
let url = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
@@ -332,8 +378,8 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
|
||||
Content::RoomCreate(c) => {
|
||||
let federate = match c {
|
||||
FullContent::Original { content, .. } => Some(content.federate),
|
||||
FullContent::Redacted(_) => None,
|
||||
FullContent::Original { content, .. } => content.federate,
|
||||
FullContent::Redacted(content) => content.federate,
|
||||
};
|
||||
Self::RoomCreate { federate }
|
||||
}
|
||||
@@ -341,25 +387,22 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
|
||||
Content::RoomHistoryVisibility(c) => {
|
||||
let history_visibility = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
Some(content.history_visibility.clone().into())
|
||||
}
|
||||
FullContent::Redacted(_) => None,
|
||||
FullContent::Original { content, .. } => &content.history_visibility,
|
||||
FullContent::Redacted(content) => &content.history_visibility,
|
||||
};
|
||||
Self::RoomHistoryVisibility { history_visibility }
|
||||
Self::RoomHistoryVisibility { history_visibility: history_visibility.into() }
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
let ruma_join_rule = match c {
|
||||
FullContent::Original { content, .. } => &content.join_rule,
|
||||
FullContent::Redacted(content) => &content.join_rule,
|
||||
};
|
||||
let join_rule = match ruma_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 }
|
||||
}
|
||||
@@ -371,8 +414,13 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Self::RoomName { name }
|
||||
}
|
||||
Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
|
||||
Content::RoomPowerLevels(c) => match c {
|
||||
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
|
||||
Content::RoomPowerLevels(c) => {
|
||||
let (content, prev_content) = match c.clone() {
|
||||
FullContent::Original { content, prev_content } => (content, prev_content),
|
||||
FullContent::Redacted(content) => (content.into(), None),
|
||||
};
|
||||
|
||||
Self::RoomPowerLevels {
|
||||
events: content
|
||||
.events
|
||||
.iter()
|
||||
@@ -385,7 +433,7 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
.map(|(k, &v)| (k.clone().into(), v.into()))
|
||||
.collect()
|
||||
}),
|
||||
thresholds: Some(PowerLevelChanges {
|
||||
thresholds: PowerLevelChanges {
|
||||
ban: content.ban.into(),
|
||||
kick: content.kick.into(),
|
||||
events_default: content.events_default.into(),
|
||||
@@ -394,7 +442,7 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
state_default: content.state_default.into(),
|
||||
users_default: content.users_default.into(),
|
||||
notifications: content.notifications.room.into(),
|
||||
}),
|
||||
},
|
||||
previous_thresholds: prev_content.as_ref().map(|prev_content| {
|
||||
PowerLevelChanges {
|
||||
ban: prev_content.ban.into(),
|
||||
@@ -407,23 +455,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
notifications: prev_content.notifications.room.into(),
|
||||
}
|
||||
}),
|
||||
users: power_level_user_changes(content, prev_content)
|
||||
users: power_level_user_changes(&content, &prev_content)
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect(),
|
||||
previous_users: prev_content.as_ref().map(|prev_content| {
|
||||
prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
|
||||
}),
|
||||
},
|
||||
FullContent::Redacted(_) => Self::RoomPowerLevels {
|
||||
events: Default::default(),
|
||||
previous_events: None,
|
||||
users: Default::default(),
|
||||
previous_users: None,
|
||||
thresholds: None,
|
||||
previous_thresholds: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Content::RoomServerAcl(_) => Self::RoomServerAcl,
|
||||
Content::RoomThirdPartyInvite(c) => {
|
||||
let display_name = match c {
|
||||
|
||||
@@ -21,7 +21,7 @@ use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
event_cache::RoomPaginationStatus,
|
||||
event_cache::PaginationStatus,
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
@@ -38,8 +38,9 @@ use matrix_sdk_ui::timeline::{
|
||||
use mime::Mime;
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
use ruma::{
|
||||
assign,
|
||||
EventId, UInt, assign,
|
||||
events::{
|
||||
AnyMessageLikeEventContent,
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
poll::{
|
||||
unstable_end::UnstablePollEndEventContent,
|
||||
@@ -53,16 +54,13 @@ use ruma::{
|
||||
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
|
||||
TextMessageEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::TimelineItemContent;
|
||||
pub use self::msg_like::MessageContent;
|
||||
pub use self::{content::TimelineItemContent, msg_like::MessageContent};
|
||||
use crate::{
|
||||
error::{ClientError, RoomError},
|
||||
event::EventOrTransactionId,
|
||||
@@ -79,6 +77,7 @@ pub mod configuration;
|
||||
mod content;
|
||||
mod msg_like;
|
||||
mod reply;
|
||||
pub mod threads;
|
||||
|
||||
use matrix_sdk::utils::formatted_body_from;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
@@ -284,8 +283,17 @@ impl Timeline {
|
||||
// be that the listener be called before the initial items have been
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
|
||||
// Note we pass initial items as a reset update, as a way to give the callers a
|
||||
// unified way to handle the initial batch of items as well as other
|
||||
// batches, instead of having a separate callback for the initial items.
|
||||
//
|
||||
// Start with passing all the items of a *non-empty* timeline as a reset update
|
||||
// (if the initial items are empty, then the timeline would transition
|
||||
// from empty to empty, which is a no-op).
|
||||
if !timeline_items.is_empty() {
|
||||
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);
|
||||
@@ -821,7 +829,7 @@ pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, status: RoomPaginationStatus);
|
||||
fn on_update(&self, status: PaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
@@ -1012,6 +1020,9 @@ pub struct EventTimelineItem {
|
||||
is_own: bool,
|
||||
is_editable: bool,
|
||||
content: TimelineItemContent,
|
||||
/// The raw Matrix event type string (e.g. `"m.room.message"`), or `None`
|
||||
/// when the original type is not available (e.g. redacted events).
|
||||
event_type_raw: Option<String>,
|
||||
timestamp: Timestamp,
|
||||
local_send_state: Option<EventSendState>,
|
||||
local_created_at: Option<u64>,
|
||||
@@ -1037,6 +1048,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
event_type_raw: item.content().event_type_str(),
|
||||
timestamp: item.timestamp().into(),
|
||||
local_send_state: item.send_state().map(|s| s.into()),
|
||||
local_created_at: item.local_created_at().map(|t| t.0.into()),
|
||||
@@ -1314,6 +1326,13 @@ impl LazyTimelineItemProvider {
|
||||
fn contains_only_emojis(&self) -> bool {
|
||||
self.0.contains_only_emojis()
|
||||
}
|
||||
|
||||
/// Returns the full raw JSON string of the latest version of the event
|
||||
/// (including edits). Returns `None` for local echoes that haven't been
|
||||
/// echoed back by the server yet.
|
||||
fn latest_json(&self) -> Option<String> {
|
||||
Some(self.0.latest_json()?.json().get().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// Mimic the [`UiLatestEventValue`] type.
|
||||
@@ -1387,7 +1406,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 ruma::{EventId, assign, events::room::message::TextMessageEventContent};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
@@ -1395,7 +1414,7 @@ mod galleries {
|
||||
error::RoomError,
|
||||
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{build_thumbnail_info, Timeline, UploadSource},
|
||||
timeline::{Timeline, UploadSource, build_thumbnail_info},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
use ruma::events::{MessageLikeEventContent, room::MediaSource as RumaMediaSource};
|
||||
|
||||
use super::{
|
||||
content::Reaction,
|
||||
content::{BeaconInfo, LiveLocationContent, Reaction},
|
||||
reply::{EmbeddedEventDetails, InReplyToDetails},
|
||||
};
|
||||
use crate::{
|
||||
@@ -54,6 +54,12 @@ pub enum MsgLikeKind {
|
||||
|
||||
/// A custom message like event.
|
||||
Other { event_type: MessageLikeEventType },
|
||||
|
||||
/// A live location sharing session (MSC3489).
|
||||
///
|
||||
/// Represents a `org.matrix.msc3672.beacon_info` state event with all
|
||||
/// aggregated location updates from `org.matrix.msc3672.beacon` events.
|
||||
LiveLocation { content: LiveLocationContent },
|
||||
}
|
||||
|
||||
/// A special kind of [`super::TimelineItemContent`] that groups together
|
||||
@@ -195,6 +201,34 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
Kind::LiveLocation(state) => {
|
||||
let locations = state
|
||||
.locations()
|
||||
.iter()
|
||||
.map(|location| BeaconInfo {
|
||||
geo_uri: location.geo_uri().to_owned(),
|
||||
ts: location.ts().into(),
|
||||
description: location.description().map(ToOwned::to_owned),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::LiveLocation {
|
||||
content: LiveLocationContent {
|
||||
is_live: state.is_live(),
|
||||
ts: state.ts().into(),
|
||||
description: state.description().map(ToOwned::to_owned),
|
||||
timeout_ms: state.timeout().as_millis() as u64,
|
||||
asset_type: state.asset_type().into(),
|
||||
locations,
|
||||
},
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
|
||||
|
||||
use super::{content::TimelineItemContent, ProfileDetails};
|
||||
use super::{ProfileDetails, content::TimelineItemContent};
|
||||
use crate::{event::EventOrTransactionId, utils::Timestamp};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
// Copyright 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room::{
|
||||
ListThreadsOptions as SdkListThreadsOptions, ThreadSubscription as SdkThreadSubscription,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
RoomExt,
|
||||
thread_list_service::{
|
||||
ThreadListItem as UIThreadListItem, ThreadListItemEvent as UIThreadListItemEvent,
|
||||
ThreadListPaginationState as UIThreadListPaginationState,
|
||||
ThreadListService as UIThreadListService,
|
||||
ThreadListServiceError as UIThreadListServiceError,
|
||||
},
|
||||
};
|
||||
use ruma::api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads;
|
||||
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
error::ClientError,
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{ProfileDetails, TimelineItemContent},
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
pub automatic: bool,
|
||||
}
|
||||
|
||||
impl From<ThreadSubscription> for SdkThreadSubscription {
|
||||
fn from(subscription: ThreadSubscription) -> Self {
|
||||
Self { automatic: subscription.automatic }
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for [`Room::load_thread_list`].
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct ListThreadsOptions {
|
||||
/// An extra filter to select which threads should be returned.
|
||||
pub include_threads: IncludeThreads,
|
||||
|
||||
/// The token to start returning events from.
|
||||
///
|
||||
/// This token can be obtained from a [`ThreadList::prev_batch_token`]
|
||||
/// returned by a previous call to [`Room::load_thread_list()`].
|
||||
///
|
||||
/// If `from` isn't provided the homeserver shall return a list of thread
|
||||
/// roots from end of the timeline history.
|
||||
pub from: Option<String>,
|
||||
|
||||
/// The maximum number of events to return.
|
||||
///
|
||||
/// Default: 10.
|
||||
pub limit: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<ListThreadsOptions> for SdkListThreadsOptions {
|
||||
fn from(opts: ListThreadsOptions) -> Self {
|
||||
Self {
|
||||
include_threads: opts.include_threads.into(),
|
||||
from: opts.from,
|
||||
limit: opts.limit.and_then(ruma::UInt::new),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which threads to include in the response.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum IncludeThreads {
|
||||
/// `all`
|
||||
///
|
||||
/// Include all thread roots found in the room.
|
||||
///
|
||||
/// This is the default.
|
||||
All,
|
||||
|
||||
/// `participated`
|
||||
///
|
||||
/// Only include thread roots for threads where
|
||||
/// [`current_user_participated`] is `true`.
|
||||
///
|
||||
/// [`current_user_participated`]: https://spec.matrix.org/latest/client-server-api/#server-side-aggregation-of-mthread-relationships
|
||||
Participated,
|
||||
}
|
||||
|
||||
impl From<IncludeThreads> for SdkIncludeThreads {
|
||||
fn from(include_threads: IncludeThreads) -> Self {
|
||||
match include_threads {
|
||||
IncludeThreads::All => Self::All,
|
||||
IncludeThreads::Participated => Self::Participated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Each `ThreadListItem` represents one thread root event in the room. The
|
||||
/// fields are pre-resolved from the raw homeserver response: the sender's
|
||||
/// profile is fetched eagerly and the event content is parsed into a
|
||||
/// `TimelineItemContent` so that consumers can render the item without any
|
||||
/// additional work.
|
||||
///
|
||||
/// `ThreadListItem`s are produced page by page via `Room::load_thread_list()`
|
||||
/// and are accumulated inside the `ThreadListService` as pages are fetched
|
||||
/// through `ThreadListService::paginate()`.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ThreadListItem {
|
||||
/// The thread root event.
|
||||
///
|
||||
/// Contains the event ID, timestamp, sender, sender profile, and parsed
|
||||
/// content of the thread's root message. Use `root_event.event_id` to open
|
||||
/// a per-thread `Timeline` or to navigate the user to the thread view.
|
||||
root_event: ThreadListItemEvent,
|
||||
|
||||
/// The latest event in the thread (i.e. the most recent reply), if
|
||||
/// available.
|
||||
///
|
||||
/// Initially populated from the server's bundled thread summary and
|
||||
/// updated in real time as new events arrive via sync or back-pagination.
|
||||
latest_event: Option<ThreadListItemEvent>,
|
||||
|
||||
/// The number of replies in this thread (excluding the root event).
|
||||
///
|
||||
/// Initially populated from the server's bundled thread summary and
|
||||
/// updated in real time as new events arrive via sync.
|
||||
num_replies: u32,
|
||||
}
|
||||
|
||||
impl From<UIThreadListItem> for ThreadListItem {
|
||||
fn from(item: UIThreadListItem) -> Self {
|
||||
Self {
|
||||
root_event: item.root_event.into(),
|
||||
latest_event: item.latest_event.map(Into::into),
|
||||
num_replies: item.num_replies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about an event in a thread (either the root or the latest
|
||||
/// reply).
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ThreadListItemEvent {
|
||||
/// The event ID.
|
||||
pub event_id: String,
|
||||
/// The timestamp of the event.
|
||||
pub timestamp: Timestamp,
|
||||
/// The sender of the event.
|
||||
pub sender: String,
|
||||
/// The sender's profile details.
|
||||
pub sender_profile: ProfileDetails,
|
||||
/// Whether the event was sent by the current user.
|
||||
pub is_own: bool,
|
||||
/// The content of the event, if available.
|
||||
pub content: Option<TimelineItemContent>,
|
||||
}
|
||||
|
||||
impl From<UIThreadListItemEvent> for ThreadListItemEvent {
|
||||
fn from(event: UIThreadListItemEvent) -> Self {
|
||||
Self {
|
||||
event_id: event.event_id.to_string(),
|
||||
timestamp: event.timestamp.into(),
|
||||
sender: event.sender.to_string(),
|
||||
is_own: event.is_own,
|
||||
sender_profile: event.sender_profile.into(),
|
||||
content: event.content.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Listener for changes to the [`ThreadListService`] pagination state.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ThreadListPaginationStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, state: UIThreadListPaginationState);
|
||||
}
|
||||
|
||||
/// Listener for changes to the [`ThreadListService`] item list.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ThreadListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, diff: Vec<ThreadListUpdate>);
|
||||
}
|
||||
|
||||
/// A diff applied to the observable thread list.
|
||||
///
|
||||
/// Mirrors [`eyeball_im::VectorDiff`] for [`ThreadListItem`].
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum ThreadListUpdate {
|
||||
/// New items were appended at the back.
|
||||
Append { values: Vec<ThreadListItem> },
|
||||
/// The list was cleared.
|
||||
Clear,
|
||||
/// A new item was prepended at the front.
|
||||
PushFront { value: ThreadListItem },
|
||||
/// A new item was appended at the back.
|
||||
PushBack { value: ThreadListItem },
|
||||
/// The first item was removed.
|
||||
PopFront,
|
||||
/// The last item was removed.
|
||||
PopBack,
|
||||
/// An item was inserted at the given position.
|
||||
Insert { index: u32, value: ThreadListItem },
|
||||
/// The item at the given position was replaced.
|
||||
Set { index: u32, value: ThreadListItem },
|
||||
/// The item at the given position was removed.
|
||||
Remove { index: u32 },
|
||||
/// The list was truncated to the given length.
|
||||
Truncate { length: u32 },
|
||||
/// The whole list was replaced with new items.
|
||||
Reset { values: Vec<ThreadListItem> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<UIThreadListItem>> for ThreadListUpdate {
|
||||
fn from(diff: VectorDiff<UIThreadListItem>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(Into::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(Into::into).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A high-level, reactive, paginated list of threads for a room.
|
||||
///
|
||||
/// `ThreadListService` is the FFI-facing wrapper around
|
||||
/// [`matrix_sdk_ui::timeline::thread_list_service::ThreadListService`]. It
|
||||
/// maintains an observable list of [`ThreadListItem`]s and exposes a
|
||||
/// pagination state publisher, making it straightforward to build reactive UIs
|
||||
/// on top of the thread list.
|
||||
///
|
||||
/// Obtain an instance via [`Room::thread_list_service`].
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct ThreadListService {
|
||||
inner: UIThreadListService,
|
||||
}
|
||||
|
||||
impl ThreadListService {
|
||||
pub(crate) fn new(room: &matrix_sdk::Room) -> Self {
|
||||
Self { inner: room.thread_list_service() }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ThreadListService {
|
||||
/// Returns a snapshot of the current pagination state.
|
||||
pub fn pagination_state(&self) -> UIThreadListPaginationState {
|
||||
self.inner.pagination_state()
|
||||
}
|
||||
|
||||
/// Subscribes to changes in the pagination state.
|
||||
///
|
||||
/// The `listener` is called once for every state transition. The returned
|
||||
/// [`TaskHandle`] keeps the subscription alive
|
||||
pub fn subscribe_to_pagination_state_updates(
|
||||
&self,
|
||||
listener: Box<dyn ThreadListPaginationStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_to_pagination_state_updates();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = subscriber.next().await {
|
||||
listener.on_update(state);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns a snapshot of the current thread list items.
|
||||
pub fn items(&self) -> Vec<ThreadListItem> {
|
||||
self.inner.items().into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Subscribes to changes in the thread list.
|
||||
///
|
||||
/// The `listener` receives an initial `Reset` diff containing all currently
|
||||
/// loaded items, followed by subsequent diffs as the list changes.
|
||||
pub fn subscribe_to_items_updates(
|
||||
&self,
|
||||
listener: Box<dyn ThreadListEntriesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_items_updates();
|
||||
|
||||
// Emit the current snapshot immediately so the caller starts with a
|
||||
// consistent view of the list.
|
||||
listener.on_update(vec![ThreadListUpdate::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());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Fetches the next page of threads and appends the results to the list.
|
||||
///
|
||||
/// This is a no-op when the list is already loading or the end has been
|
||||
/// reached.
|
||||
pub async fn paginate(&self) -> Result<(), ClientError> {
|
||||
self.inner.paginate().await.map_err(|e| match e {
|
||||
UIThreadListServiceError::Sdk(sdk_err) => ClientError::from(sdk_err),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the service back to its initial, empty state.
|
||||
///
|
||||
/// Clears all loaded items, discards the pagination token, and sets the
|
||||
/// state to `Idle { end_reached: false }`. The next call to
|
||||
/// [`Self::paginate`] will restart from the beginning of the thread list.
|
||||
pub async fn reset(&self) {
|
||||
self.inner.reset().await;
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,10 @@ impl UnableToDecryptHook for UtdHook {
|
||||
|
||||
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
|
||||
// not considered UTDs.
|
||||
if let Some(duration) = &info.time_to_decrypt {
|
||||
if *duration < IGNORE_UTD_PERIOD {
|
||||
return;
|
||||
}
|
||||
if let Some(duration) = &info.time_to_decrypt
|
||||
&& *duration < IGNORE_UTD_PERIOD
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Report the UTD to the client.
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use language_tags::LanguageTag;
|
||||
@@ -252,6 +266,7 @@ pub fn get_element_call_required_permissions(
|
||||
requires_client: true,
|
||||
update_delayed_event: true,
|
||||
send_delayed_event: true,
|
||||
download_files: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +332,8 @@ pub struct WidgetCapabilities {
|
||||
pub update_delayed_event: bool,
|
||||
/// This allows the widget to send events with a delay.
|
||||
pub send_delayed_event: bool,
|
||||
/// This allows the widget to download files (avatars)
|
||||
pub download_files: bool,
|
||||
}
|
||||
|
||||
impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
|
||||
@@ -327,6 +344,7 @@ impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
|
||||
requires_client: value.requires_client,
|
||||
update_delayed_event: value.update_delayed_event,
|
||||
send_delayed_event: value.send_delayed_event,
|
||||
download_file: value.download_files,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,6 +357,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
|
||||
requires_client: value.requires_client,
|
||||
update_delayed_event: value.update_delayed_event,
|
||||
send_delayed_event: value.send_delayed_event,
|
||||
download_files: value.download_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,5 +558,8 @@ mod tests {
|
||||
// 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");
|
||||
|
||||
// Download avatars
|
||||
cap_assert("org.matrix.msc4039.download_file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Room keys are now rotated whenever the client fully reloads the member list by making a
|
||||
request to `/members`, which prevents clients using keys that may have been shared under
|
||||
[MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) even if a gappy
|
||||
sync occurs.
|
||||
([#6339](https://github.com/matrix-org/matrix-rust-sdk/pull/6339))
|
||||
|
||||
- Fix invited/knocked rooms disappearing from the room list after
|
||||
join → leave/kick → re-invite when using Sliding Sync. The SDK now always
|
||||
emits a room update so the room is surfaced correctly again.
|
||||
@@ -25,6 +31,11 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Features
|
||||
|
||||
- Add support in the `MemoryStore`'s implementation of `EventCacheStore` for
|
||||
having duplicate events in a room, where each duplicate is in a different
|
||||
`LinkedChunk`. This is useful, e.g., when an event is in a room and a
|
||||
thread in that room.
|
||||
(#[6200](https://github.com/matrix-org/matrix-rust-sdk/pull/6200))
|
||||
- Add `StateStore::upsert_thread_subscriptions()` method for bulk upserts.
|
||||
([#5848](https://github.com/matrix-org/matrix-rust-sdk/pull/5848))
|
||||
- The `LatestEventValue::LocalHasBeenSent` variant gains a new `event_id:
|
||||
@@ -33,9 +44,33 @@ All notable changes to this project will be documented in this file.
|
||||
- [**breaking**] `RelationalLinkedChunk::apply_updates` returns an error rather
|
||||
than panicking. This is necessary in order to ensure certain behaviors are disallowed.
|
||||
([#6061](https://github.com/matrix-org/matrix-rust-sdk/pull/6061))
|
||||
- Add `RoomInfo::active_room_call_consensus_intent()` method to get the call intent for the current call,
|
||||
based on what members are advertising.
|
||||
([#6274](https://github.com/matrix-org/matrix-rust-sdk/pull/6274))
|
||||
- Add `Room::is_call` to check for Call rooms (MSC3417)
|
||||
([#6315](https://github.com/matrix-org/matrix-rust-sdk/pull/6315))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `TtlStoreValue` was moved and renamed to `matrix_sdk_common::ttl_cache::TtlValue`.
|
||||
- [**breaking**] `Gap::prev_token` has been renamed to `Gap::token` since it's now used for both
|
||||
the previous batch token and the next batch token.
|
||||
([#6236](https://github.com/matrix-org/matrix-rust-sdk/pull/6236))
|
||||
- [**breaking**] Invite acceptance details are no longer stored in `RoomInfo`,
|
||||
and the accessors `RoomInfo.invite_acceptance_details()` and
|
||||
`Room::invite_acceptance_details` have been removed. Instead, equivalent
|
||||
details are stored in the Crypto store, and, provided the `e2e-encryption`
|
||||
feature is enabled, are accessible via
|
||||
`BaseClient::get_pending_key_bundle_details_for_room`.
|
||||
([#6199](https://github.com/matrix-org/matrix-rust-sdk/pull/6199))
|
||||
- [**breaking**] `once_cell` is no longer reexported from this crate. The types that were stabilized
|
||||
in the Rust standard library can be used instead in most cases.
|
||||
([#6194](https://github.com/matrix-org/matrix-rust-sdk/pull/6194))
|
||||
- [**breaking**] All the `*StoreLock` structs use a `CrossProcessLockConfig` now instead of the previous `holder` value
|
||||
and so does `StoreConfig` and `BaseClient::clone_with_in_memory_state_store. Passing a
|
||||
`CrossProcessLockConfig::MultiProcess` will keep the same behaviour we had where the client uses the cross process
|
||||
lock and using `CrossProcessLockConfig::SingleProcess` will disable the cross process lock.
|
||||
([#6061](https://github.com/matrix-org/matrix-rust-sdk/pull/6061))
|
||||
- [**breaking**] The `StateStore::upsert_thread_subscription` method has been removed in favor of a
|
||||
bulk method `StateStore::upsert_thread_subscriptions`.
|
||||
- [**breaking**] The `message-ids` feature has been removed. It was already a no-op and has now
|
||||
@@ -48,7 +83,8 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- 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)) (Low, [CVE-2025-66622](https://www.cve.org/CVERecord?id=CVE-2025-66622), [GHSA-jj6p-3m75-g2p3](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-jj6p-3m75-g2p3)).
|
||||
([#5924](https://github.com/matrix-org/matrix-rust-sdk/pull/5924)) (
|
||||
Low, [CVE-2025-66622](https://www.cve.org/CVERecord?id=CVE-2025-66622), [GHSA-jj6p-3m75-g2p3](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-jj6p-3m75-g2p3)).
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -58,8 +94,8 @@ All notable changes to this project will be documented in this file.
|
||||
`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.
|
||||
- `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`.
|
||||
@@ -80,11 +116,13 @@ All notable changes to this project will be documented in this file.
|
||||
### 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)).
|
||||
([#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
|
||||
@@ -107,6 +145,7 @@ All notable changes to this project will be documented in this file.
|
||||
([#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`.
|
||||
@@ -153,6 +192,7 @@ All notable changes to this project will be documented in this file.
|
||||
## [0.13.0] - 2025-07-10
|
||||
|
||||
### Features
|
||||
|
||||
- The `RoomInfo` now remembers when an invite was explicitly accepted when the
|
||||
`BaseClient::room_joined()` method was called. A new getter for this
|
||||
timestamp exists, the `RoomInfo::invite_accepted_at()` method returns this
|
||||
@@ -190,9 +230,9 @@ No notable changes in this release.
|
||||
- [**breaking**] The `MediaRetentionPolicy` can now trigger regular cleanups
|
||||
with its new `cleanup_frequency` setting.
|
||||
([#4603](https://github.com/matrix-org/matrix-rust-sdk/pull/4603))
|
||||
- `Clone` is a supertrait of `EventCacheStoreMedia`.
|
||||
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
|
||||
- There are new `'static` bounds in `MediaService` for the media cache stores
|
||||
- `Clone` is a supertrait of `EventCacheStoreMedia`.
|
||||
- `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
|
||||
@@ -228,17 +268,17 @@ No notable changes in this release.
|
||||
- [**breaking**] `EventCacheStore` allows to control which media content is
|
||||
allowed in the media cache, and how long it should be kept, with a
|
||||
`MediaRetentionPolicy`:
|
||||
- `EventCacheStore::add_media_content()` has an extra argument,
|
||||
`ignore_policy`, which decides whether a media content should ignore the
|
||||
`MediaRetentionPolicy`. It should be stored alongside the media content.
|
||||
- `EventCacheStore` has four new methods: `media_retention_policy()`,
|
||||
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
|
||||
`clean_up_media_cache()`.
|
||||
- `EventCacheStore` implementations should delegate media cache methods to the
|
||||
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
|
||||
They need to implement the `EventCacheStoreMedia` trait that can be tested
|
||||
with the `event_cache_store_media_integration_tests!` macro.
|
||||
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
|
||||
- `EventCacheStore::add_media_content()` has an extra argument,
|
||||
`ignore_policy`, which decides whether a media content should ignore the
|
||||
`MediaRetentionPolicy`. It should be stored alongside the media content.
|
||||
- `EventCacheStore` has four new methods: `media_retention_policy()`,
|
||||
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
|
||||
`clean_up_media_cache()`.
|
||||
- `EventCacheStore` implementations should delegate media cache methods to the
|
||||
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
|
||||
They need to implement the `EventCacheStoreMedia` trait that can be tested
|
||||
with the `event_cache_store_media_integration_tests!` macro.
|
||||
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -278,8 +318,8 @@ No notable changes in this release.
|
||||
|
||||
- Use the `DisplayName` struct to protect against homoglyph attacks.
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- Add `BaseClient::room_key_recipient_strategy` field
|
||||
|
||||
- `AmbiguityCache` contains the room member's user ID.
|
||||
@@ -292,17 +332,17 @@ No notable changes in this release.
|
||||
disambiguation.
|
||||
|
||||
- `Client::cross_process_store_locks_holder_name` is used everywhere:
|
||||
- `StoreConfig::new()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `StoreConfig` no longer implements `Default`.
|
||||
- `BaseClient::new()` has been removed.
|
||||
- `BaseClient::clone_with_in_memory_state_store()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `BaseClient` no longer implements `Default`.
|
||||
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
|
||||
- `BuilderStoreConfig` no longer has
|
||||
`cross_process_store_locks_holder_name` field for `Sqlite` and
|
||||
`IndexedDb`.
|
||||
- `StoreConfig::new()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `StoreConfig` no longer implements `Default`.
|
||||
- `BaseClient::new()` has been removed.
|
||||
- `BaseClient::clone_with_in_memory_state_store()` now takes a
|
||||
`cross_process_store_locks_holder_name` argument.
|
||||
- `BaseClient` no longer implements `Default`.
|
||||
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
|
||||
- `BuilderStoreConfig` no longer has
|
||||
`cross_process_store_locks_holder_name` field for `Sqlite` and
|
||||
`IndexedDb`.
|
||||
|
||||
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
|
||||
|
||||
@@ -312,8 +352,7 @@ No notable changes in this release.
|
||||
by a custom one.
|
||||
|
||||
- Introduce a `DisplayName` struct which normalizes and sanitizes
|
||||
display names.
|
||||
|
||||
display names.
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -353,34 +392,35 @@ display names.
|
||||
other state events in `state` and `stripped_state`.
|
||||
- `StateStore::get_user_ids` takes a `RoomMemberships` to be able to filter the results by any
|
||||
membership state.
|
||||
- `StateStore::get_joined_user_ids` and `StateStore::get_invited_user_ids` are deprecated.
|
||||
- `StateStore::get_joined_user_ids` and `StateStore::get_invited_user_ids` are deprecated.
|
||||
- `Room::members` takes a `RoomMemberships` to be able to filter the results by any membership
|
||||
state.
|
||||
- `Room::active_members` and `Room::joined_members` are deprecated.
|
||||
- `Room::active_members` and `Room::joined_members` are deprecated.
|
||||
- `RoomMember` has new methods:
|
||||
- `can_ban`
|
||||
- `can_invite`
|
||||
- `can_kick`
|
||||
- `can_redact`
|
||||
- `can_send_message`
|
||||
- `can_send_state`
|
||||
- `can_trigger_room_notification`
|
||||
- `can_ban`
|
||||
- `can_invite`
|
||||
- `can_kick`
|
||||
- `can_redact`
|
||||
- `can_send_message`
|
||||
- `can_send_state`
|
||||
- `can_trigger_room_notification`
|
||||
- Move `StateStore::get_member_event` to `StateStoreExt`
|
||||
- `StateStore::get_stripped_room_infos` is deprecated. All room infos should now be returned by
|
||||
`get_room_infos`.
|
||||
- `BaseClient::get_stripped_rooms` is deprecated. Use `get_rooms_filtered` with
|
||||
`RoomStateFilter::INVITED` instead.
|
||||
- Add methods to `StateStore` to be able to retrieve data in batch
|
||||
- `get_state_events_for_keys`
|
||||
- `get_profiles`
|
||||
- `get_presence_events`
|
||||
- `get_users_with_display_names`
|
||||
- `get_state_events_for_keys`
|
||||
- `get_profiles`
|
||||
- `get_presence_events`
|
||||
- `get_users_with_display_names`
|
||||
- Move `Session`, `SessionTokens` and associated methods to the `matrix-sdk` crate.
|
||||
- Add `Room::subscribe_info`
|
||||
|
||||
# 0.5.1
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- #664: Fix regression with push rules being applied to the own user_id only instead of all but the own user_id
|
||||
|
||||
# 0.5.0
|
||||
|
||||
@@ -40,6 +40,12 @@ experimental-encrypted-state-events = [
|
||||
"matrix-sdk-crypto?/experimental-encrypted-state-events"
|
||||
]
|
||||
|
||||
# Enable experimental support for pushing secrets
|
||||
experimental-push-secrets = [
|
||||
"e2e-encryption",
|
||||
"matrix-sdk-crypto?/experimental-push-secrets"
|
||||
]
|
||||
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
@@ -80,7 +86,6 @@ matrix-sdk-common.workspace = true
|
||||
matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption.workspace = true
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell.workspace = true
|
||||
regex.workspace = true
|
||||
ruma = { workspace = true, features = [
|
||||
"unstable-msc2867",
|
||||
@@ -114,6 +119,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen-test.workspace = true
|
||||
getrandom3 = { version = "0.3.4", package = "getrandom", default-features = false, features = ["wasm_js"] }
|
||||
gloo-timers = { workspace = true, features = ["futures"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -24,18 +24,19 @@ use std::{
|
||||
use eyeball::{SharedObservable, Subscriber};
|
||||
use eyeball_im::{Vector, VectorDiff};
|
||||
use futures_util::Stream;
|
||||
use matrix_sdk_common::timer;
|
||||
use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, timer};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{
|
||||
CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
|
||||
TrustRequirement, store::DynCryptoStore, types::requests::ToDeviceRequest,
|
||||
TrustRequirement, store::DynCryptoStore, store::types::RoomPendingKeyBundleDetails,
|
||||
types::requests::ToDeviceRequest,
|
||||
};
|
||||
#[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,
|
||||
OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
events::{
|
||||
StateEvent, StateEventType,
|
||||
@@ -54,7 +55,7 @@ use tracing::{Level, debug, enabled, info, instrument, warn};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
|
||||
RoomStateFilter, SessionMeta,
|
||||
deserialized_responses::DisplayName,
|
||||
error::{Error, Result},
|
||||
event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
|
||||
@@ -79,9 +80,12 @@ use crate::{
|
||||
///
|
||||
/// ```rust
|
||||
/// use matrix_sdk_base::{BaseClient, ThreadingSupport, store::StoreConfig};
|
||||
/// use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
|
||||
///
|
||||
/// let client = BaseClient::new(
|
||||
/// StoreConfig::new("cross-process-holder-name".to_owned()),
|
||||
/// StoreConfig::new(CrossProcessLockConfig::multi_process(
|
||||
/// "cross-process-holder-name".to_owned(),
|
||||
/// )),
|
||||
/// ThreadingSupport::Disabled,
|
||||
/// );
|
||||
/// ```
|
||||
@@ -201,11 +205,10 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub async fn clone_with_in_memory_state_store(
|
||||
&self,
|
||||
cross_process_store_locks_holder_name: &str,
|
||||
cross_process_mode: CrossProcessLockConfig,
|
||||
handle_verification_events: bool,
|
||||
) -> Result<Self> {
|
||||
let config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
|
||||
.state_store(MemoryStore::new());
|
||||
let config = StoreConfig::new(cross_process_mode).state_store(MemoryStore::new());
|
||||
let config = config.crypto_store(self.crypto_store.clone());
|
||||
|
||||
let copy = Self {
|
||||
@@ -238,11 +241,10 @@ impl BaseClient {
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn clone_with_in_memory_state_store(
|
||||
&self,
|
||||
cross_process_store_locks_holder: &str,
|
||||
cross_process_store_config: CrossProcessLockConfig,
|
||||
_handle_verification_events: bool,
|
||||
) -> Result<Self> {
|
||||
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
|
||||
.state_store(MemoryStore::new());
|
||||
let config = StoreConfig::new(cross_process_store_config).state_store(MemoryStore::new());
|
||||
Ok(Self::new(config, ThreadingSupport::Disabled))
|
||||
}
|
||||
|
||||
@@ -397,6 +399,14 @@ impl BaseClient {
|
||||
room_info.mark_as_knocked();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
|
||||
// We are no longer joined to the room, so the invite acceptance details are no
|
||||
// longer relevant.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
if let Some(olm_machine) = self.olm_machine().await.as_ref() {
|
||||
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
|
||||
}
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
self.state_store.save_changes(&changes).await?; // Update the store
|
||||
@@ -425,15 +435,16 @@ impl BaseClient {
|
||||
/// * `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.
|
||||
/// [`RoomPendingKeyBundleDetails`] are stored for this room.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
|
||||
/// # use ruma::{OwnedRoomId, OwnedUserId, RoomId};
|
||||
/// use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
|
||||
/// # async {
|
||||
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
|
||||
/// # let client = BaseClient::new(StoreConfig::new(CrossProcessLockConfig::multi_process("example")), 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!();
|
||||
@@ -457,30 +468,35 @@ impl BaseClient {
|
||||
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 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
|
||||
// based on this info. If a user has accepted an invite and we receive a room
|
||||
// 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 previous_state == RoomState::Invited
|
||||
&& let Some(inviter) = inviter
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
{
|
||||
let details = InviteAcceptanceDetails {
|
||||
invite_accepted_at: MilliSecondsSinceUnixEpoch::now(),
|
||||
inviter,
|
||||
};
|
||||
room_info.set_invite_acceptance_details(details);
|
||||
// If our previous state was an invite and we're now in the joined state, this
|
||||
// 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
|
||||
// based on this info. If a user has accepted an invite and we receive a room
|
||||
// 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.
|
||||
let previous_state = room.state();
|
||||
if previous_state == RoomState::Invited
|
||||
&& let Some(inviter) = inviter
|
||||
&& let Some(olm_machine) = self.olm_machine().await.as_ref()
|
||||
{
|
||||
olm_machine.store().store_room_pending_key_bundle(room_id, &inviter).await?
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
{
|
||||
// suppress unused argument warning
|
||||
let _ = inviter;
|
||||
}
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
@@ -507,6 +523,14 @@ impl BaseClient {
|
||||
room_info.mark_as_left();
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.mark_members_missing(); // the own member event changed
|
||||
|
||||
// We are no longer joined to the room, so the invite acceptance details are no
|
||||
// longer relevant.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
if let Some(olm_machine) = self.olm_machine().await.as_ref() {
|
||||
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
|
||||
}
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(room_info.clone());
|
||||
self.state_store.save_changes(&changes).await?; // Update the store
|
||||
@@ -625,6 +649,13 @@ impl BaseClient {
|
||||
let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
|
||||
BTreeMap::new();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let e2ee_context = processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
);
|
||||
|
||||
for (room_id, joined_room) in response.rooms.join {
|
||||
let joined_room_update = processors::room::sync_v2::update_joined_room(
|
||||
&mut context,
|
||||
@@ -641,11 +672,7 @@ impl BaseClient {
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
&e2ee_context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -667,11 +694,7 @@ impl BaseClient {
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
&e2ee_context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -689,6 +712,8 @@ impl BaseClient {
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
&e2ee_context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -706,6 +731,8 @@ impl BaseClient {
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
&e2ee_context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -893,6 +920,18 @@ impl BaseClient {
|
||||
|
||||
let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
if let Some(olm) = self.olm_machine().await.as_ref() {
|
||||
// With the introduction of MSC4268, it is no longer sufficient to check for
|
||||
// changes to session recipients when we send a message, since we may miss
|
||||
// join/leave pairs in our view of the room state. Instead, we should rotate
|
||||
// the room key whenever we fully reload the member list as a precaution.
|
||||
tracing::debug!("Rotating room key due to full member list reload");
|
||||
if let Err(e) = olm.discard_room_key(room_id).await {
|
||||
tracing::warn!("Error discarding room key: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -970,11 +1009,13 @@ impl BaseClient {
|
||||
|
||||
let members = self.state_store.get_user_ids(room_id, filter).await?;
|
||||
|
||||
let settings = EncryptionSettings::new(
|
||||
let Some(settings) = EncryptionSettings::from_possibly_redacted(
|
||||
room_encryption_event,
|
||||
history_visibility,
|
||||
self.room_key_recipient_strategy.clone(),
|
||||
);
|
||||
) else {
|
||||
return Err(Error::EncryptionNotEnabled);
|
||||
};
|
||||
|
||||
Ok(o.share_room_key(room_id, members.iter().map(Deref::deref), settings).await?)
|
||||
}
|
||||
@@ -1084,6 +1125,24 @@ impl BaseClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the record of whether we are waiting for an [MSC4268] key bundle
|
||||
/// for the given room.
|
||||
///
|
||||
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub async fn get_pending_key_bundle_details_for_room(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<RoomPendingKeyBundleDetails>> {
|
||||
let result = match self.olm_machine().await.as_ref() {
|
||||
Some(machine) => {
|
||||
machine.store().get_pending_key_bundle_details_for_room(room_id).await?
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent the `required_state` values sent by a sync request.
|
||||
@@ -1147,11 +1206,14 @@ impl From<&v5::Request> for RequestedRequiredStates {
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use assert_matches2::{assert_let, assert_matches};
|
||||
use assert_matches2::assert_let;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches2::assert_matches;
|
||||
use futures_util::FutureExt as _;
|
||||
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
|
||||
use matrix_sdk_test::{
|
||||
BOB, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent,
|
||||
SyncResponseBuilder, async_test, event_factory::EventFactory, ruma_response_from_json,
|
||||
BOB, InvitedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, async_test,
|
||||
event_factory::EventFactory, ruma_response_from_json,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{self as api, sync::sync_events::v5},
|
||||
@@ -1331,6 +1393,7 @@ mod tests {
|
||||
let room_id = room_id!("!test:example.org");
|
||||
|
||||
let client = logged_in_base_client(Some(user_id)).await;
|
||||
let f = EventFactory::new();
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
|
||||
@@ -1349,19 +1412,14 @@ mod tests {
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
|
||||
let response = sync_builder
|
||||
.add_invited_room(InvitedRoomBuilder::new(room_id).add_state_event(
|
||||
StrippedStateTestEvent::Custom(json!({
|
||||
"content": {
|
||||
"displayname": "Alice",
|
||||
"membership": "invite",
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653u64,
|
||||
"sender": "@example:example.org",
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
})),
|
||||
))
|
||||
.add_invited_room(
|
||||
InvitedRoomBuilder::new(room_id).add_state_event(
|
||||
f.member(user_id)
|
||||
.sender(user_id!("@example:example.org"))
|
||||
.membership(MembershipState::Invite)
|
||||
.display_name("Alice"),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
|
||||
@@ -1461,7 +1519,7 @@ mod tests {
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
@@ -1523,7 +1581,7 @@ mod tests {
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
@@ -1587,7 +1645,7 @@ mod tests {
|
||||
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
|
||||
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
client
|
||||
@@ -1601,23 +1659,13 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Preamble: let the SDK know about the room, and that the invited user left it.
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id).add_state_event(
|
||||
StateTestEvent::Custom(json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": null,
|
||||
"membership": "leave"
|
||||
},
|
||||
"event_id": "$151803140217rkvjc:localhost",
|
||||
"origin_server_ts": 151800139,
|
||||
"room_id": room_id,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
})),
|
||||
))
|
||||
.add_joined_room(
|
||||
matrix_sdk_test::JoinedRoomBuilder::new(room_id)
|
||||
.add_state_event(f.member(user_id).leave()),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
@@ -1661,7 +1709,7 @@ mod tests {
|
||||
async fn test_ignored_user_list_changes() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
@@ -1721,37 +1769,43 @@ mod tests {
|
||||
assert!(client.is_user_ignored(ignored_user_id).await);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
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 known_room_id = room_id!("!invited:localhost");
|
||||
let unknown_room_id = room_id!("!unknown:localhost");
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
let response = sync_builder
|
||||
.add_invited_room(InvitedRoomBuilder::new(invited_room_id))
|
||||
.add_invited_room(InvitedRoomBuilder::new(known_room_id))
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
// Let us first check the initial state, we should have a room in the invite
|
||||
// state.
|
||||
let invited_room = client
|
||||
.get_room(invited_room_id)
|
||||
.get_room(known_room_id)
|
||||
.expect("The sync should have created a room in the invited state");
|
||||
|
||||
assert_eq!(invited_room.state(), RoomState::Invited);
|
||||
assert!(invited_room.invite_acceptance_details().is_none());
|
||||
assert!(
|
||||
client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
|
||||
);
|
||||
|
||||
// Now we join the room.
|
||||
let joined_room = client
|
||||
.room_joined(invited_room_id, Some(user_id.to_owned()))
|
||||
.room_joined(known_room_id, Some(user_id.to_owned()))
|
||||
.await
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
// Yup, we now have some invite details.
|
||||
assert_eq!(joined_room.state(), RoomState::Joined);
|
||||
assert_matches!(joined_room.invite_acceptance_details(), Some(details));
|
||||
assert_matches!(
|
||||
client.get_pending_key_bundle_details_for_room(known_room_id).await,
|
||||
Ok(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
|
||||
@@ -1763,19 +1817,27 @@ mod tests {
|
||||
.expect("We should be able to mark a room as joined");
|
||||
|
||||
assert_eq!(unknown_room.state(), RoomState::Joined);
|
||||
assert!(unknown_room.invite_acceptance_details().is_none());
|
||||
assert!(
|
||||
client
|
||||
.get_pending_key_bundle_details_for_room(unknown_room_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
sync_builder.clear();
|
||||
let response =
|
||||
sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
|
||||
sync_builder.add_left_room(LeftRoomBuilder::new(known_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)
|
||||
.get_room(known_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());
|
||||
assert!(
|
||||
client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,9 @@
|
||||
|
||||
//! SDK-specific variations of response types from Ruma.
|
||||
|
||||
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
|
||||
use std::{collections::BTreeMap, fmt, hash::Hash, iter, sync::LazyLock};
|
||||
|
||||
pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, UInt,
|
||||
@@ -72,15 +71,15 @@ pub struct AmbiguityChanges {
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
static MXID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(DisplayName::MXID_PATTERN)
|
||||
.expect("We should be able to create a regex from our static MXID pattern")
|
||||
});
|
||||
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
static LEFT_TO_RIGHT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
|
||||
.expect("We should be able to create a regex from our static left-to-right pattern")
|
||||
});
|
||||
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
static HIDDEN_CHARACTERS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
|
||||
.expect("We should be able to create a regex from our static hidden characters pattern")
|
||||
});
|
||||
@@ -89,7 +88,7 @@ static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
///
|
||||
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
|
||||
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
|
||||
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
static I_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
|
||||
});
|
||||
|
||||
@@ -97,7 +96,7 @@ static I_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
///
|
||||
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
|
||||
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
|
||||
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
static ZERO_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
|
||||
});
|
||||
|
||||
@@ -105,7 +104,7 @@ static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
///
|
||||
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
|
||||
/// ambiguous.
|
||||
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
static DOT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ pub type Event = TimelineEvent;
|
||||
/// The kind of gap the event storage holds.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Gap {
|
||||
/// The token to use in the query, extracted from a previous "from" /
|
||||
/// "end" field of a `/messages` response.
|
||||
pub prev_token: String,
|
||||
/// The token to use in the pagination query as the `from` parameter,
|
||||
/// extracted from a previous `start` / `end` field of a `/messages`
|
||||
/// response, or from the `prev_batch` / `next_batch` field of a `/sync`
|
||||
/// response.
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// already exist in the store.
|
||||
async fn test_linked_chunk_exists_before_referenced(&self);
|
||||
|
||||
/// Test that the same event can exist in a room's linked chunk and a
|
||||
/// thread's linked chunk simultaneously.
|
||||
async fn test_linked_chunk_allows_same_event_in_room_and_thread(&self);
|
||||
|
||||
/// Test loading the last chunk in a linked chunk from the store.
|
||||
async fn test_load_last_chunk(&self);
|
||||
|
||||
@@ -202,18 +206,34 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test that an event can be found or not.
|
||||
async fn test_find_event(&self);
|
||||
|
||||
/// Test that an event can be found when it exists in both a room and a
|
||||
/// thread in that room.
|
||||
async fn test_find_event_when_event_in_room_and_thread(&self);
|
||||
|
||||
/// Test that finding event relations works as expected.
|
||||
async fn test_find_event_relations(&self);
|
||||
|
||||
/// Test that find event relations works as expected when an event is both a
|
||||
/// room and a thread in that room.
|
||||
async fn test_find_event_relations_when_event_in_room_and_thread(&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 getting all events in a room works as expected when the event
|
||||
/// is in both a room and thread in that room.
|
||||
async fn test_get_room_events_with_event_in_room_and_thread(&self);
|
||||
|
||||
/// Test that saving an event works as expected.
|
||||
async fn test_save_event(&self);
|
||||
|
||||
/// Test that saving an existing event updates it's contents in both room
|
||||
/// and thread linked chunks.
|
||||
async fn test_save_event_updates_event_in_room_and_thread(&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);
|
||||
@@ -242,7 +262,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "parmesan".to_owned() },
|
||||
gap: Gap { token: "parmesan".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -283,7 +303,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(second.identifier(), CId::new(1));
|
||||
|
||||
assert_matches!(second.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "parmesan");
|
||||
assert_eq!(gap.token, "parmesan");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,7 +355,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(41)),
|
||||
new: CId::new(42),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "gap".to_owned() },
|
||||
gap: Gap { token: "gap".to_owned() },
|
||||
}],
|
||||
)
|
||||
.await
|
||||
@@ -348,13 +368,69 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: None,
|
||||
new: CId::new(42),
|
||||
next: Some(CId::new(43)),
|
||||
gap: Gap { prev_token: "gap".to_owned() },
|
||||
gap: Gap { token: "gap".to_owned() },
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_allows_same_event_in_room_and_thread(&self) {
|
||||
// This test verifies that the same event can appear in both a room's linked
|
||||
// chunk and a thread's linked chunk. This is the real-world use case:
|
||||
// a thread reply appears in both the main room timeline and the thread.
|
||||
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let thread_root = event_id!("$thread_root");
|
||||
|
||||
// Create an event that will be inserted into both the room and thread linked
|
||||
// chunks.
|
||||
let event_id = event_id!("$thread_reply");
|
||||
let event = make_test_event_with_event_id(room_id, "thread reply", Some(event_id));
|
||||
|
||||
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
|
||||
|
||||
// Insert the event into the room's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event.clone()] },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert the same event into the thread's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
thread_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event] },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify both entries exist by loading chunks from both linked chunk IDs.
|
||||
let room_chunks = self.load_all_chunks(room_linked_chunk_id).await.unwrap();
|
||||
let thread_chunks = self.load_all_chunks(thread_linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(room_chunks.len(), 1);
|
||||
assert_eq!(thread_chunks.len(), 1);
|
||||
|
||||
// Verify the event is in both.
|
||||
assert_matches!(&room_chunks[0].content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
|
||||
});
|
||||
assert_matches!(&thread_chunks[0].content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_load_all_chunks_metadata(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
@@ -377,7 +453,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "parmesan".to_owned() },
|
||||
gap: Gap { token: "parmesan".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -626,7 +702,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "morbier".to_owned() },
|
||||
gap: Gap { token: "morbier".to_owned() },
|
||||
},
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -704,7 +780,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(chunk.lazy_previous(), Some(CId::new(0)));
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
assert_eq!(gap.token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -742,7 +818,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
assert_eq!(gap.token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -793,7 +869,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
assert_eq!(gap.token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -825,19 +901,19 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: None,
|
||||
new: CId::new(42),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "raclette".to_owned() },
|
||||
gap: Gap { token: "raclette".to_owned() },
|
||||
},
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(43),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "fondue".to_owned() },
|
||||
gap: Gap { token: "fondue".to_owned() },
|
||||
},
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(43)),
|
||||
new: CId::new(44),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "tartiflette".to_owned() },
|
||||
gap: Gap { token: "tartiflette".to_owned() },
|
||||
},
|
||||
Update::RemoveChunk(CId::new(43)),
|
||||
],
|
||||
@@ -855,7 +931,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(c.previous, None);
|
||||
assert_eq!(c.next, Some(CId::new(44)));
|
||||
assert_matches!(c.content, ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "raclette");
|
||||
assert_eq!(gap.token, "raclette");
|
||||
});
|
||||
|
||||
let c = chunks.remove(0);
|
||||
@@ -863,7 +939,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(c.previous, Some(CId::new(42)));
|
||||
assert_eq!(c.next, None);
|
||||
assert_matches!(c.content, ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "tartiflette");
|
||||
assert_eq!(gap.token, "tartiflette");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1043,7 +1119,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(54),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "fondue".to_owned() },
|
||||
gap: Gap { token: "fondue".to_owned() },
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
@@ -1074,7 +1150,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(54),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "fondue".to_owned() },
|
||||
gap: Gap { token: "fondue".to_owned() },
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
@@ -1203,7 +1279,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
|
||||
gap: Gap { token: "bleu d'auvergne".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -1330,7 +1406,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "brillat-savarin".to_owned() },
|
||||
gap: Gap { token: "brillat-savarin".to_owned() },
|
||||
},
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
Update::PushItems {
|
||||
@@ -1361,12 +1437,12 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
self.filter_duplicated_events(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
event_raclette.event_id().unwrap().to_owned(),
|
||||
event_morbier.event_id().unwrap().to_owned(),
|
||||
event_gruyere.event_id().unwrap().to_owned(),
|
||||
event_tome.event_id().unwrap().to_owned(),
|
||||
event_mont_dor.event_id().unwrap().to_owned(),
|
||||
event_comte.event_id().unwrap(),
|
||||
event_raclette.event_id().unwrap(),
|
||||
event_morbier.event_id().unwrap(),
|
||||
event_gruyere.event_id().unwrap(),
|
||||
event_tome.event_id().unwrap(),
|
||||
event_mont_dor.event_id().unwrap(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -1460,6 +1536,75 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_find_event_when_event_in_room_and_thread(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let thread_root = event_id!("$thread_root");
|
||||
|
||||
// Create an event that will be only be inserted into the room
|
||||
let room_event_id = event_id!("$room_event");
|
||||
let room_event = make_test_event_with_event_id(room_id, "room event", Some(room_event_id));
|
||||
|
||||
// Create an event that will only be inserted into the thread
|
||||
let thread_event_id = event_id!("$thread_event");
|
||||
let thread_event =
|
||||
make_test_event_with_event_id(room_id, "thread event", Some(thread_event_id));
|
||||
|
||||
// Create an event that will be inserted into both the room and thread linked
|
||||
// chunks.
|
||||
let room_and_thread_event_id = event_id!("$room_and_thread");
|
||||
let room_and_thread_event = make_test_event_with_event_id(
|
||||
room_id,
|
||||
"room and thread",
|
||||
Some(room_and_thread_event_id),
|
||||
);
|
||||
|
||||
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
|
||||
|
||||
// Insert the relevant events into the room's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(1), 0),
|
||||
items: vec![room_event, room_and_thread_event.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert the relevant events into the thread's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
thread_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(1), 0),
|
||||
items: vec![thread_event, room_and_thread_event],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that event that is only in the room can be retrieved
|
||||
assert_matches!(self.find_event(room_id, room_event_id).await, Ok(Some(event)) => {
|
||||
assert_eq!(event.event_id().unwrap(), room_event_id)
|
||||
});
|
||||
|
||||
// Verify that the event that is only in the thread can be retrieved
|
||||
assert_matches!(self.find_event(room_id, thread_event_id).await, Ok(Some(event)) => {
|
||||
assert_eq!(event.event_id().unwrap(), thread_event_id)
|
||||
});
|
||||
|
||||
// Verify that event that is in both room and thread can be retrieved
|
||||
assert_matches!(self.find_event(room_id, room_and_thread_event_id).await, Ok(Some(event)) => {
|
||||
assert_eq!(event.event_id().unwrap(), room_and_thread_event_id);
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_find_event_relations(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
@@ -1574,6 +1719,121 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_find_event_relations_when_event_in_room_and_thread(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let thread_root = event_id!("$thread_root");
|
||||
|
||||
// Create an event that will inserted into both the room and thread linked
|
||||
// chunks.
|
||||
let event_id = event_id!("$event");
|
||||
let event = make_test_event_with_event_id(room_id, "event", Some(event_id));
|
||||
|
||||
// Create an event that will only be inserted into the thread in order to help
|
||||
// distinguish between the room and thread linked chunks.
|
||||
let extra_thread_event_id = event_id!("$extra_thread_event");
|
||||
let extra_thread_event = make_test_event_with_event_id(
|
||||
room_id,
|
||||
"extra thread event",
|
||||
Some(extra_thread_event_id),
|
||||
);
|
||||
|
||||
// Create a reaction that will only be inserted into the room
|
||||
let room_reaction_id = event_id!("$room_reaction");
|
||||
let room_reaction = EventFactory::new()
|
||||
.room(room_id)
|
||||
.sender(*ALICE)
|
||||
.reaction(event_id, "room")
|
||||
.event_id(room_reaction_id)
|
||||
.into_event();
|
||||
|
||||
// Create a reaction that will only be inserted into the thread
|
||||
let thread_reaction_id = event_id!("$thread_reaction");
|
||||
let thread_reaction = EventFactory::new()
|
||||
.room(room_id)
|
||||
.sender(*ALICE)
|
||||
.reaction(event_id, "thread")
|
||||
.event_id(thread_reaction_id)
|
||||
.into_event();
|
||||
|
||||
// Create a reaction that will be inserted into both the room and thread linked
|
||||
// chunks.
|
||||
let room_and_thread_reaction_id = event_id!("$room_and_thread_reaction");
|
||||
let room_and_thread_reaction = EventFactory::new()
|
||||
.room(room_id)
|
||||
.sender(*ALICE)
|
||||
.reaction(event_id, "room and thread")
|
||||
.event_id(room_and_thread_reaction_id)
|
||||
.into_event();
|
||||
|
||||
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
|
||||
|
||||
// Insert the relevant events into the room's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(1), 0),
|
||||
items: vec![event.clone(), room_reaction, room_and_thread_reaction.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert the relevant events into the thread's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
thread_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(1), 0),
|
||||
items: vec![
|
||||
event.clone(),
|
||||
extra_thread_event,
|
||||
thread_reaction,
|
||||
room_and_thread_reaction,
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that only related events from the room are returned
|
||||
assert_matches!(self.find_event_relations(room_id, event_id, None).await, Ok(relations) => {
|
||||
assert_eq!(relations.len(), 3);
|
||||
// Verify that room reaction is in the list and associated with its
|
||||
// position in the room linked chunk.
|
||||
let room_relation = relations
|
||||
.iter()
|
||||
.find(|relation| relation.0.event_id().unwrap() == room_reaction_id)
|
||||
.unwrap();
|
||||
assert_matches!(room_relation, (_, Some(position)) => {
|
||||
assert_eq!(*position, Position::new(CId::new(1), 1));
|
||||
});
|
||||
|
||||
// Verify that thread reaction is in the list and not associated with a
|
||||
// position, as all positions are provided for the room linked chunk.
|
||||
let thread_relation = relations
|
||||
.iter()
|
||||
.find(|relation| relation.0.event_id().unwrap() == thread_reaction_id)
|
||||
.unwrap();
|
||||
assert_matches!(thread_relation, (_, None));
|
||||
|
||||
// Verify that room and thread reaction is in the list and associated
|
||||
// with its position in the room linked chunk, not the thread linked chunk.
|
||||
let room_and_thread_relation = relations
|
||||
.iter()
|
||||
.find(|relation| relation.0.event_id().unwrap() == room_and_thread_reaction_id)
|
||||
.unwrap();
|
||||
assert_matches!(room_and_thread_relation, (_, Some(position)) => {
|
||||
assert_eq!(*position, Position::new(CId::new(1), 2));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_get_room_events(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
@@ -1702,6 +1962,73 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_expected_events!(events, [first_event]);
|
||||
}
|
||||
|
||||
async fn test_get_room_events_with_event_in_room_and_thread(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let thread_root = event_id!("$thread_root");
|
||||
|
||||
// Create an event that will be only be inserted into the room
|
||||
let room_event_id = event_id!("$room_event");
|
||||
let room_event = make_test_event_with_event_id(room_id, "room event", Some(room_event_id));
|
||||
|
||||
// Create an event that will only be inserted into the thread. This may not be a
|
||||
// sensible operation in practice, as threads seem to always exist in a
|
||||
// room, but let's test it anyway.
|
||||
let thread_event_id = event_id!("$thread_event");
|
||||
let thread_event =
|
||||
make_test_event_with_event_id(room_id, "thread event", Some(thread_event_id));
|
||||
|
||||
// Create an event that will be inserted into both the room and thread linked
|
||||
// chunks.
|
||||
let room_and_thread_event_id = event_id!("$room_and_thread");
|
||||
let room_and_thread_event = make_test_event_with_event_id(
|
||||
room_id,
|
||||
"room and thread",
|
||||
Some(room_and_thread_event_id),
|
||||
);
|
||||
|
||||
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
|
||||
|
||||
// Insert the relevant events into the room's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(1), 0),
|
||||
items: vec![room_event, room_and_thread_event.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert the relevant events into the thread's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
thread_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(1), 0),
|
||||
items: vec![thread_event, room_and_thread_event],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that all events can be retrieved and none are duplicated in the
|
||||
// returned list.
|
||||
let expected_event_ids =
|
||||
BTreeSet::from([room_event_id, thread_event_id, room_and_thread_event_id]);
|
||||
assert_matches!(self.get_room_events(room_id, None, None).await, Ok(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
assert!(events.iter().all(|event| {
|
||||
expected_event_ids.contains(&event.event_id().unwrap().as_ref())
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_save_event(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
@@ -1746,6 +2073,66 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_save_event_updates_event_in_room_and_thread(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let thread_root = event_id!("$thread_root");
|
||||
|
||||
// Create an event that will be inserted into both the room and thread linked
|
||||
// chunks.
|
||||
let event_id = event_id!("$event");
|
||||
let event = make_test_event_with_event_id(room_id, "event", Some(event_id));
|
||||
|
||||
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
|
||||
|
||||
// Insert the relevant events into the room's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event.clone()] },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert the relevant events into the thread's linked chunk.
|
||||
self.handle_linked_chunk_updates(
|
||||
thread_linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event.clone()] },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Save updated version of original event, which should replace the content of
|
||||
// the existing event
|
||||
let updated_content = "updated content";
|
||||
let updated = make_test_event_with_event_id(room_id, updated_content, Some(event_id));
|
||||
self.save_event(room_id, updated).await.unwrap();
|
||||
|
||||
// Load all chunks from both room and thread
|
||||
let room_chunks = self.load_all_chunks(room_linked_chunk_id).await.unwrap();
|
||||
let thread_chunks = self.load_all_chunks(thread_linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(room_chunks.len(), 1);
|
||||
assert_eq!(thread_chunks.len(), 1);
|
||||
|
||||
// Verify the event has been updated in both room and thread
|
||||
assert_matches!(&room_chunks[0].content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
|
||||
check_test_event(&events[0], updated_content);
|
||||
});
|
||||
assert_matches!(&thread_chunks[0].content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
|
||||
check_test_event(&events[0], updated_content);
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_thread_vs_room_linked_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
@@ -1826,10 +2213,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
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(),
|
||||
],
|
||||
vec![thread1_ev.event_id().unwrap(), room_ev.event_id().unwrap()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1933,6 +2317,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_linked_chunk_exists_before_referenced().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_allow_same_event_in_room_and_thread() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_allows_same_event_in_room_and_thread().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_last_chunk() {
|
||||
let event_cache_store =
|
||||
@@ -2066,6 +2457,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_find_event().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event_when_event_in_room_and_thread() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_find_event_when_event_in_room_and_thread().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event_relations() {
|
||||
let event_cache_store =
|
||||
@@ -2073,6 +2471,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_find_event_relations().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event_relations_when_event_in_room_and_thread() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_find_event_relations_when_event_in_room_and_thread().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room_events() {
|
||||
let event_cache_store =
|
||||
@@ -2087,6 +2492,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_get_room_events_filtered().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room_events_with_event_in_room_and_thread() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_get_room_events_with_event_in_room_and_thread().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_save_event() {
|
||||
let event_cache_store =
|
||||
@@ -2094,6 +2506,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_save_event().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_save_event_updates_event_in_room_and_thread() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_save_event_updates_event_in_room_and_thread().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_thread_vs_room_linked_chunk() {
|
||||
let event_cache_store =
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
@@ -97,7 +97,7 @@ impl EventCacheStore for MemoryStore {
|
||||
inner
|
||||
.events
|
||||
.apply_updates(linked_chunk_id, updates)
|
||||
.map_err(|e| Self::Error::Backend(Box::new(e)))?;
|
||||
.map_err(|e| Self::Error::Backend(Arc::new(e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -188,10 +188,9 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
|
||||
let event = inner.events.items(room_id).find_map(|(_, (event, _pos))| {
|
||||
(event.event_id()? == event_id).then_some(event.clone())
|
||||
});
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
@@ -204,10 +203,10 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let related_events = inner
|
||||
let related_events: Vec<_> = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.filter_map(|(event, pos)| {
|
||||
.filter_map(|(linked_chunk_id, (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());
|
||||
@@ -219,14 +218,35 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
// Must not be filtered out.
|
||||
if let Some(filters) = &filters {
|
||||
filters.contains(&rel_type).then_some((event.clone(), pos))
|
||||
filters.contains(&rel_type).then_some((linked_chunk_id, (event.clone(), pos)))
|
||||
} else {
|
||||
Some((event.clone(), pos))
|
||||
Some((linked_chunk_id, (event.clone(), pos)))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(related_events)
|
||||
// Remove any duplicate events which may exist in both a room and thread
|
||||
// linked chunk. Additionally, remove any position information from non-room
|
||||
// linked chunks.
|
||||
let mut deduplicated = HashMap::new();
|
||||
for (linked_chunk_id, (event, position)) in related_events {
|
||||
let event_id = event
|
||||
.event_id()
|
||||
.ok_or(Self::Error::InvalidData { details: String::from("missing event id") })?;
|
||||
match linked_chunk_id.as_ref() {
|
||||
LinkedChunkId::Room(_) => {
|
||||
// Prioritize events that come from a room linked chunk
|
||||
deduplicated.insert(event_id, (event, position));
|
||||
}
|
||||
_ => {
|
||||
// Remove position information from events that come
|
||||
// from any other type of linked chunk
|
||||
deduplicated.entry(event_id).or_insert_with(|| (event, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(deduplicated.into_values().collect())
|
||||
}
|
||||
|
||||
async fn get_room_events(
|
||||
@@ -237,17 +257,29 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event: Vec<_> = inner
|
||||
let (_, event): (_, Vec<_>) = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.map(|(event, _pos)| event.clone())
|
||||
.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();
|
||||
|
||||
.map(|e| {
|
||||
e.event_id()
|
||||
.map(|id| (id, e))
|
||||
.ok_or(Self::Error::InvalidData { details: String::from("missing event id") })
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.fold((HashSet::new(), Vec::new()), |(mut ids, mut es), (id, e)| {
|
||||
if !ids.contains(&id) {
|
||||
ids.insert(id);
|
||||
es.push(e);
|
||||
}
|
||||
(ids, es)
|
||||
});
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ mod memory_store;
|
||||
mod traits;
|
||||
|
||||
use matrix_sdk_common::cross_process_lock::{
|
||||
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
|
||||
MappedCrossProcessLockState, TryLock,
|
||||
CrossProcessLock, CrossProcessLockConfig, CrossProcessLockError, CrossProcessLockGeneration,
|
||||
CrossProcessLockGuard, MappedCrossProcessLockState, TryLock,
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
use ruma::{OwnedEventId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
@@ -64,32 +64,27 @@ impl fmt::Debug for EventCacheStoreLock {
|
||||
impl EventCacheStoreLock {
|
||||
/// Create a new lock around the [`EventCacheStore`].
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
/// The `cross_process_lock_config` argument controls whether we need to
|
||||
/// hold the cross process lock or not.
|
||||
pub fn new<S>(store: S, cross_process_lock_config: CrossProcessLockConfig) -> Self
|
||||
where
|
||||
S: IntoEventCacheStore,
|
||||
{
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: Arc::new(CrossProcessLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
)),
|
||||
store,
|
||||
}
|
||||
let cross_process_lock = Arc::new(CrossProcessLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
cross_process_lock_config,
|
||||
));
|
||||
Self { cross_process_lock, store }
|
||||
}
|
||||
|
||||
/// 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(lock_state)
|
||||
Ok(self.cross_process_lock.spin_lock(None).await??.map(|cross_process_lock_guard| {
|
||||
EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.clone() }
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,11 +137,11 @@ impl Deref for EventCacheStoreLockGuard {
|
||||
}
|
||||
|
||||
/// Event cache store specific error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub enum EventCacheStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
Backend(Arc<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// The store is locked with a passphrase and an incorrect passphrase
|
||||
/// was given.
|
||||
@@ -159,7 +154,7 @@ pub enum EventCacheStoreError {
|
||||
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
|
||||
Encryption(#[from] StoreEncryptionError),
|
||||
Encryption(#[from] Arc<StoreEncryptionError>),
|
||||
|
||||
/// The store failed to encode or decode some data.
|
||||
#[error("Error encoding or decoding data from the event cache store: {0}")]
|
||||
@@ -167,7 +162,7 @@ pub enum EventCacheStoreError {
|
||||
|
||||
/// The store failed to serialize or deserialize some data.
|
||||
#[error("Error serializing or deserializing data from the event cache store: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
Serialization(#[from] Arc<serde_json::Error>),
|
||||
|
||||
/// The database format has changed in a backwards incompatible way.
|
||||
#[error(
|
||||
@@ -193,13 +188,13 @@ impl EventCacheStoreError {
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
Self::Backend(Arc::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheStoreError> for CrossProcessLockError {
|
||||
fn from(value: EventCacheStoreError) -> Self {
|
||||
Self::TryLock(Box::new(value))
|
||||
Self::TryLock(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ pub mod event_cache;
|
||||
pub mod latest_event;
|
||||
pub mod media;
|
||||
pub mod notification_settings;
|
||||
pub mod read_receipts;
|
||||
mod response_processors;
|
||||
mod room;
|
||||
|
||||
pub mod read_receipts;
|
||||
pub mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
@@ -56,19 +56,18 @@ pub use client::{BaseClient, ThreadingSupport};
|
||||
pub use http;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use room::{
|
||||
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
|
||||
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp,
|
||||
RoomState, RoomStateFilter, SuccessorRoom, apply_redaction,
|
||||
CallIntentConsensus, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
|
||||
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp, RoomState, RoomStateFilter,
|
||||
SuccessorRoom, apply_redaction,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
|
||||
QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
|
||||
ThreadSubscriptionCatchupToken,
|
||||
};
|
||||
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawSyncStateEventWithKeys};
|
||||
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawStateEventWithKeys};
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test_utils::init_tracing_for_tests!();
|
||||
|
||||
@@ -261,7 +261,7 @@ mod tests {
|
||||
},
|
||||
"iv": "AK1wyzigZtQAAAABAAAAKK",
|
||||
"hashes": {
|
||||
"sha256": "foobar",
|
||||
"sha256": "/NogKqW5bz/m8xHgFiH5haFGjCNVmUIPLzfvOhHdrxY",
|
||||
},
|
||||
"v": "v2",
|
||||
}))
|
||||
|
||||
@@ -32,8 +32,8 @@ use std::fmt;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::cross_process_lock::{
|
||||
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
|
||||
CrossProcessLockState, TryLock,
|
||||
CrossProcessLock, CrossProcessLockConfig, CrossProcessLockError, CrossProcessLockGeneration,
|
||||
CrossProcessLockGuard, CrossProcessLockState, TryLock,
|
||||
};
|
||||
use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner};
|
||||
@@ -84,7 +84,7 @@ impl MediaStoreError {
|
||||
|
||||
impl From<MediaStoreError> for CrossProcessLockError {
|
||||
fn from(value: MediaStoreError) -> Self {
|
||||
Self::TryLock(Box::new(value))
|
||||
Self::TryLock(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,22 +113,20 @@ impl fmt::Debug for MediaStoreLock {
|
||||
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
|
||||
/// The `cross_process_lock_config` argument controls whether we need to
|
||||
/// hold the cross process lock or not.
|
||||
pub fn new<S>(store: S, cross_process_lock_config: CrossProcessLockConfig) -> 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,
|
||||
}
|
||||
let cross_process_lock = Arc::new(CrossProcessLock::new(
|
||||
LockableMediaStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
cross_process_lock_config,
|
||||
));
|
||||
Self { cross_process_lock, store }
|
||||
}
|
||||
|
||||
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
|
||||
@@ -138,9 +136,10 @@ impl MediaStoreLock {
|
||||
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.
|
||||
// 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();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user