Compare commits
885 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 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 | |||
| f1e41222f8 | |||
| df6fc21576 | |||
| 72c1a655f2 | |||
| ae36751c9d | |||
| b1c4e45657 | |||
| f0ee76c27f | |||
| c1c48c2309 | |||
| b2fb7ad4e9 | |||
| 6e20fb60fb | |||
| bc3ea6854b | |||
| 0ca9a0e6d1 | |||
| cc852c661f | |||
| 37c85fc774 | |||
| 6ef549e29a | |||
| b288ecbd25 | |||
| de3c5550a5 | |||
| 8f04a48d1e | |||
| f787511616 | |||
| ea58bc8139 | |||
| 872bb095b6 | |||
| bff5c2f0e1 | |||
| 627fe82ef3 | |||
| 70faf2c3f3 | |||
| 0ec3db59f8 | |||
| caa75981bb | |||
| c49df20cd3 | |||
| 036c5d35cd | |||
| 80920e9ff2 | |||
| 071f982eed | |||
| 4aff3b566f | |||
| eb8bd2d0b1 | |||
| 59fd7530f9 | |||
| 5778685352 | |||
| d3742d2b30 | |||
| fcf0b87489 | |||
| d7ecbc3c83 | |||
| f2279cd737 | |||
| da5ac9e3e6 | |||
| ca027f7eb6 | |||
| 17840e52f5 | |||
| 2904735786 | |||
| 790a410474 | |||
| 01bd672d05 | |||
| 9ad7cf9662 | |||
| 8f156cbba8 | |||
| ae9fe44a7c | |||
| edbddbdb55 | |||
| 2ff33cd354 | |||
| 7675d22676 | |||
| af11c34399 | |||
| f51809d524 | |||
| 3ff9b55077 | |||
| a35770b5d2 | |||
| eb8b2c81f2 | |||
| 034c1fcbcf | |||
| f56f168b99 | |||
| 386c5ee338 | |||
| 1b0b6a2f9a | |||
| d52d0d4a83 | |||
| dc680b8594 | |||
| 44c2c01642 | |||
| 7708087019 | |||
| 52014dc0bb | |||
| f3b9a01904 | |||
| 2fb5a32db2 | |||
| 9be5bc1977 | |||
| 70dd02012b | |||
| f05e0b1b81 | |||
| 7ffcf72483 | |||
| bb420360a4 | |||
| 9b6d54ef30 | |||
| 9dc2901268 | |||
| 50cc5f4102 | |||
| 1152eb6d37 | |||
| f270eda75d | |||
| 2b298fbb85 | |||
| f2abc555e2 | |||
| d6293c75a9 | |||
| d92b3ef781 | |||
| ee1c95f6a3 | |||
| f68696c2a6 | |||
| 40c6c330b0 | |||
| 7c86e1b896 | |||
| 06700cdf38 | |||
| 19234abf32 | |||
| 24d3ee4433 | |||
| 27965cf177 | |||
| 50b06e8e5b | |||
| 3d5679954b | |||
| 561b04cab0 | |||
| 334bd04252 | |||
| 9195c000f3 | |||
| 28d1bd7ce3 | |||
| cb9690ecba | |||
| 78d681f24f | |||
| 709d4cb80d | |||
| 3d32981dd4 | |||
| d5de36badb | |||
| ab3995fe26 | |||
| c641f5aea8 | |||
| 1b4e6aa0e7 | |||
| 6e34585dca | |||
| 2f7887d607 | |||
| 837590d90e | |||
| 94bc870686 | |||
| 911b559113 | |||
| 2ffe003136 | |||
| d348fe62f2 | |||
| be2c1d44e0 | |||
| 8b26751a66 | |||
| 1dc1d2a3f0 | |||
| 99b83b98b6 | |||
| 21448ab874 | |||
| b203b43bb2 | |||
| 09a1bf8ec2 | |||
| 7918a1817c | |||
| 43e1932691 | |||
| 87ce49e14f | |||
| d4e8731edd | |||
| b03c482c95 | |||
| addafe2878 | |||
| c4a04eee97 | |||
| 97157c8fcf | |||
| bfe42d9ccf | |||
| 036fa5ca82 | |||
| 90651a3067 | |||
| c19ac306a1 | |||
| 38731f12de | |||
| 2d4053bed3 | |||
| faf7f5577a | |||
| 4feaa0ba49 | |||
| 4e7b77d7d0 | |||
| 712c99f3cf | |||
| 5d1382f507 | |||
| 8932869423 | |||
| 225644111c | |||
| 1807a8765b | |||
| c420d45e64 | |||
| 1fb2ca5843 | |||
| 2ebab067b4 | |||
| e601c1d9b4 | |||
| 2a968661e7 | |||
| 67a45b0772 | |||
| fccf44f285 | |||
| 9f093737c4 | |||
| 731cb0b426 | |||
| ae428446f9 | |||
| d099239427 | |||
| b16d12568a | |||
| db56d14177 | |||
| 4ee3a6d0a6 | |||
| 19724cf2c2 | |||
| fb0713275e | |||
| 8f4ac73ca9 | |||
| 3924463c6d | |||
| bd77f6673c | |||
| 3be8726afb | |||
| e128f0fae3 | |||
| 3c42e1b4b9 | |||
| 12af5c4444 | |||
| 3738091689 | |||
| 750f59e4ea | |||
| 3486701602 | |||
| e60b204cda | |||
| 0443615683 | |||
| d93238d086 | |||
| fe1ab18474 | |||
| 2c8a915018 | |||
| 4f34b51bda | |||
| cd1fbef0ea | |||
| ca9c74d2c4 | |||
| a7ab53838e | |||
| 680bc74543 | |||
| 3852129612 | |||
| be8abdc1cf | |||
| e77e2ae18f | |||
| 9590b3c683 | |||
| 9cf649eb60 | |||
| b0bc4b2af9 | |||
| f524b28d36 | |||
| debc8e5e65 | |||
| 50a5a1d0a8 | |||
| 6d5a43b47a | |||
| 4fcb553880 | |||
| a1b3cd48fe | |||
| 77515dda8e | |||
| e69f9e4f89 | |||
| 3989e483d5 | |||
| ab53ec7c67 | |||
| 3b339be422 | |||
| 32666e4c24 | |||
| a32c11f5ee | |||
| fe885360cc | |||
| 070d17a2d0 | |||
| 1c0bcf4a69 | |||
| 8ae4acdc00 | |||
| 439fb9d9c9 | |||
| b3aa849f87 | |||
| a9ccb443bc | |||
| 0cca375f03 | |||
| c5bdeb26ed | |||
| d278f20dd4 | |||
| a3111e7541 | |||
| b96b7bc71f | |||
| 3ff49becbe | |||
| 3150dd4628 | |||
| a46b006473 | |||
| cd5b8eefe0 | |||
| bd8041765f | |||
| 9183c80ea6 | |||
| b68b6b95b0 | |||
| c9ac788172 | |||
| eb13eeeacd | |||
| 2ff1f66f37 | |||
| 0053ecb5f4 | |||
| d1c8b1090b | |||
| 13b3e0349e | |||
| 90390d7488 | |||
| 5b8ff8a76d | |||
| e9f5ed108a | |||
| 34ef8fc324 | |||
| 92a7d033a2 | |||
| cb8a9995ac | |||
| fb3c1f8ace | |||
| 53b5f04b15 | |||
| 4f6b4bf709 | |||
| 8a89726726 | |||
| fbff1ee99a | |||
| 734cc5b77e | |||
| 19c958add0 | |||
| d64c990658 | |||
| e156d8e00c | |||
| 831ab6d429 | |||
| 3baaef5109 | |||
| 11d430563b | |||
| bfeafa9948 | |||
| dfd607f195 | |||
| 9b15400936 | |||
| edc1aee471 | |||
| 387df77a6f | |||
| b9ef07a719 | |||
| df49cbb44d | |||
| b1ca2bbbb5 | |||
| ccbb7575c0 | |||
| 5be3ea3d46 | |||
| 7b16a32910 | |||
| 7caac55580 | |||
| f19249c47d | |||
| 6c067a7981 | |||
| f2991134f2 | |||
| e39a23e5ae | |||
| eba2a57122 | |||
| 7595ae0916 | |||
| faff2d70f8 | |||
| f3aec8d33e | |||
| 632ca3f9bf | |||
| 5291ff2b4d | |||
| 97110edbe1 | |||
| fbc0981e8d | |||
| 6b0c1e2992 | |||
| c65026b70a | |||
| b5a0042e14 | |||
| 3be6fb1a80 | |||
| 5dcd877dcd | |||
| ed1c847e7b | |||
| 87962d10e2 | |||
| 4ac4ad3440 | |||
| 742b6e5200 | |||
| 43657d8302 | |||
| b28999e40c | |||
| 42de8307bf | |||
| 48d1d1f80f | |||
| d52e5cfb50 | |||
| fe46a0cce0 | |||
| b081654c51 | |||
| cc622a56ea | |||
| a5b1231f8c | |||
| a8c9257dfe | |||
| bfdd3ccc07 | |||
| 503234976f | |||
| 6fdd83478a | |||
| 0a5a22ec6f | |||
| 21cad56213 | |||
| b0a536aaeb | |||
| 5ee379d588 | |||
| ebbf34e924 | |||
| e4aff871de | |||
| 0a9994c529 | |||
| 06764e2542 | |||
| 84cd2a67d4 | |||
| e3867aa7df | |||
| 4c59282822 | |||
| a68aeafb4b | |||
| bc33878c64 | |||
| cd9f433358 | |||
| 42a5910d8f | |||
| 66daf3f7d3 | |||
| c3c367c54c | |||
| bf3cb6ba84 | |||
| 5c5dcaa027 | |||
| ca64af1390 | |||
| ebe00841fd | |||
| 85f321f30a | |||
| e2ea84f3e3 | |||
| d5898a64ab | |||
| 42b79d7d8a | |||
| 9363745fb0 | |||
| b5f2128db1 | |||
| d5ce01acab | |||
| dbefaef777 | |||
| 7438c59acd | |||
| f5cda21d59 | |||
| 759c5a9fcd | |||
| 1549194b2f | |||
| 4665b4343d | |||
| 1f94e9d20c | |||
| b3f6df939b | |||
| 0e568a4ee6 | |||
| f94ce7e91c | |||
| 6042bc93f6 | |||
| 80be172fdf | |||
| 4cc863a9fb | |||
| 51e07d9fba | |||
| 143d96e300 | |||
| a2fd2536c4 | |||
| f5d751b3eb | |||
| 096dfd61cb | |||
| 89f66ecd10 | |||
| 85f07b10ad | |||
| 809643a159 | |||
| f753d478fa | |||
| 282a2bc8ef | |||
| c6fb3c25f3 | |||
| 5429106ab3 | |||
| 5c387f2b81 | |||
| 6c68ca2a64 | |||
| 4ee6906f47 | |||
| 46b9c11139 | |||
| 8e1510821b | |||
| 288f28620c | |||
| aa5497e385 | |||
| 6bf121b58e | |||
| 01b130a401 | |||
| 256fb0406d | |||
| c57478ee18 | |||
| e74bf33178 | |||
| 2bec882c83 | |||
| 2cfcc957ca | |||
| 972b3dc88b | |||
| ea43e3f5a8 | |||
| 67b1de613c | |||
| 1af22a70b7 | |||
| 504d15f171 | |||
| 1302afb844 | |||
| 028d610397 | |||
| 7323c79dc2 | |||
| 16c6b57c9a | |||
| 329bdaa785 | |||
| e57185a009 | |||
| 4e90ceae91 | |||
| ccf11ad041 | |||
| 631671fb1c | |||
| 1480ede8d4 | |||
| e0b1f471fa | |||
| 58d25464c2 | |||
| 277bdce01d | |||
| 9cf7719958 | |||
| 46f313ac28 | |||
| a630904b41 | |||
| f80140d5ff | |||
| a21079b2ac | |||
| f283a0aadf | |||
| 248961fe31 | |||
| cdc39b69a1 | |||
| 95dac018e3 | |||
| 0e5077dab1 | |||
| 3b5b0f81c4 | |||
| a527439eae | |||
| 4eb1981779 | |||
| 073f95436f | |||
| fa1ebbfdb8 | |||
| 9bdd2ae977 | |||
| 3a63838cdb | |||
| 7a1a2202f8 | |||
| ce65317ab8 | |||
| cd988e53f8 | |||
| ad58607013 | |||
| eae3006f8d | |||
| 024fd99e71 | |||
| 4d3125e58e | |||
| 17a4888481 | |||
| cc4cf3d54f | |||
| 9a6acd5334 | |||
| 2522a3694f | |||
| f2ba338e12 | |||
| 0035259e3d | |||
| 431eb88a2d | |||
| f8d5014921 | |||
| b0072160af | |||
| 536f889649 | |||
| 2c4a718c1a | |||
| b19377a96c | |||
| 76cae09f37 | |||
| 81eb466555 | |||
| 0a24622ca2 | |||
| db0f4dd31c | |||
| fbc69837e1 | |||
| b9b5ead89e | |||
| 1c0f447632 | |||
| e21a95f631 | |||
| 5a05bcb6b7 | |||
| 183116a4b1 | |||
| e916d9d374 | |||
| 604ce4acfd | |||
| cd57da59ec | |||
| 9ff90a9b4d | |||
| 98f34f010c | |||
| d513807c9f | |||
| 8bd401b003 | |||
| b7d22da9f0 | |||
| b98a832c67 | |||
| acf3a7a04b | |||
| 790136b9db | |||
| 265cfc7710 | |||
| 29b693e625 | |||
| 8637bdce12 | |||
| db4e1b2c00 | |||
| c82e1b8ec3 | |||
| 9105db3d82 | |||
| 465300560e | |||
| c97ab27052 | |||
| b776ae9090 | |||
| 238e4e8a87 | |||
| 37c516f7f9 | |||
| 81a8aa063b | |||
| 91091c7819 | |||
| e4141b216a | |||
| b0574c1c2c | |||
| bfd8cfa1c8 | |||
| 67815e7787 | |||
| 031d8457b6 | |||
| c84f5965c3 | |||
| 638028ba7c | |||
| ae9600e872 | |||
| b5d5a41453 | |||
| 93f71ba977 |
+1
-1
@@ -52,5 +52,5 @@ allow-git = [
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
# A patch override for the bindings: https://github.com/Alorel/rust-indexed-db/pull/72
|
||||
"https://github.com/matrix-org/rust-indexed-db",
|
||||
"https://github.com/matrix-org/rust-indexed-db"
|
||||
]
|
||||
|
||||
@@ -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,7 +82,9 @@ 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
|
||||
@@ -92,8 +97,7 @@ jobs:
|
||||
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad
|
||||
uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537
|
||||
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,17 @@ 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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # 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 +51,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -69,32 +73,35 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # 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 +109,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -136,16 +143,18 @@ 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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
|
||||
- name: Install aarch64-apple-ios target
|
||||
run: rustup target install aarch64-apple-ios
|
||||
@@ -156,12 +165,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
@@ -179,7 +188,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 +200,18 @@ 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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
|
||||
- name: Add rust targets
|
||||
run: |
|
||||
@@ -212,7 +223,7 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
|
||||
+58
-40
@@ -7,6 +7,8 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -35,7 +37,7 @@ jobs:
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- experimental-encrypted-state-events
|
||||
- rustls-tls
|
||||
- native-tls
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
@@ -43,10 +45,12 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -54,7 +58,7 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -65,10 +69,10 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -85,21 +89,23 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -116,7 +122,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 +132,20 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -169,10 +177,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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
@@ -183,17 +193,17 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -241,22 +251,24 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.2
|
||||
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@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -267,10 +279,10 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -291,10 +303,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.40.0
|
||||
uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -303,26 +317,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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
|
||||
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@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -362,7 +378,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 +388,15 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
@@ -390,7 +408,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
|
||||
@@ -97,9 +99,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 +112,7 @@ jobs:
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # 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 +120,19 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
with:
|
||||
prefix-key: "coverage"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
uses: taiki-e/install-action@8a8ecf49de4feac160d13668bc406fe17413c1bf # cargo-llvm-cov
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -167,7 +170,7 @@ jobs:
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
- name: Store coverage report in artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.0
|
||||
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@72602674bc341ca927683caddbf578672c352476
|
||||
uses: bnjbvr/cargo-machete@2e2703d664c176156a22936b9203c38cc7a273dd
|
||||
|
||||
@@ -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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
|
||||
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@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
|
||||
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@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
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@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
|
||||
@@ -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,14 @@ 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@4146cc238a18f9b3f3427572fd4585d2d2d78c88 # cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets
|
||||
|
||||
@@ -10,6 +10,8 @@ on:
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Upload coverage report
|
||||
@@ -18,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- name: 'Fetch coverage report from artifacts'
|
||||
id: prepare_report
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
@@ -58,13 +60,14 @@ jobs:
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
path: repo_root
|
||||
persist-credentials: false
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -79,7 +82,7 @@ jobs:
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -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@v4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
@@ -64,7 +68,7 @@ jobs:
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
|
||||
- name: Build
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
|
||||
+6
-1
@@ -16,12 +16,17 @@ extend-ignore-re = [
|
||||
[default.extend-identifiers]
|
||||
WeeChat = "WeeChat"
|
||||
|
||||
# all of these are valid words, but should never appear in this repo
|
||||
[default.extend-words]
|
||||
# all of these are valid words, but should never appear in this repo
|
||||
bellow = "below"
|
||||
stat = "state"
|
||||
sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
# crate name
|
||||
ratatui = "ratatui"
|
||||
# path name
|
||||
consts = "consts"
|
||||
# base64 false positives
|
||||
Nd = "Nd"
|
||||
Abl = "Abl"
|
||||
|
||||
Generated
+497
-298
File diff suppressed because it is too large
Load Diff
+108
-73
@@ -11,63 +11,70 @@ 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 = "1.0.100"
|
||||
aquamarine = "0.6.0"
|
||||
as_variant = "1.3.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-compat = "0.2.5"
|
||||
async-rx = "0.1.3"
|
||||
anyhow = { version = "1.0.100", default-features = false }
|
||||
aquamarine = { version = "0.6.0", default-features = false }
|
||||
as_variant = { version = "1.3.0", default-features = false }
|
||||
assert-json-diff = { version = "2.0.2", default-features = false }
|
||||
assert_matches = { version = "1.5.0", default-features = false }
|
||||
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 }
|
||||
# Bumping this to 0.3.6 produces a test failure because the semantic between the
|
||||
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.10.0"
|
||||
byteorder = "1.5.0"
|
||||
cfg-if = "1.0.4"
|
||||
clap = "4.5.53"
|
||||
chrono = "0.4.42"
|
||||
dirs = "6.0.0"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.8.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.10.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
getrandom = { version = "0.2.15", default-features = false }
|
||||
gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.3.1"
|
||||
imbl = "6.1.0"
|
||||
indexed_db_futures = { version = "0.7.0", package = "matrix_indexed_db_futures" }
|
||||
indexmap = "2.12.1"
|
||||
async-stream = { version = "0.3.6", default-features = false }
|
||||
async-trait = { version = "0.1.89", default-features = false }
|
||||
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
||||
bitflags = { version = "2.10.0", default-features = false }
|
||||
byteorder = { version = "1.5.0", default-features = false, features = ["std"] }
|
||||
cfg-if = { version = "1.0.4", default-features = false }
|
||||
clap = { version = "4.5.53", default-features = false, features = ["std", "help", "usage"] }
|
||||
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std", "oldtime", "wasmbind"] }
|
||||
dirs = { version = "6.0.0", default-features = false }
|
||||
eyeball = { version = "0.8.8", default-features = false, features = ["tracing"] }
|
||||
eyeball-im = { version = "0.8.0", default-features = false, features = ["tracing"] }
|
||||
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.3.4", 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 }
|
||||
hkdf = { version = "0.12.4", default-features = false }
|
||||
hmac = { version = "0.12.1", default-features = false }
|
||||
http = { version = "1.3.1", default-features = false }
|
||||
imbl = { version = "6.1.0", default-features = false }
|
||||
indexed_db_futures = { version = "0.7.0", package = "matrix_indexed_db_futures", default-features = false }
|
||||
indexmap = { version = "2.12.1", default-features = false }
|
||||
insta = { version = "1.44.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.82"
|
||||
mime = "0.3.17"
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
|
||||
once_cell = "1.21.3"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
regex = "1.12.2"
|
||||
reqwest = { version = "0.12.24", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
ruma = { version = "0.14.0", features = [
|
||||
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 = ["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.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"] }
|
||||
regex = { version = "1.12.2", default-features = false }
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
ring = { version = "0.17.14", default-features = false }
|
||||
rmp-serde = { version = "1.3.0", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "dc36ccf119c065b75a9872ec5a6487d680289dc1", features = [
|
||||
"client-api-c",
|
||||
"compat-unset-avatar",
|
||||
"compat-upload-signatures",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
@@ -76,6 +83,7 @@ ruma = { version = "0.14.0", features = [
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3230",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3417",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
@@ -88,34 +96,38 @@ ruma = { version = "0.14.0", features = [
|
||||
"unstable-msc4308",
|
||||
"unstable-msc4310",
|
||||
] }
|
||||
rustls = { version = "0.23.37", default-features = false, features = ["ring"] }
|
||||
rustls-pki-types = { version = "1.14.0", default-features = false }
|
||||
sentry = { version = "0.46.0", default-features = false }
|
||||
sentry-tracing = "0.46.0"
|
||||
serde = { version = "1.0.228", features = ["rc"] }
|
||||
serde_html_form = "0.2.8"
|
||||
serde_json = "1.0.145"
|
||||
sha2 = "0.10.9"
|
||||
similar-asserts = "1.7.0"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2.0.17"
|
||||
sentry-tracing = { version = "0.46.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"] }
|
||||
sha2 = { version = "0.10.9", default-features = false }
|
||||
similar-asserts = { version = "1.7.0", default-features = false }
|
||||
stream_assert = { version = "0.1.1", default-features = false }
|
||||
syn = { version = "2.0.43", default-features = false, features = ["derive", "parsing", "printing", "clone-impls"] }
|
||||
tempfile = { version = "3.23.0", default-features = false }
|
||||
thiserror = { version = "2.0.17", default-features = false }
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-stream = { version = "0.1.17", default-features = false }
|
||||
tracing = { version = "0.1.41", default-features = false, features = ["std"] }
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-core = "0.1.34"
|
||||
tracing-subscriber = "0.3.20"
|
||||
unicode-normalization = "0.1.25"
|
||||
uniffi = { version = "0.30.0" }
|
||||
uniffi_bindgen = { version = "0.30.0" }
|
||||
url = "2.5.7"
|
||||
uuid = "1.18.1"
|
||||
vergen-gitcl = "1.0.8"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.105"
|
||||
wasm-bindgen-test = "0.3.55"
|
||||
web-sys = "0.3.82"
|
||||
wiremock = "0.6.5"
|
||||
zeroize = "1.8.2"
|
||||
tracing-appender = { version = "0.2.3", default-features = false }
|
||||
tracing-core = { version = "0.1.34", default-features = false }
|
||||
tracing-subscriber = { version = "0.3.20", default-features = false, features = ["std", "smallvec", "fmt"] }
|
||||
unicode-normalization = { version = "0.1.25", default-features = false }
|
||||
unicode-segmentation = { version = "1.12.0", default-features = false }
|
||||
uniffi = { version = "0.31.0", default-features = false, features = ["cargo-metadata"] }
|
||||
uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["cargo-metadata"] }
|
||||
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"] }
|
||||
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 }
|
||||
wiremock = { version = "0.6.5", default-features = false }
|
||||
zeroize = { version = "1.8.2", default-features = false }
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.16.0" }
|
||||
@@ -167,6 +179,19 @@ unused_async = "warn"
|
||||
# Saves a lot of disk space. If symbols are needed, use the dbg profile.
|
||||
debug = 0
|
||||
|
||||
# Profile for debug builds with full optimization and minimal debug symbols.
|
||||
# This should be just enough to have proper backtraces, having way smaller binaries
|
||||
# (10% of the size with full debug symbols profile, like `reldbg`).
|
||||
# This profile differs from `reldbg` in not containing the debug symbols needed for
|
||||
# debugging with LLDB/GDB, trading that for binary size, allowing quick iterations
|
||||
# of building the bindings, installing in a real device, testing your changes, repeat.
|
||||
# It's also different from `dev` in having enough debug symbols to display backtraces.
|
||||
[profile.reldev]
|
||||
inherits = "dev"
|
||||
opt-level = 3
|
||||
debug = "line-tables-only"
|
||||
strip = "debuginfo"
|
||||
|
||||
[profile.dev.package]
|
||||
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
@@ -187,6 +212,16 @@ debug = 2
|
||||
inherits = "dbg"
|
||||
opt-level = 3
|
||||
|
||||
[profile.dist]
|
||||
# Use release profile as a base
|
||||
inherits = "release"
|
||||
# Strip the minimal debug info, while still allowing us to have proper backtraces, but it will affect debuggers
|
||||
strip = "debuginfo"
|
||||
# Use link time optimizations
|
||||
lto = true
|
||||
# Use binary size optimization, since this is intended for distributed copies of the SDK
|
||||
opt-level = "s"
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
# LTO is too slow to compile.
|
||||
|
||||
@@ -15,9 +15,9 @@ codspeed = []
|
||||
|
||||
[dependencies]
|
||||
assert_matches.workspace = true
|
||||
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
|
||||
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 = ["rustls-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
@@ -28,7 +28,7 @@ ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
wiremock.workspace = true
|
||||
|
||||
[[bench]]
|
||||
|
||||
@@ -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;
|
||||
@@ -188,10 +180,7 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.unwrap();
|
||||
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.with_focus(TimelineFocus::PinnedEvents {
|
||||
max_events_to_load: 100,
|
||||
max_concurrent_requests: 10,
|
||||
})
|
||||
.with_focus(TimelineFocus::PinnedEvents)
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
@@ -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 ruma::{OwnedRoomId, RoomId, owned_user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a room list.
|
||||
@@ -30,15 +30,26 @@ pub fn create(c: &mut Criterion) {
|
||||
let server_ts_range = Uniform::from(100..1000);
|
||||
|
||||
for room_nth in 0..NUMBER_OF_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!r{room_nth}")).unwrap();
|
||||
// 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 {
|
||||
|
||||
@@ -51,6 +51,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dependencies.js_int]
|
||||
version = "0.2.2"
|
||||
default-features = false
|
||||
features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
@@ -63,12 +64,11 @@ features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen-gitcl = { workspace = true, features = ["build"] }
|
||||
uniffi = { workspace = true, default-features = false, features = ["build"] }
|
||||
vergen-gitcl = { workspace = true, default-features = false, features = ["build"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches2.workspace = true
|
||||
|
||||
@@ -76,6 +76,49 @@ pub enum DecryptionError {
|
||||
Store { error: String },
|
||||
}
|
||||
|
||||
/// Error describing what went wrong when exporting a [`SecretsBundle`].
|
||||
///
|
||||
/// The [`SecretsBundle`] can only be exported if we have all cross-signing
|
||||
/// private keys in the store.
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum SecretsBundleExportError {
|
||||
/// The store itself had an error.
|
||||
#[error(transparent)]
|
||||
CryptoStore(CryptoStoreError),
|
||||
/// We're missing one or more cross-signing keys.
|
||||
#[error("The store doesn't contain all the cross-signing keys")]
|
||||
MissingCrossSigningKeys,
|
||||
/// We have a backup key stored, but we don't know the version of the
|
||||
/// backup.
|
||||
#[error("The store contains a backup key, but no backup version")]
|
||||
MissingBackupVersion,
|
||||
#[error("serialization error: {error}")]
|
||||
Serialization { error: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::SecretsBundleExportError> for SecretsBundleExportError {
|
||||
fn from(value: matrix_sdk_crypto::store::SecretsBundleExportError) -> Self {
|
||||
match value {
|
||||
matrix_sdk_crypto::store::SecretsBundleExportError::Store(e) => {
|
||||
Self::CryptoStore(e.into())
|
||||
}
|
||||
matrix_sdk_crypto::store::SecretsBundleExportError::MissingCrossSigningKey(_)
|
||||
| matrix_sdk_crypto::store::SecretsBundleExportError::MissingCrossSigningKeys => {
|
||||
Self::MissingCrossSigningKeys
|
||||
}
|
||||
matrix_sdk_crypto::store::SecretsBundleExportError::MissingBackupVersion => {
|
||||
Self::MissingBackupVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for SecretsBundleExportError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Self::Serialization { error: err.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MegolmError> for DecryptionError {
|
||||
fn from(value: MegolmError) -> Self {
|
||||
match &value {
|
||||
|
||||
@@ -506,6 +506,7 @@ fn collect_sessions(
|
||||
})
|
||||
.collect::<anyhow::Result<_>>()?,
|
||||
sender_data: SenderData::legacy(),
|
||||
forwarder_data: None,
|
||||
room_id: RoomId::parse(session.room_id)?,
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
|
||||
@@ -53,7 +53,10 @@ use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
dehydrated_devices::DehydratedDevices,
|
||||
error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError},
|
||||
error::{
|
||||
CryptoStoreError, DecryptionError, SecretImportError, SecretsBundleExportError,
|
||||
SignatureError,
|
||||
},
|
||||
parse_user_id,
|
||||
responses::{response_from_string, OwnedResponse},
|
||||
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
|
||||
@@ -802,12 +805,12 @@ impl OlmMachine {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let content = serde_json::from_str(&content)?;
|
||||
|
||||
let encrypted_content = self
|
||||
let result = self
|
||||
.runtime
|
||||
.block_on(self.inner.encrypt_room_event_raw(&room_id, &event_type, &content))
|
||||
.expect("Encrypting an event produced an error");
|
||||
|
||||
Ok(serde_json::to_string(&encrypted_content)?)
|
||||
Ok(serde_json::to_string(&result.content)?)
|
||||
}
|
||||
|
||||
/// Encrypt the given event with the given type and content for the given
|
||||
@@ -1409,6 +1412,24 @@ impl OlmMachine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export all the secrets we have in the store into a serialized
|
||||
/// SecretsBundle.
|
||||
///
|
||||
/// This method will export all the private cross-signing keys and, if
|
||||
/// available, the private part of a backup key and its accompanying
|
||||
/// version.
|
||||
///
|
||||
/// The method will fail if we don't have all three private cross-signing
|
||||
/// keys available.
|
||||
///
|
||||
/// **Warning**: Only export this and share it with a trusted recipient,
|
||||
/// i.e. if an existing device is sharing this with a new device.
|
||||
pub fn export_secrets_bundle(&self) -> Result<String, SecretsBundleExportError> {
|
||||
let bundle = self.runtime.block_on(self.inner.store().export_secrets_bundle())?;
|
||||
|
||||
Ok(serde_json::to_string(&bundle)?)
|
||||
}
|
||||
|
||||
/// Request missing local secrets from our devices (cross signing private
|
||||
/// keys, megolm backup). This will ask the sdk to create outgoing
|
||||
/// request to get the missing secrets.
|
||||
|
||||
@@ -209,7 +209,7 @@ impl Sas {
|
||||
///
|
||||
/// # Flowchart
|
||||
///
|
||||
/// The flow of the verification process is pictured bellow. Please note
|
||||
/// The flow of the verification process is pictured below. Please note
|
||||
/// that the process can be cancelled at each step of the process.
|
||||
/// Either side can cancel the process.
|
||||
///
|
||||
|
||||
@@ -17,9 +17,9 @@ test = false
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.86"
|
||||
quote = "1.0.18"
|
||||
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
|
||||
proc-macro2 = { workspace = true , features = ["proc-macro"] }
|
||||
quote.workspace = true
|
||||
syn = { workspace = true, features = ["full", "extra-traits", "proc-macro"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -6,6 +6,157 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 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
|
||||
[this issue](https://github.com/mozilla/uniffi-rs/issues/2740)).
|
||||
([#6069](https://github.com/matrix-org/matrix-rust-sdk/pull/6069),
|
||||
[#6112](https://github.com/matrix-org/matrix-rust-sdk/pull/6112),
|
||||
[#6115](https://github.com/matrix-org/matrix-rust-sdk/pull/6115),
|
||||
[#6116](https://github.com/matrix-org/matrix-rust-sdk/pull/6116)).
|
||||
- `Client::create_room` now uses `RoomPowerLevelsContentOverride` under the hood instead of
|
||||
`RoomPowerLevelsEventContent` to be able to explicitly set values which would previously be
|
||||
ignored if they matched the default power level values specified by the spec: these may not be
|
||||
the same in the homeserver and result in rooms with incorrect power levels being created.
|
||||
([#6034](https://github.com/matrix-org/matrix-rust-sdk/pull/6034))
|
||||
- Fix the `is_last_admin` check in `LeaveSpaceRoom` since it was not
|
||||
accounting for the membership state.
|
||||
[#6032](https://github.com/matrix-org/matrix-rust-sdk/pull/6032)
|
||||
- [**breaking**] `LatestEventValue::Local { is_sending: bool }` is replaced
|
||||
by [`state: LatestEventValueLocalState`] to represent 3 states: `IsSending`,
|
||||
`HasBeenSent` and `CannotBeSent`.
|
||||
([#5968](https://github.com/matrix-org/matrix-rust-sdk/pull/5968/))
|
||||
|
||||
### Features
|
||||
|
||||
- [**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.
|
||||
[#6050](https://github.com/matrix-org/matrix-rust-sdk/pull/6050)
|
||||
- [**breaking**] Remove `TimelineFilter::EventTypeFilter` which has been replaced by
|
||||
the more generic `TimelineFilter::EventFilter`. Users of `TimelineEventTypeFilter::include`
|
||||
and `TimelineEventTypeFilter::exclude` can switch to `TimelineEventFilter::include_event_types`
|
||||
and `TimelineEventFilter::exclude_event_types`.
|
||||
([#6070](https://github.com/matrix-org/matrix-rust-sdk/pull/6070/))
|
||||
- Add `TimelineFilter::EventFilter` for filtering events based on their type or
|
||||
content. For content filtering, only membership and profile change filters
|
||||
are available as of now.
|
||||
([#6048](https://github.com/matrix-org/matrix-rust-sdk/pull/6048/))
|
||||
- Introduce `SpaceFilter`s as a mechanism for narrowing down what's displayed in
|
||||
the room list ([#6025](https://github.com/matrix-org/matrix-rust-sdk/pull/6025))
|
||||
- Expose room power level thresholds in `OtherState::RoomPowerLevels` (ban, kick, invite, redact, state &
|
||||
events defaults, per-event overrides, notifications), so clients can compute the required power level
|
||||
for actions and compare with previous values. ([#5931](https://github.com/matrix-org/matrix-rust-sdk/pull/5931))
|
||||
- Add `RoomCreationParameters::is_space` parameter to be able to create spaces. ([#6010](https://github.com/matrix-org/matrix-rust-sdk/pull/6010/))
|
||||
- [**breaking**] `LazyTimelineItemProvider::get_shields` no longer returns an
|
||||
an `Option`: the `ShieldState` type contains a `None` variant, so the
|
||||
`Option` was redundant. The `message` field has also been removed: since there
|
||||
was no way to localise the returned string, applications should not be using it.
|
||||
([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959))
|
||||
- Add `Room::list_threads` to list all the threads in a room.
|
||||
([#5953](https://github.com/matrix-org/matrix-rust-sdk/pull/5953))
|
||||
- Add `SpaceService::get_space_room` to get a space given its id from the space graph if available.
|
||||
[#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944)
|
||||
- Add `QrCodeData::to_bytes()` to allow generation of a QR code.
|
||||
([#5939](https://github.com/matrix-org/matrix-rust-sdk/pull/5939))
|
||||
- [**breaking**]: The new Latest Event API replaces the old API.
|
||||
`Room::new_latest_event` overwrites the `Room::latest_event` method. See the
|
||||
documentation of `matrix_sdk::latest_event` to learn about the new API.
|
||||
[#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/)
|
||||
- Created `RoomPowerLevels::events` function which returns a `HashMap<TimelineEventType, i64>` with all the power
|
||||
levels per event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
|
||||
- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of
|
||||
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 `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
|
||||
|
||||
- `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))
|
||||
- [**breaking**] The existing `TimelineEventType` was renamed to `TimelineEventContent`, because it contained the
|
||||
actual contents of the event. Then, we created a new `TimelineEventType` enum that actually contains *just* the
|
||||
event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
|
||||
- [**breaking**] The function `TimelineEvent::event_type` is now `TimelineEvent::content`.
|
||||
([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
|
||||
- [**breaking**] The `SpaceService` will no longer auto-subscribe to required
|
||||
client events when invoking the `subscribe_to_joined_spaces` but instead do it
|
||||
through its, now async, constructor.
|
||||
([#5972](https://github.com/matrix-org/matrix-rust-sdk/pull/5972))
|
||||
- [**breaking**] The `SpaceService`'s `joined_spaces` method has been renamed
|
||||
`top_level_joined_spaces` and `subscribe_to_joined_spaces` to `space_service.subscribe_to_top_level_joined_spaces`
|
||||
([#5972](https://github.com/matrix-org/matrix-rust-sdk/pull/5972))
|
||||
|
||||
## [0.16.0] - 2025-12-04
|
||||
|
||||
### Breaking changes
|
||||
|
||||
@@ -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", "rustls-tls"]
|
||||
# Use SQLite for the session storage.
|
||||
sqlite = ["matrix-sdk/sqlite"]
|
||||
# Use an embedded version of SQLite.
|
||||
@@ -45,11 +45,12 @@ experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-em
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
extension-trait = "1.0.2"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
language-tags = "0.3.2"
|
||||
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
||||
log-panics = { version = "2.1.0", default-features = false, features = ["with-backtrace"] }
|
||||
matrix-sdk = { workspace = true, features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
@@ -63,15 +64,38 @@ matrix-sdk-base.workspace = true
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.17"
|
||||
once_cell.workspace = true
|
||||
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
|
||||
mime = { version = "0.3.17", default-features = false }
|
||||
ruma = { workspace = true, features = [
|
||||
"html",
|
||||
"unstable-msc3488",
|
||||
"compat-unset-avatar",
|
||||
"unstable-msc3245-v1-compat",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc3230",
|
||||
# Audio event type
|
||||
"unstable-msc3927",
|
||||
# File event type
|
||||
"unstable-msc3551",
|
||||
# Image event type
|
||||
"unstable-msc3552",
|
||||
# Video event type
|
||||
"unstable-msc3553",
|
||||
# Voice event type
|
||||
"unstable-msc3245",
|
||||
# Emote event type
|
||||
"unstable-msc3954",
|
||||
# Image pack event type
|
||||
"unstable-msc2545",
|
||||
# Room language event type
|
||||
"unstable-msc4334",
|
||||
] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sentry = { workspace = true, optional = true, default-features = false, features = [
|
||||
sentry = { workspace = true, optional = true, features = [
|
||||
# Most default features enabled otherwise.
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
"panic",
|
||||
"reqwest",
|
||||
"sentry-debug-images",
|
||||
@@ -83,12 +107,12 @@ tracing-appender.workspace = true
|
||||
tracing-core.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
url.workspace = true
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
uuid = { version = "1.4.1", default-features = false, features = ["std", "v4"] }
|
||||
zeroize.workspace = true
|
||||
oauth2.workspace = true
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_error_panic_hook = { version = "0.1.7", default-features = false }
|
||||
tokio = { workspace = true, features = ["sync", "macros"] }
|
||||
uniffi = { workspace = true, features = ["wasm-unstable-single-threaded"] }
|
||||
futures-executor.workspace = true
|
||||
@@ -99,10 +123,19 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = "0.2.2"
|
||||
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"
|
||||
# Used to avoid having to call an exposed JNI call from the Android client to initialize `rustls-platform-verifier`
|
||||
libloading = "0.9.0"
|
||||
# Needed for intializing and keeping the JavaVM reference around
|
||||
once_cell = "1.21.4"
|
||||
|
||||
[dev-dependencies]
|
||||
similar-asserts.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -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},
|
||||
@@ -85,7 +99,7 @@ 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,
|
||||
@@ -13,10 +27,9 @@ use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use matrix_sdk::STATE_STORE_DATABASE_NAME;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
authentication::oauth::{ClientId, OAuthAuthorizationData, OAuthError, OAuthSession},
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
executor::AbortOnDrop,
|
||||
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
|
||||
ruma::{
|
||||
api::client::{
|
||||
@@ -41,9 +54,12 @@ use matrix_sdk::{
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
task_monitor::BackgroundTaskFailureReason,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_common::{
|
||||
cross_process_lock::CrossProcessLockConfig, stream::StreamExt, SendOutsideWasm, SyncOutsideWasm,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
@@ -57,8 +73,12 @@ use oauth2::Scope;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
alias::get_alias,
|
||||
discovery::get_authorization_server_metadata::v1::{
|
||||
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
|
||||
},
|
||||
error::ErrorKind,
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
events::{
|
||||
@@ -75,7 +95,6 @@ use ruma::{
|
||||
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
|
||||
},
|
||||
message::{OriginalSyncRoomMessageEvent, Relation},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
@@ -86,7 +105,7 @@ use ruma::{
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
room_version_rules::AuthorizationRules,
|
||||
room::RoomType,
|
||||
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -187,7 +206,19 @@ impl From<PushFormat> for RumaPushFormat {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait ClientDelegate: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// A callback invoked whenever the SDK runs into an unknown token error.
|
||||
fn did_receive_auth_error(&self, is_soft_logout: bool);
|
||||
|
||||
/// A callback invoked when a background task registered with the client's
|
||||
/// task monitor encounters an error.
|
||||
///
|
||||
/// Can default to an empty implementation, if the embedder doesn't care
|
||||
/// about handling background jobs errors.
|
||||
fn on_background_task_error_report(
|
||||
&self,
|
||||
task_name: String,
|
||||
error: BackgroundTaskFailureReason,
|
||||
);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -223,6 +254,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 {
|
||||
@@ -255,11 +312,20 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegateData {
|
||||
/// The delegate itself, that will receive the callbacks.
|
||||
delegate: Arc<dyn ClientDelegate>,
|
||||
|
||||
// The background task error listener task, that will forward errors occurring in background
|
||||
// jobs to the delegate.
|
||||
_background_error_listener_task: Arc<AbortOnDrop<()>>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Client {
|
||||
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
|
||||
|
||||
delegate: OnceLock<Arc<dyn ClientDelegate>>,
|
||||
delegate_data: OnceLock<ClientDelegateData>,
|
||||
|
||||
pub(crate) utd_hook_manager: OnceLock<Arc<UtdHookManager>>,
|
||||
|
||||
@@ -276,7 +342,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> {
|
||||
@@ -308,29 +373,26 @@ impl Client {
|
||||
}
|
||||
});
|
||||
|
||||
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()),
|
||||
delegate: OnceLock::new(),
|
||||
delegate_data: OnceLock::new(),
|
||||
utd_hook_manager: OnceLock::new(),
|
||||
session_verification_controller,
|
||||
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 {
|
||||
@@ -725,6 +787,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
|
||||
@@ -882,46 +966,50 @@ impl Client {
|
||||
.collect();
|
||||
|
||||
// Convert SDK event to FFI type
|
||||
let (sender, event, thread_id) = match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let thread_id = match &deserialized {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
match event.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(
|
||||
content,
|
||||
)) => match content.relates_to {
|
||||
Some(Relation::Thread(thread)) => {
|
||||
Some(thread.event_id.to_string())
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let event = NotificationEvent::Timeline {
|
||||
event: Arc::new(crate::event::TimelineEvent(Box::new(
|
||||
deserialized,
|
||||
))),
|
||||
};
|
||||
(sender, event, thread_id)
|
||||
_ => 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;
|
||||
}
|
||||
}
|
||||
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)
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
@@ -984,6 +1072,7 @@ impl Client {
|
||||
listener.on_notification(
|
||||
NotificationItem {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
@@ -1131,14 +1220,14 @@ impl Client {
|
||||
self: Arc<Self>,
|
||||
delegate: Option<Box<dyn ClientDelegate>>,
|
||||
) -> Result<Option<Arc<TaskHandle>>, ClientError> {
|
||||
if self.delegate.get().is_some() {
|
||||
if self.delegate_data.get().is_some() {
|
||||
return Err(ClientError::Generic {
|
||||
msg: "Delegate already initialized".to_owned(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(delegate.map(|delegate| {
|
||||
let handle = delegate.map(|delegate| {
|
||||
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
|
||||
let client_clone = self.clone();
|
||||
let session_change_task = get_runtime_handle().spawn(async move {
|
||||
@@ -1154,13 +1243,29 @@ impl Client {
|
||||
}
|
||||
});
|
||||
|
||||
self.delegate.get_or_init(|| Arc::from(delegate));
|
||||
let delegate: Arc<dyn ClientDelegate> = delegate.into();
|
||||
|
||||
let client = self.inner.clone();
|
||||
let delegate_clone = delegate.clone();
|
||||
let task = Arc::new(AbortOnDrop::new(get_runtime_handle().spawn(async move {
|
||||
let mut receiver = client.task_monitor().subscribe();
|
||||
while let Ok(error) = receiver.recv().await {
|
||||
delegate_clone.on_background_task_error_report(error.task.name, error.reason);
|
||||
}
|
||||
})));
|
||||
|
||||
let delegate_data =
|
||||
ClientDelegateData { delegate, _background_error_listener_task: task };
|
||||
|
||||
self.delegate_data.get_or_init(|| delegate_data);
|
||||
|
||||
Arc::new(TaskHandle::new(session_change_task))
|
||||
}))
|
||||
});
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Sets the [UnableToDecryptDelegate] which will inform about UTDs.
|
||||
/// Sets the [`UnableToDecryptDelegate`] which will inform about UTDs.
|
||||
/// Returns an error if the delegate was already set.
|
||||
pub async fn set_utd_delegate(
|
||||
self: Arc<Self>,
|
||||
@@ -1206,20 +1311,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> {
|
||||
@@ -1505,8 +1610,8 @@ impl Client {
|
||||
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
|
||||
}
|
||||
|
||||
pub fn space_service(&self) -> Arc<SpaceService> {
|
||||
let inner = UISpaceService::new((*self.inner).clone());
|
||||
pub async fn space_service(&self) -> Arc<SpaceService> {
|
||||
let inner = UISpaceService::new((*self.inner).clone()).await;
|
||||
Arc::new(SpaceService::new(inner))
|
||||
}
|
||||
|
||||
@@ -1870,6 +1975,12 @@ impl Client {
|
||||
.any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
|
||||
}
|
||||
|
||||
/// Checks if the server supports login using a QR code.
|
||||
pub async fn is_login_with_qr_code_supported(&self) -> Result<bool, ClientError> {
|
||||
Ok(matches!(self.inner.auth_api(), Some(AuthApi::OAuth(_)))
|
||||
&& self.inner.unstable_features().await?.contains(&ruma::api::FeatureFlag::Msc4108))
|
||||
}
|
||||
|
||||
/// Get server vendor information from the federation API.
|
||||
///
|
||||
/// This method retrieves information about the server's name and version
|
||||
@@ -2124,11 +2235,12 @@ impl From<&search_users::v3::User> for UserProfile {
|
||||
|
||||
impl Client {
|
||||
fn process_session_change(&self, session_change: SessionChange) {
|
||||
if let Some(delegate) = self.delegate.get().cloned() {
|
||||
if let Some(delegate_data) = self.delegate_data.get() {
|
||||
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 => {}
|
||||
});
|
||||
@@ -2224,34 +2336,17 @@ pub struct PowerLevels {
|
||||
pub events: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
impl From<PowerLevels> for RoomPowerLevelsContentOverride {
|
||||
fn from(value: PowerLevels) -> Self {
|
||||
let mut power_levels = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
|
||||
|
||||
if let Some(users_default) = value.users_default {
|
||||
power_levels.users_default = users_default.into();
|
||||
}
|
||||
if let Some(state_default) = value.state_default {
|
||||
power_levels.state_default = state_default.into();
|
||||
}
|
||||
if let Some(events_default) = value.events_default {
|
||||
power_levels.events_default = events_default.into();
|
||||
}
|
||||
if let Some(ban) = value.ban {
|
||||
power_levels.ban = ban.into();
|
||||
}
|
||||
if let Some(kick) = value.kick {
|
||||
power_levels.kick = kick.into();
|
||||
}
|
||||
if let Some(redact) = value.redact {
|
||||
power_levels.redact = redact.into();
|
||||
}
|
||||
if let Some(invite) = value.invite {
|
||||
power_levels.invite = invite.into();
|
||||
}
|
||||
if let Some(notifications) = value.notifications {
|
||||
power_levels.notifications = notifications.into()
|
||||
}
|
||||
let mut power_levels = RoomPowerLevelsContentOverride::default();
|
||||
power_levels.users_default = value.users_default.map(Into::into);
|
||||
power_levels.state_default = value.state_default.map(Into::into);
|
||||
power_levels.events_default = value.events_default.map(Into::into);
|
||||
power_levels.ban = value.ban.map(Into::into);
|
||||
power_levels.kick = value.kick.map(Into::into);
|
||||
power_levels.redact = value.redact.map(Into::into);
|
||||
power_levels.invite = value.invite.map(Into::into);
|
||||
power_levels.notifications = value.notifications.map(Into::into).unwrap_or_default();
|
||||
power_levels.users = value
|
||||
.users
|
||||
.iter()
|
||||
@@ -2263,7 +2358,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels.events = value
|
||||
.events
|
||||
.iter()
|
||||
@@ -2272,7 +2366,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
(event_type, (*power_level).into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels
|
||||
}
|
||||
}
|
||||
@@ -2299,6 +2392,8 @@ pub struct CreateRoomParameters {
|
||||
pub history_visibility_override: Option<RoomHistoryVisibility>,
|
||||
#[uniffi(default = None)]
|
||||
pub canonical_alias: Option<String>,
|
||||
#[uniffi(default = false)]
|
||||
pub is_space: bool,
|
||||
}
|
||||
|
||||
impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
@@ -2353,8 +2448,14 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
|
||||
request.initial_state = initial_state;
|
||||
|
||||
if value.is_space {
|
||||
let mut creation_content = CreationContent::new();
|
||||
creation_content.room_type = Some(RoomType::Space);
|
||||
request.creation_content = Some(Raw::new(&creation_content)?);
|
||||
}
|
||||
|
||||
if let Some(power_levels) = value.power_level_content_override {
|
||||
match Raw::new(&power_levels.into()) {
|
||||
match Raw::<RoomPowerLevelsContentOverride>::new(&power_levels.into()) {
|
||||
Ok(power_levels) => {
|
||||
request.power_level_content_override = Some(power_levels);
|
||||
}
|
||||
@@ -2559,23 +2660,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,
|
||||
@@ -2887,3 +2988,66 @@ impl From<matrix_sdk::StoreSizes> for StoreSizes {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::{
|
||||
api::client::room::{create_room, Visibility},
|
||||
events::StateEventType,
|
||||
room::RoomType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{CreateRoomParameters, JoinRule, RoomPreset, RoomVisibility},
|
||||
room::RoomHistoryVisibility,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_create_room_parameters_mapping() {
|
||||
let params = CreateRoomParameters {
|
||||
name: Some("A room".to_owned()),
|
||||
topic: Some("A topic".to_owned()),
|
||||
is_encrypted: true,
|
||||
is_direct: true,
|
||||
visibility: RoomVisibility::Public,
|
||||
preset: RoomPreset::PublicChat,
|
||||
invite: Some(vec!["@user:example.com".to_owned()]),
|
||||
avatar: Some("http://example.com/avatar.jpg".to_owned()),
|
||||
power_level_content_override: None,
|
||||
join_rule_override: Some(JoinRule::Knock),
|
||||
history_visibility_override: Some(RoomHistoryVisibility::Shared),
|
||||
canonical_alias: Some("#a-room:example.com".to_owned()),
|
||||
is_space: true,
|
||||
};
|
||||
|
||||
let request: create_room::v3::Request =
|
||||
params.try_into().expect("CreateRoomParameters couldn't be transformed into a Request");
|
||||
let initial_state = request
|
||||
.initial_state
|
||||
.iter()
|
||||
.map(|raw| raw.deserialize().expect("Initial state event failed to deserialize"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(request.name, Some("A room".to_owned()));
|
||||
assert_eq!(request.topic, Some("A topic".to_owned()));
|
||||
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomEncryption));
|
||||
assert!(request.is_direct);
|
||||
assert_eq!(request.visibility, Visibility::Public);
|
||||
assert_eq!(request.preset, Some(create_room::v3::RoomPreset::PublicChat));
|
||||
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_eq!(request.room_alias_name, Some("#a-room:example.com".to_owned()));
|
||||
|
||||
let room_type = request
|
||||
.creation_content
|
||||
.expect("Creation content is missing")
|
||||
.deserialize()
|
||||
.expect("Creation content can't be deserialized")
|
||||
.room_type;
|
||||
assert_eq!(room_type, Some(RoomType::Space));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +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.
|
||||
|
||||
// Allow UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
cross_process_lock::CrossProcessLockConfig as SdkCrossProcessLockConfig,
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
ruma::{ServerName, UserId},
|
||||
@@ -115,8 +130,7 @@ 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,
|
||||
@@ -160,8 +174,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(),
|
||||
@@ -183,18 +196,12 @@ impl ClientBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -352,12 +359,8 @@ impl ClientBuilder {
|
||||
|
||||
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()? {
|
||||
@@ -406,27 +409,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();
|
||||
@@ -498,15 +508,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 +626,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,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;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
@@ -398,6 +412,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 +421,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()
|
||||
}
|
||||
@@ -563,11 +595,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,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, error::Error, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
@@ -736,7 +750,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 +764,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 +778,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 +806,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 +825,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 +836,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,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 anyhow::{bail, Context};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
@@ -11,13 +25,14 @@ use ruma::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
TimelineEventType as RumaTimelineEventType,
|
||||
},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, RtcNotificationType},
|
||||
ruma::{MessageType, RtcCallIntent, RtcNotificationType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
@@ -39,16 +54,16 @@ impl TimelineEvent {
|
||||
self.0.origin_server_ts().into()
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
let event_type = match &*self.0 {
|
||||
pub fn content(&self) -> Result<TimelineEventContent, ClientError> {
|
||||
let content = match &*self.0 {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
TimelineEventType::MessageLike { content: event.clone().try_into()? }
|
||||
TimelineEventContent::MessageLike { content: event.clone().try_into()? }
|
||||
}
|
||||
AnySyncTimelineEvent::State(event) => {
|
||||
TimelineEventType::State { content: event.clone().try_into()? }
|
||||
TimelineEventContent::State { content: event.clone().try_into()? }
|
||||
}
|
||||
};
|
||||
Ok(event_type)
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Returns the thread root event id for the event, if it's part of a
|
||||
@@ -72,16 +87,216 @@ impl From<AnyTimelineEvent> for TimelineEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// The timeline event type.
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
pub enum TimelineEventType {
|
||||
/// The event is a message-like one and should be displayed as such.
|
||||
MessageLike { value: MessageLikeEventType },
|
||||
/// The event is a state event, and may or may not be displayed in the
|
||||
/// timeline.
|
||||
State { value: StateEventType },
|
||||
}
|
||||
|
||||
impl From<RumaTimelineEventType> for TimelineEventType {
|
||||
fn from(value: RumaTimelineEventType) -> Self {
|
||||
match value {
|
||||
RumaTimelineEventType::Audio => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Audio }
|
||||
}
|
||||
RumaTimelineEventType::File => Self::MessageLike { value: MessageLikeEventType::File },
|
||||
RumaTimelineEventType::Image => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Image }
|
||||
}
|
||||
RumaTimelineEventType::Video => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Video }
|
||||
}
|
||||
RumaTimelineEventType::Voice => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Voice }
|
||||
}
|
||||
RumaTimelineEventType::Emote => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Emote }
|
||||
}
|
||||
RumaTimelineEventType::Encrypted => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Encrypted }
|
||||
}
|
||||
RumaTimelineEventType::RoomMessage => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RoomMessage }
|
||||
}
|
||||
RumaTimelineEventType::CallAnswer => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallAnswer }
|
||||
}
|
||||
RumaTimelineEventType::CallInvite => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallInvite }
|
||||
}
|
||||
RumaTimelineEventType::CallHangup => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallHangup }
|
||||
}
|
||||
RumaTimelineEventType::CallCandidates => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallCandidates }
|
||||
}
|
||||
RumaTimelineEventType::CallNegotiate => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallNegotiate }
|
||||
}
|
||||
RumaTimelineEventType::CallReject => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallReject }
|
||||
}
|
||||
RumaTimelineEventType::CallSdpStreamMetadataChanged => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallSdpStreamMetadataChanged }
|
||||
}
|
||||
RumaTimelineEventType::CallSelectAnswer => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallSelectAnswer }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationReady => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationReady }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationStart => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationStart }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationCancel => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationCancel }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationAccept => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationAccept }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationKey => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationKey }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationMac => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationMac }
|
||||
}
|
||||
RumaTimelineEventType::KeyVerificationDone => {
|
||||
Self::MessageLike { value: MessageLikeEventType::KeyVerificationDone }
|
||||
}
|
||||
RumaTimelineEventType::Location => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Location }
|
||||
}
|
||||
RumaTimelineEventType::Message => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Message }
|
||||
}
|
||||
RumaTimelineEventType::PollStart => {
|
||||
Self::MessageLike { value: MessageLikeEventType::PollStart }
|
||||
}
|
||||
RumaTimelineEventType::UnstablePollStart => {
|
||||
Self::MessageLike { value: MessageLikeEventType::UnstablePollStart }
|
||||
}
|
||||
RumaTimelineEventType::PollResponse => {
|
||||
Self::MessageLike { value: MessageLikeEventType::PollResponse }
|
||||
}
|
||||
RumaTimelineEventType::UnstablePollResponse => {
|
||||
Self::MessageLike { value: MessageLikeEventType::UnstablePollResponse }
|
||||
}
|
||||
RumaTimelineEventType::PollEnd => {
|
||||
Self::MessageLike { value: MessageLikeEventType::PollEnd }
|
||||
}
|
||||
RumaTimelineEventType::UnstablePollEnd => {
|
||||
Self::MessageLike { value: MessageLikeEventType::UnstablePollEnd }
|
||||
}
|
||||
RumaTimelineEventType::Beacon => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Beacon }
|
||||
}
|
||||
RumaTimelineEventType::Reaction => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Reaction }
|
||||
}
|
||||
RumaTimelineEventType::RoomEncrypted => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RoomEncrypted }
|
||||
}
|
||||
RumaTimelineEventType::RoomRedaction => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RoomRedaction }
|
||||
}
|
||||
RumaTimelineEventType::Sticker => {
|
||||
Self::MessageLike { value: MessageLikeEventType::Sticker }
|
||||
}
|
||||
RumaTimelineEventType::CallNotify => {
|
||||
Self::MessageLike { value: MessageLikeEventType::CallNotify }
|
||||
}
|
||||
RumaTimelineEventType::RtcNotification => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RtcNotification }
|
||||
}
|
||||
RumaTimelineEventType::RtcDecline => {
|
||||
Self::MessageLike { value: MessageLikeEventType::RtcDecline }
|
||||
}
|
||||
RumaTimelineEventType::PolicyRuleRoom => {
|
||||
Self::State { value: StateEventType::PolicyRuleRoom }
|
||||
}
|
||||
RumaTimelineEventType::PolicyRuleServer => {
|
||||
Self::State { value: StateEventType::PolicyRuleServer }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
RumaTimelineEventType::RoomCreate => Self::State { value: StateEventType::RoomCreate },
|
||||
RumaTimelineEventType::RoomEncryption => {
|
||||
Self::State { value: StateEventType::RoomEncryption }
|
||||
}
|
||||
RumaTimelineEventType::RoomGuestAccess => {
|
||||
Self::State { value: StateEventType::RoomGuestAccess }
|
||||
}
|
||||
RumaTimelineEventType::RoomHistoryVisibility => {
|
||||
Self::State { value: StateEventType::RoomHistoryVisibility }
|
||||
}
|
||||
RumaTimelineEventType::RoomJoinRules => {
|
||||
Self::State { value: StateEventType::RoomJoinRules }
|
||||
}
|
||||
RumaTimelineEventType::RoomMember => {
|
||||
Self::State { value: StateEventType::RoomMemberEvent }
|
||||
}
|
||||
RumaTimelineEventType::RoomLanguage => {
|
||||
Self::State { value: StateEventType::RoomLanguage }
|
||||
}
|
||||
RumaTimelineEventType::RoomName => Self::State { value: StateEventType::RoomName },
|
||||
RumaTimelineEventType::RoomImagePack => {
|
||||
Self::State { value: StateEventType::RoomImagePack }
|
||||
}
|
||||
RumaTimelineEventType::RoomPinnedEvents => {
|
||||
Self::State { value: StateEventType::RoomPinnedEvents }
|
||||
}
|
||||
RumaTimelineEventType::RoomPowerLevels => {
|
||||
Self::State { value: StateEventType::RoomPowerLevels }
|
||||
}
|
||||
RumaTimelineEventType::RoomServerAcl => {
|
||||
Self::State { value: StateEventType::RoomServerAcl }
|
||||
}
|
||||
RumaTimelineEventType::RoomThirdPartyInvite => {
|
||||
Self::State { value: StateEventType::RoomThirdPartyInvite }
|
||||
}
|
||||
RumaTimelineEventType::RoomTombstone => {
|
||||
Self::State { value: StateEventType::RoomTombstone }
|
||||
}
|
||||
RumaTimelineEventType::RoomTopic => Self::State { value: StateEventType::RoomTopic },
|
||||
RumaTimelineEventType::SpaceChild => Self::State { value: StateEventType::SpaceChild },
|
||||
RumaTimelineEventType::SpaceParent => {
|
||||
Self::State { value: StateEventType::SpaceParent }
|
||||
}
|
||||
RumaTimelineEventType::BeaconInfo => Self::State { value: StateEventType::BeaconInfo },
|
||||
RumaTimelineEventType::CallMember => Self::State { value: StateEventType::CallMember },
|
||||
RumaTimelineEventType::MemberHints => {
|
||||
Self::State { value: StateEventType::MemberHints }
|
||||
}
|
||||
RumaTimelineEventType::_Custom(_) => {
|
||||
Self::State { value: StateEventType::Custom { value: value.to_string() } }
|
||||
}
|
||||
_ => Self::MessageLike { value: MessageLikeEventType::Other(value.to_string()) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `TimelineEventType`, we would need to
|
||||
// In order to reduce the size of `TimelineEventContent`, we would need to
|
||||
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
|
||||
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
|
||||
// `uniffi::Object`, which is a radical change. It would simplify the memory
|
||||
// usage, but it would slow down the performance around the FFI border. Thus,
|
||||
// let's consider this is a false-positive lint in this particular case.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TimelineEventType {
|
||||
pub enum TimelineEventContent {
|
||||
MessageLike { content: MessageLikeEventContent },
|
||||
State { content: StateEventContent },
|
||||
}
|
||||
@@ -170,6 +385,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,
|
||||
@@ -212,6 +429,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,
|
||||
@@ -254,7 +472,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 {
|
||||
@@ -300,9 +518,11 @@ where
|
||||
Ok(original_content)
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
pub enum StateEventType {
|
||||
BeaconInfo,
|
||||
CallMember,
|
||||
MemberHints,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
@@ -313,8 +533,10 @@ pub enum StateEventType {
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomImagePack,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomLanguage,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
@@ -324,12 +546,15 @@ pub enum StateEventType {
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::BeaconInfo => Self::BeaconInfo,
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::MemberHints => Self::MemberHints,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
@@ -340,7 +565,9 @@ impl From<StateEventType> for ruma::events::StateEventType {
|
||||
StateEventType::RoomEncryption => Self::RoomEncryption,
|
||||
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
|
||||
StateEventType::RoomImagePack => Self::RoomImagePack,
|
||||
StateEventType::RoomJoinRules => Self::RoomJoinRules,
|
||||
StateEventType::RoomLanguage => Self::RoomLanguage,
|
||||
StateEventType::RoomMemberEvent => Self::RoomMember,
|
||||
StateEventType::RoomName => Self::RoomName,
|
||||
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
|
||||
@@ -351,17 +578,28 @@ impl From<StateEventType> for ruma::events::StateEventType {
|
||||
StateEventType::RoomTopic => Self::RoomTopic,
|
||||
StateEventType::SpaceChild => Self::SpaceChild,
|
||||
StateEventType::SpaceParent => Self::SpaceParent,
|
||||
StateEventType::Custom { value } => value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
pub enum MessageLikeEventType {
|
||||
Audio,
|
||||
Beacon,
|
||||
CallAnswer,
|
||||
CallCandidates,
|
||||
CallHangup,
|
||||
CallInvite,
|
||||
RtcNotification,
|
||||
CallNegotiate,
|
||||
CallNotify,
|
||||
CallReject,
|
||||
CallSdpStreamMetadataChanged,
|
||||
CallSelectAnswer,
|
||||
Emote,
|
||||
Encrypted,
|
||||
File,
|
||||
Image,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationDone,
|
||||
@@ -369,6 +607,8 @@ pub enum MessageLikeEventType {
|
||||
KeyVerificationMac,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
Location,
|
||||
Message,
|
||||
PollEnd,
|
||||
PollResponse,
|
||||
PollStart,
|
||||
@@ -376,21 +616,39 @@ pub enum MessageLikeEventType {
|
||||
RoomEncrypted,
|
||||
RoomMessage,
|
||||
RoomRedaction,
|
||||
RtcDecline,
|
||||
RtcNotification,
|
||||
Sticker,
|
||||
UnstablePollEnd,
|
||||
UnstablePollResponse,
|
||||
UnstablePollStart,
|
||||
Video,
|
||||
Voice,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
fn from(val: MessageLikeEventType) -> Self {
|
||||
match val {
|
||||
MessageLikeEventType::Audio => Self::Audio,
|
||||
MessageLikeEventType::File => Self::File,
|
||||
MessageLikeEventType::Image => Self::Image,
|
||||
MessageLikeEventType::Video => Self::Video,
|
||||
MessageLikeEventType::Voice => Self::Voice,
|
||||
MessageLikeEventType::Beacon => Self::Beacon,
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::RtcNotification => Self::RtcNotification,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallNegotiate => Self::CallNegotiate,
|
||||
MessageLikeEventType::CallNotify => Self::CallNotify,
|
||||
MessageLikeEventType::CallReject => Self::CallReject,
|
||||
MessageLikeEventType::CallSdpStreamMetadataChanged => {
|
||||
Self::CallSdpStreamMetadataChanged
|
||||
}
|
||||
MessageLikeEventType::CallSelectAnswer => Self::CallSelectAnswer,
|
||||
MessageLikeEventType::Emote => Self::Emote,
|
||||
MessageLikeEventType::Encrypted => Self::Encrypted,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
|
||||
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
|
||||
@@ -398,14 +656,18 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
|
||||
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
|
||||
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
|
||||
MessageLikeEventType::Location => Self::Location,
|
||||
MessageLikeEventType::Message => Self::Message,
|
||||
MessageLikeEventType::Reaction => Self::Reaction,
|
||||
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
|
||||
MessageLikeEventType::RoomMessage => Self::RoomMessage,
|
||||
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
|
||||
MessageLikeEventType::RtcDecline => Self::RtcDecline,
|
||||
MessageLikeEventType::Sticker => Self::Sticker,
|
||||
MessageLikeEventType::PollEnd => Self::PollEnd,
|
||||
MessageLikeEventType::PollResponse => Self::PollResponse,
|
||||
MessageLikeEventType::PollStart => Self::PollStart,
|
||||
MessageLikeEventType::RtcNotification => Self::RtcNotification,
|
||||
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
|
||||
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
|
||||
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -30,7 +30,6 @@ mod store;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod tracing;
|
||||
mod utd;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
// 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;
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
|
||||
@@ -1,8 +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, sync::Arc};
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
|
||||
NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
|
||||
RawNotificationEvent as SdkRawNotificationEvent,
|
||||
};
|
||||
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
|
||||
|
||||
@@ -43,6 +58,9 @@ pub struct NotificationRoomInfo {
|
||||
pub struct NotificationItem {
|
||||
pub event: NotificationEvent,
|
||||
|
||||
/// The raw JSON of the underlying event.
|
||||
pub raw_event: String,
|
||||
|
||||
pub sender_info: NotificationSenderInfo,
|
||||
pub room_info: NotificationRoomInfo,
|
||||
|
||||
@@ -67,8 +85,15 @@ impl NotificationItem {
|
||||
NotificationEvent::Invite { sender: event.sender.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
let raw_event = match &item.raw_event {
|
||||
SdkRawNotificationEvent::Timeline(raw) => raw.json().get().to_owned(),
|
||||
SdkRawNotificationEvent::Invite(raw) => raw.json().get().to_owned(),
|
||||
};
|
||||
|
||||
Self {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info: NotificationSenderInfo {
|
||||
display_name: item.sender_display_name,
|
||||
avatar_url: item.sender_avatar_url,
|
||||
@@ -106,6 +131,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 {
|
||||
@@ -116,6 +143,7 @@ impl From<SdkNotificationStatus> for NotificationStatus {
|
||||
}
|
||||
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
|
||||
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
|
||||
SdkNotificationStatus::EventRedacted => NotificationStatus::EventRedacted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, RwLock};
|
||||
|
||||
use matrix_sdk::{
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::error::Error;
|
||||
|
||||
use jni::{
|
||||
errors::JniError,
|
||||
sys::{jint, jsize, JavaVM},
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type JniGetCreatedJavaVms =
|
||||
unsafe extern "system" fn(_: *mut *mut JavaVM, _: jsize, _: *mut jsize) -> jint;
|
||||
const JNI_GET_JAVA_VMS_NAME: &[u8] = b"JNI_GetCreatedJavaVMs";
|
||||
|
||||
fn get_java_vm() -> Result<jni::JavaVM, Box<dyn Error>> {
|
||||
// Use libloading to avoid having to create and expose a JNI function and call
|
||||
// it from the Android side.
|
||||
let library = libloading::os::unix::Library::this();
|
||||
let get_created_java_vms: JniGetCreatedJavaVms =
|
||||
unsafe { *library.get(JNI_GET_JAVA_VMS_NAME)? };
|
||||
|
||||
let mut java_vms: [*mut JavaVM; 1] = [std::ptr::null_mut() as *mut JavaVM];
|
||||
let mut vm_count: i32 = 0;
|
||||
|
||||
let ok = unsafe { get_created_java_vms(java_vms.as_mut_ptr(), 1, &mut vm_count) };
|
||||
if ok != jni::sys::JNI_OK {
|
||||
return Err("Failed to get JavaVM".into());
|
||||
}
|
||||
if vm_count != 1 {
|
||||
return Err(format!("Invalid JavaVM count: {vm_count}").into());
|
||||
}
|
||||
|
||||
let jvm = unsafe { jni::JavaVM::from_raw(java_vms[0]) }?;
|
||||
Ok(jvm)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
+108
-31
@@ -1,10 +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.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
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;
|
||||
@@ -23,9 +38,25 @@ use tracing_subscriber::{
|
||||
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;
|
||||
|
||||
use self::tracing::LogLevel;
|
||||
#[cfg(feature = "sentry")]
|
||||
use crate::tracing::BRIDGE_SPAN_NAME;
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
use self::tracing::BRIDGE_SPAN_NAME;
|
||||
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
@@ -153,7 +184,7 @@ type ReloadHandle = Handle<
|
||||
Layered<EnvFilter, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
SizeAndDateRollingWriter,
|
||||
>,
|
||||
Layered<EnvFilter, Registry>,
|
||||
>;
|
||||
@@ -218,21 +249,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 +281,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)]
|
||||
@@ -289,6 +333,7 @@ enum LogTarget {
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkLatestEvents,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkSendQueue,
|
||||
MatrixSdkSlidingSync,
|
||||
@@ -319,6 +364,7 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
|
||||
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
|
||||
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
|
||||
LogTarget::MatrixSdkLatestEvents => "matrix_sdk::latest_events",
|
||||
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
@@ -341,6 +387,7 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkSendQueue, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkLatestEvents, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonCrossProcessLock, LogLevel::Warn),
|
||||
@@ -372,6 +419,8 @@ pub enum TraceLogPacks {
|
||||
NotificationClient,
|
||||
/// Enables all the logs relevant to sync profiling.
|
||||
SyncProfiling,
|
||||
/// Enables all the logs relevant to the latest events.
|
||||
LatestEvents,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
@@ -399,6 +448,11 @@ impl TraceLogPacks {
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
TraceLogPacks::LatestEvents => &[
|
||||
LogTarget::MatrixSdkLatestEvents,
|
||||
LogTarget::MatrixSdkSendQueue,
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -441,9 +495,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 {
|
||||
@@ -464,10 +526,10 @@ 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
|
||||
@@ -483,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.
|
||||
@@ -548,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
|
||||
}
|
||||
@@ -618,6 +685,9 @@ pub fn init_platform(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
android_platform::init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -675,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
|
||||
}));
|
||||
}
|
||||
@@ -725,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);
|
||||
@@ -746,6 +820,7 @@ mod tests {
|
||||
matrix_sdk_ui::timeline=info,
|
||||
matrix_sdk::send_queue=info,
|
||||
matrix_sdk::event_cache=info,
|
||||
matrix_sdk::latest_events=info,
|
||||
matrix_sdk_base::event_cache=info,
|
||||
matrix_sdk_sqlite::event_cache_store=info,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
@@ -770,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);
|
||||
@@ -791,6 +866,7 @@ mod tests {
|
||||
matrix_sdk_ui::timeline=trace,
|
||||
matrix_sdk::send_queue=trace,
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk::latest_events=trace,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
@@ -816,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);
|
||||
@@ -837,6 +913,7 @@ mod tests {
|
||||
matrix_sdk_ui::timeline=info,
|
||||
matrix_sdk::send_queue=trace,
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk::latest_events=info,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
@@ -0,0 +1,923 @@
|
||||
// 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 {
|
||||
if let Some(state) = state.as_ref() {
|
||||
if !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) {
|
||||
if 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 {
|
||||
if 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);
|
||||
}
|
||||
}
|
||||
+18
-5
@@ -1,11 +1,24 @@
|
||||
// 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_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
@@ -7,6 +21,7 @@ use matrix_sdk::authentication::oauth::{
|
||||
},
|
||||
OAuth,
|
||||
};
|
||||
use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent};
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::{
|
||||
@@ -236,17 +251,48 @@ impl QrCodeData {
|
||||
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
|
||||
}
|
||||
|
||||
/// Serialize the [`QrCodeData`] into a byte vector for encoding as a QR
|
||||
/// code.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
/// The server name contained within the scanned QR code data.
|
||||
///
|
||||
/// Note: This value is only present when scanning a QR code the belongs to
|
||||
/// Note: This value is only present when scanning a QR code that belongs to
|
||||
/// a logged in client. The mode where the new client shows the QR code
|
||||
/// will return `None`.
|
||||
pub fn server_name(&self) -> Option<String> {
|
||||
match &self.inner.mode_data {
|
||||
qrcode::QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
|
||||
qrcode::QrCodeModeData::Login => None,
|
||||
match &self.inner.intent_data() {
|
||||
qr_login::QrCodeIntentData::Msc4108 { data, .. } => match data {
|
||||
qrcode::Msc4108IntentData::Login => None,
|
||||
qrcode::Msc4108IntentData::Reciprocate { server_name } => {
|
||||
Some(server_name.to_owned())
|
||||
}
|
||||
},
|
||||
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`].
|
||||
@@ -286,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 {
|
||||
@@ -317,6 +365,9 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::UnsupportedQrCodeType => {
|
||||
HumanQrLoginError::UnsupportedQrCodeType
|
||||
}
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
@@ -371,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())
|
||||
}
|
||||
@@ -395,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,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, fs, path::PathBuf, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@@ -6,7 +20,6 @@ use matrix_sdk::{
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
@@ -53,8 +66,8 @@ use crate::{
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
AbstractProgress, EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
|
||||
UploadSource,
|
||||
threads::{ListThreadsOptions, ThreadList, ThreadSubscription},
|
||||
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
|
||||
},
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
@@ -258,10 +271,10 @@ impl Room {
|
||||
});
|
||||
}
|
||||
|
||||
TimelineFilter::EventTypeFilter { filter: event_type_filter } => {
|
||||
TimelineFilter::EventFilter { filter: event_filter } => {
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
// Always perform the default filter first
|
||||
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
|
||||
default_event_filter(event, room_version_id) && event_filter.filter(event)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -303,12 +316,8 @@ impl Room {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<EventTimelineItem> {
|
||||
self.inner.latest_event_item().await.map(Into::into)
|
||||
}
|
||||
|
||||
async fn new_latest_event(&self) -> LatestEventValue {
|
||||
self.inner.new_latest_event().await.into()
|
||||
async fn latest_event(&self) -> LatestEventValue {
|
||||
self.inner.latest_event().await.into()
|
||||
}
|
||||
|
||||
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
|
||||
@@ -348,6 +357,14 @@ impl Room {
|
||||
Ok(avatar_url_string)
|
||||
}
|
||||
|
||||
pub async fn set_own_member_display_name(
|
||||
&self,
|
||||
display_name: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.set_own_member_display_name(display_name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
///
|
||||
/// Returns:
|
||||
@@ -465,18 +482,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(())
|
||||
}
|
||||
@@ -1123,7 +1131,7 @@ impl Room {
|
||||
geo_uri: event.last_location.location.uri.clone().to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset: None,
|
||||
asset: event.beacon_info.as_ref().map(|b| b.asset.type_.clone()).into(),
|
||||
};
|
||||
|
||||
let Some(beacon_info) = event.beacon_info else {
|
||||
@@ -1233,6 +1241,21 @@ impl Room {
|
||||
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
|
||||
}
|
||||
|
||||
/// Retrieve a list of all the threads for the current 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
|
||||
/// [`ThreadList::prev_batch_token`]. This token can be passed to the next
|
||||
/// call to this function, through the `from` field of
|
||||
/// [`ListThreadsOptions`].
|
||||
pub async fn load_thread_list(
|
||||
&self,
|
||||
opts: ListThreadsOptions,
|
||||
) -> Result<ThreadList, ClientError> {
|
||||
let thread_list = self.inner.load_thread_list(opts.into()).await?;
|
||||
Ok(thread_list.into())
|
||||
}
|
||||
|
||||
/// Either loads the event associated with the `event_id` from the event
|
||||
/// cache or fetches it from the homeserver.
|
||||
pub async fn load_or_fetch_event(
|
||||
@@ -1250,14 +1273,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,
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
|
||||
@@ -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;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -29,6 +43,10 @@ impl RoomPowerLevels {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
|
||||
fn events(&self) -> HashMap<crate::event::TimelineEventType, i64> {
|
||||
self.inner.events.iter().map(|(key, value)| (key.clone().into(), (*value).into())).collect()
|
||||
}
|
||||
|
||||
/// Gets a map with the `UserId` of users with power levels other than `0`
|
||||
/// and their power level.
|
||||
pub fn user_power_levels(&self) -> HashMap<String, i64> {
|
||||
|
||||
@@ -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::{
|
||||
@@ -11,8 +25,32 @@ use crate::{
|
||||
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
|
||||
},
|
||||
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,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.
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
@@ -15,10 +29,10 @@ 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_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,
|
||||
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,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
@@ -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.
|
||||
@@ -472,6 +474,7 @@ impl RoomListDynamicEntriesController {
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Identifiers { identifiers: Vec<String> },
|
||||
NonSpace,
|
||||
Space,
|
||||
NonLeft,
|
||||
@@ -482,6 +485,7 @@ pub enum RoomListEntriesDynamicFilterKind {
|
||||
Favourite,
|
||||
LowPriority,
|
||||
NonLowPriority,
|
||||
NonFavorite,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
None,
|
||||
@@ -516,6 +520,9 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
Kind::Any { filters } => Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::Identifiers { identifiers } => Box::new(new_filter_identifiers(
|
||||
identifiers.into_iter().map(|id| RoomId::parse(id).unwrap()).collect(),
|
||||
)),
|
||||
Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
|
||||
Kind::Space => Box::new(new_filter_space()),
|
||||
Kind::NonLeft => Box::new(new_filter_non_left()),
|
||||
@@ -524,6 +531,7 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
Kind::Favourite => Box::new(new_filter_favourite()),
|
||||
Kind::LowPriority => Box::new(new_filter_low_priority()),
|
||||
Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
|
||||
Kind::NonFavorite => Box::new(new_filter_not(Box::new(new_filter_favourite()))),
|
||||
Kind::Invite => Box::new(new_filter_invite()),
|
||||
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
|
||||
Kind::None => Box::new(new_filter_none()),
|
||||
|
||||
@@ -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 anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
|
||||
|
||||
@@ -56,7 +56,9 @@ use ruma::{
|
||||
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::{
|
||||
@@ -470,11 +472,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 +508,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 +566,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 +605,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 +642,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 +677,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 +923,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 +938,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, RwLock};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
@@ -264,7 +278,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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
|
||||
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
|
||||
SpaceService as UISpaceService,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
@@ -55,17 +56,17 @@ impl SpaceService {
|
||||
/// Returns a list of all the top-level joined spaces. It will eagerly
|
||||
/// compute the latest version and also notify subscribers if there were
|
||||
/// any changes.
|
||||
pub async fn joined_spaces(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.joined_spaces().await.into_iter().map(Into::into).collect()
|
||||
pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.top_level_joined_spaces().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Subscribes to updates on the joined spaces list. If space rooms are
|
||||
/// joined or left, the stream will yield diffs that reflect the changes.
|
||||
pub async fn subscribe_to_joined_spaces(
|
||||
pub async fn subscribe_to_top_level_joined_spaces(
|
||||
&self,
|
||||
listener: Box<dyn SpaceServiceJoinedSpacesListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_joined_spaces().await;
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_top_level_joined_spaces().await;
|
||||
|
||||
listener.on_update(vec![SpaceListUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
@@ -78,11 +79,40 @@ impl SpaceService {
|
||||
})))
|
||||
}
|
||||
|
||||
/// Space filters provide access to a custom subset of the space graph that
|
||||
/// can be used in tandem with the [`crate::RoomListService`] to narrow
|
||||
/// down the presented rooms.
|
||||
///
|
||||
/// They are limited to the first 2 levels of the graph, with the first
|
||||
/// level only containing direct descendants while the second holds the rest
|
||||
/// of them recursively.
|
||||
pub async fn space_filters(&self) -> Vec<SpaceFilter> {
|
||||
self.inner.space_filters().await.into_iter().map(|s| s.into()).collect()
|
||||
}
|
||||
|
||||
/// Subscribe to changes or updates to the space filters.
|
||||
pub async fn subscribe_to_space_filters(
|
||||
&self,
|
||||
listener: Box<dyn SpaceServiceSpaceFiltersListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe_to_space_filters().await;
|
||||
|
||||
listener.on_update(vec![SpaceFilterUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns a flattened list containing all the spaces where the user has
|
||||
/// permission to send `m.space.child` state events.
|
||||
///
|
||||
/// Note: Unlike [`Self::joined_spaces()`], this method does not recompute
|
||||
/// the space graph, nor does it notify subscribers about changes.
|
||||
/// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
|
||||
/// recompute the space graph, nor does it notify subscribers about changes.
|
||||
pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
|
||||
self.inner.editable_spaces().await.into_iter().map(Into::into).collect()
|
||||
}
|
||||
@@ -108,6 +138,13 @@ impl SpaceService {
|
||||
Ok(parents.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
/// Returns the corresponding `SpaceRoom` for the given room ID, or `None`
|
||||
/// if it isn't known.
|
||||
pub async fn get_space_room(&self, room_id: String) -> Result<Option<SpaceRoom>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id.as_str())?;
|
||||
Ok(self.inner.get_space_room(&room_id).await.map(Into::into))
|
||||
}
|
||||
|
||||
pub async fn add_child_to_space(
|
||||
&self,
|
||||
child_id: String,
|
||||
@@ -149,7 +186,7 @@ impl SpaceService {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `SpaceRoomList`represents a paginated list of direct rooms
|
||||
/// The `SpaceRoomList` represents a paginated list of direct rooms
|
||||
/// that belong to a particular space.
|
||||
///
|
||||
/// It can be used to paginate through the list (and have live updates on the
|
||||
@@ -242,6 +279,18 @@ impl SpaceRoomList {
|
||||
pub async fn paginate(&self) -> Result<(), ClientError> {
|
||||
self.inner.paginate().await.map_err(ClientError::from)
|
||||
}
|
||||
|
||||
/// Clears the room list back to its initial state so that any new changes
|
||||
/// to the hierarchy will be included the next time [`Self::paginate`] is
|
||||
/// called.
|
||||
///
|
||||
/// This is useful when you've added or removed children from the space as
|
||||
/// the list is based on a cached state that lives server-side, meaning
|
||||
/// the /hierarchy request needs to be restarted from scratch to pick up
|
||||
/// the changes.
|
||||
pub async fn reset(&self) {
|
||||
self.inner.reset().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -264,6 +313,11 @@ pub trait SpaceServiceJoinedSpacesListener: SendOutsideWasm + SyncOutsideWasm +
|
||||
fn on_update(&self, room_updates: Vec<SpaceListUpdate>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SpaceServiceSpaceFiltersListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, filter_updates: Vec<SpaceFilterUpdate>);
|
||||
}
|
||||
|
||||
/// Structure representing a room in a space and aggregated information
|
||||
/// relevant to the UI layer.
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -371,6 +425,47 @@ impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum SpaceFilterUpdate {
|
||||
Append { values: Vec<SpaceFilter> },
|
||||
Clear,
|
||||
PushFront { value: SpaceFilter },
|
||||
PushBack { value: SpaceFilter },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: SpaceFilter },
|
||||
Set { index: u32, value: SpaceFilter },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<SpaceFilter> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<UISpaceFilter>> for SpaceFilterUpdate {
|
||||
fn from(diff: VectorDiff<UISpaceFilter>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `LeaveSpaceHandle` processes rooms to be left in the order they were
|
||||
/// provided by the [`SpaceService`] and annotates them with extra data to
|
||||
/// inform the leave process e.g. if the current user is the last room admin.
|
||||
@@ -413,14 +508,43 @@ impl From<UILeaveSpaceHandle> for LeaveSpaceHandle {
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LeaveSpaceRoom {
|
||||
/// The underlying [`SpaceRoom`]
|
||||
space_room: SpaceRoom,
|
||||
/// Whether the user is the last admin in the room. This helps clients
|
||||
pub space_room: SpaceRoom,
|
||||
/// Whether the user is the last owner in the room. This helps clients
|
||||
/// better inform the user about the consequences of leaving the room.
|
||||
is_last_admin: bool,
|
||||
pub is_last_owner: bool,
|
||||
/// If the room creators have infinite PL.
|
||||
pub are_creators_privileged: bool,
|
||||
}
|
||||
|
||||
impl From<UILeaveSpaceRoom> for LeaveSpaceRoom {
|
||||
fn from(room: UILeaveSpaceRoom) -> Self {
|
||||
LeaveSpaceRoom { space_room: room.space_room.into(), is_last_admin: room.is_last_admin }
|
||||
LeaveSpaceRoom {
|
||||
space_room: room.space_room.into(),
|
||||
is_last_owner: room.is_last_owner,
|
||||
are_creators_privileged: room.are_creators_privileged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SpaceFilter {
|
||||
/// The underlying [`SpaceRoom`]
|
||||
space_room: SpaceRoom,
|
||||
/// The level of the space filter in the tree/hierarchy.
|
||||
/// At this point in time the filters are limited to the first 2 levels.
|
||||
level: u8,
|
||||
/// The room identifiers of the descendants of this space.
|
||||
/// For top level spaces (level 0) these will be direct descendants while
|
||||
/// for first level spaces they will be all other descendants, recursively.
|
||||
descendants: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<UISpaceFilter> for SpaceFilter {
|
||||
fn from(filter: UISpaceFilter) -> Self {
|
||||
SpaceFilter {
|
||||
space_room: filter.space_room.into(),
|
||||
level: filter.level,
|
||||
descendants: filter.descendants.into_iter().map(|id| id.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.
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,8 +1,22 @@
|
||||
// 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_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter,
|
||||
TimelineReadReceiptTracking,
|
||||
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
|
||||
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
|
||||
};
|
||||
use ruma::{
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
@@ -15,31 +29,50 @@ use crate::{
|
||||
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
|
||||
};
|
||||
|
||||
/// A timeline filter that includes or excludes events based on their type or
|
||||
/// content.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
pub struct TimelineEventFilter {
|
||||
inner: InnerTimelineEventFilter,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
impl TimelineEventFilter {
|
||||
#[uniffi::constructor]
|
||||
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
|
||||
pub fn include(conditions: Vec<FilterTimelineEventCondition>) -> Arc<Self> {
|
||||
let conditions: Vec<TimelineEventCondition> =
|
||||
conditions.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventFilter::Include(conditions) })
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let event_types: Vec<TimelineEventType> =
|
||||
event_types.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
|
||||
pub fn include_event_types(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let conditions = event_types
|
||||
.iter()
|
||||
.map(|t| TimelineEventCondition::EventType(t.clone().into()))
|
||||
.collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventFilter::Include(conditions) })
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn exclude(conditions: Vec<FilterTimelineEventCondition>) -> Arc<Self> {
|
||||
let conditions: Vec<TimelineEventCondition> =
|
||||
conditions.iter().map(|t| t.clone().into()).collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventFilter::Exclude(conditions) })
|
||||
}
|
||||
|
||||
#[uniffi::constructor]
|
||||
pub fn exclude_event_types(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
|
||||
let conditions = event_types
|
||||
.iter()
|
||||
.map(|t| TimelineEventCondition::EventType(t.clone().into()))
|
||||
.collect();
|
||||
Arc::new(Self { inner: InnerTimelineEventFilter::Exclude(conditions) })
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineEventTypeFilter {
|
||||
/// Filters an [`event`] to decide whether it should be part of the timeline
|
||||
/// based on [`AnySyncTimelineEvent::event_type()`].
|
||||
impl TimelineEventFilter {
|
||||
/// Filters an `event` to decide whether it should be part of the timeline.
|
||||
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
|
||||
self.inner.filter(event)
|
||||
}
|
||||
@@ -64,6 +97,31 @@ impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
}
|
||||
}
|
||||
|
||||
/// A condition that matches on an event's type or content.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum FilterTimelineEventCondition {
|
||||
/// The event has the specified event type.
|
||||
EventType { event_type: FilterTimelineEventType },
|
||||
/// The event is an `m.room.member` event that represents a membership
|
||||
/// change (join, leave, etc.).
|
||||
MembershipChange,
|
||||
/// The event is an `m.room.member` event that represents a profile
|
||||
/// change (displayname or avatar URL).
|
||||
ProfileChange,
|
||||
}
|
||||
|
||||
impl From<FilterTimelineEventCondition> for TimelineEventCondition {
|
||||
fn from(value: FilterTimelineEventCondition) -> Self {
|
||||
match value {
|
||||
FilterTimelineEventCondition::EventType { event_type } => {
|
||||
Self::EventType(event_type.into())
|
||||
}
|
||||
FilterTimelineEventCondition::MembershipChange => Self::MembershipChange,
|
||||
FilterTimelineEventCondition::ProfileChange => Self::ProfileChange,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineFocus {
|
||||
Live {
|
||||
@@ -76,17 +134,14 @@ pub enum TimelineFocus {
|
||||
event_id: String,
|
||||
/// The number of context events to load around the focused event.
|
||||
num_context_events: u16,
|
||||
/// Whether to hide in-thread replies from the live timeline.
|
||||
hide_threaded_events: bool,
|
||||
/// How to handle threaded events.
|
||||
thread_mode: TimelineEventFocusThreadMode,
|
||||
},
|
||||
Thread {
|
||||
/// The thread root event ID to focus on.
|
||||
root_event_id: String,
|
||||
},
|
||||
PinnedEvents {
|
||||
max_events_to_load: u16,
|
||||
max_concurrent_requests: u16,
|
||||
},
|
||||
PinnedEvents,
|
||||
}
|
||||
|
||||
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
@@ -97,18 +152,14 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
|
||||
match value {
|
||||
TimelineFocus::Live { hide_threaded_events } => Ok(Self::Live { hide_threaded_events }),
|
||||
TimelineFocus::Event { event_id, num_context_events, hide_threaded_events } => {
|
||||
TimelineFocus::Event { event_id, num_context_events, thread_mode } => {
|
||||
let parsed_event_id =
|
||||
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
|
||||
event_id: event_id.clone(),
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Self::Event {
|
||||
target: parsed_event_id,
|
||||
num_context_events,
|
||||
hide_threaded_events,
|
||||
})
|
||||
Ok(Self::Event { target: parsed_event_id, num_context_events, thread_mode })
|
||||
}
|
||||
TimelineFocus::Thread { root_event_id } => {
|
||||
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
|
||||
@@ -120,9 +171,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
|
||||
Ok(Self::Thread { root_event_id: parsed_root_event_id })
|
||||
}
|
||||
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
|
||||
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
}
|
||||
TimelineFocus::PinnedEvents => Ok(Self::PinnedEvents),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,8 +203,8 @@ pub enum TimelineFilter {
|
||||
/// appear in the timeline.
|
||||
types: Vec<RoomMessageEventMessageType>,
|
||||
},
|
||||
/// Show only events which match this filter.
|
||||
EventTypeFilter { filter: Arc<TimelineEventTypeFilter> },
|
||||
/// Show only events which match this event filter.
|
||||
EventFilter { filter: Arc<TimelineEventFilter> },
|
||||
}
|
||||
|
||||
/// Various options used to configure the timeline's behavior.
|
||||
|
||||
@@ -17,10 +17,13 @@ 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,
|
||||
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, StateEventContentChange,
|
||||
};
|
||||
|
||||
use crate::{client::JoinRule, timeline::msg_like::MsgLikeContent, utils::Timestamp};
|
||||
use crate::{
|
||||
client::JoinRule, event::TimelineEventType, ruma::AssetType,
|
||||
timeline::msg_like::MsgLikeContent, utils::Timestamp,
|
||||
};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
@@ -41,7 +44,7 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
FullStateEventContent::Original { content, .. } => content.reason.clone(),
|
||||
StateEventContentChange::Original { content, .. } => content.reason.clone(),
|
||||
_ => None,
|
||||
};
|
||||
TimelineItemContent::RoomMembership {
|
||||
@@ -130,8 +133,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,
|
||||
@@ -242,36 +245,120 @@ impl From<matrix_sdk_ui::timeline::MembershipChange> for MembershipChange {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct PowerLevelChanges {
|
||||
ban: i64,
|
||||
kick: i64,
|
||||
events_default: i64,
|
||||
invite: i64,
|
||||
redact: i64,
|
||||
state_default: i64,
|
||||
users_default: i64,
|
||||
notifications: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
// Added because the RoomPowerLevels variant is quite large.
|
||||
// This is the same issue than for TimelineItemContent.
|
||||
pub enum OtherState {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar { url: Option<String> },
|
||||
RoomAvatar {
|
||||
url: Option<String>,
|
||||
},
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate { federate: Option<bool> },
|
||||
RoomCreate {
|
||||
federate: bool,
|
||||
},
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility { history_visibility: Option<HistoryVisibility> },
|
||||
RoomJoinRules { join_rule: Option<JoinRule> },
|
||||
RoomName { name: Option<String> },
|
||||
RoomPinnedEvents { change: RoomPinnedEventsChange },
|
||||
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
|
||||
RoomHistoryVisibility {
|
||||
history_visibility: HistoryVisibility,
|
||||
},
|
||||
RoomJoinRules {
|
||||
join_rule: Option<JoinRule>,
|
||||
},
|
||||
RoomName {
|
||||
name: Option<String>,
|
||||
},
|
||||
RoomPinnedEvents {
|
||||
change: RoomPinnedEventsChange,
|
||||
},
|
||||
RoomPowerLevels {
|
||||
events: HashMap<TimelineEventType, i64>,
|
||||
previous_events: Option<HashMap<TimelineEventType, i64>>,
|
||||
users: HashMap<String, i64>,
|
||||
previous_users: Option<HashMap<String, i64>>,
|
||||
thresholds: PowerLevelChanges,
|
||||
previous_thresholds: Option<PowerLevelChanges>,
|
||||
},
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite { display_name: Option<String> },
|
||||
RoomThirdPartyInvite {
|
||||
display_name: Option<String>,
|
||||
},
|
||||
RoomTombstone,
|
||||
RoomTopic { topic: Option<String> },
|
||||
RoomTopic {
|
||||
topic: Option<String>,
|
||||
},
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
Custom { event_type: String },
|
||||
Custom {
|
||||
event_type: String,
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -289,8 +376,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 }
|
||||
}
|
||||
@@ -298,25 +385,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 }
|
||||
}
|
||||
@@ -328,20 +412,56 @@ 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 {
|
||||
users: power_level_user_changes(content, prev_content)
|
||||
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()
|
||||
.map(|(k, &v)| (k.clone().into(), v.into()))
|
||||
.collect(),
|
||||
previous_events: prev_content.as_ref().map(|prev_content| {
|
||||
prev_content
|
||||
.events
|
||||
.iter()
|
||||
.map(|(k, &v)| (k.clone().into(), v.into()))
|
||||
.collect()
|
||||
}),
|
||||
thresholds: PowerLevelChanges {
|
||||
ban: content.ban.into(),
|
||||
kick: content.kick.into(),
|
||||
events_default: content.events_default.into(),
|
||||
invite: content.invite.into(),
|
||||
redact: content.redact.into(),
|
||||
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(),
|
||||
kick: prev_content.kick.into(),
|
||||
events_default: prev_content.events_default.into(),
|
||||
invite: prev_content.invite.into(),
|
||||
redact: prev_content.redact.into(),
|
||||
state_default: prev_content.state_default.into(),
|
||||
users_default: prev_content.users_default.into(),
|
||||
notifications: prev_content.notifications.room.into(),
|
||||
}
|
||||
}),
|
||||
users: power_level_user_changes(&content, &prev_content)
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect(),
|
||||
previous: prev_content.as_ref().map(|prev_content| {
|
||||
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 { users: Default::default(), previous: None }
|
||||
}
|
||||
},
|
||||
}
|
||||
Content::RoomServerAcl(_) => Self::RoomServerAcl,
|
||||
Content::RoomThirdPartyInvite(c) => {
|
||||
let display_name = match c {
|
||||
|
||||
@@ -21,8 +21,7 @@ use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
event_cache::RoomPaginationStatus,
|
||||
event_cache::PaginationStatus,
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
@@ -31,8 +30,10 @@ use matrix_sdk_common::{
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, AttachmentConfig, AttachmentSource, EventItemOrigin,
|
||||
LatestEventValue as UiLatestEventValue, MediaUploadProgress as SdkMediaUploadProgress, Profile,
|
||||
TimelineDetails, TimelineUniqueId as SdkTimelineUniqueId,
|
||||
LatestEventValue as UiLatestEventValue, LatestEventValueLocalState,
|
||||
MediaUploadProgress as SdkMediaUploadProgress, Profile, TimelineDetails,
|
||||
TimelineEventShieldState as SdkShieldState, TimelineEventShieldStateCode,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
@@ -60,8 +61,7 @@ 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,
|
||||
@@ -78,6 +78,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};
|
||||
@@ -722,7 +723,7 @@ impl Timeline {
|
||||
/// pinned.
|
||||
async fn pin_event(&self, event_id: String) -> Result<bool, ClientError> {
|
||||
let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
|
||||
self.inner.pin_event(&event_id).await.map_err(ClientError::from)
|
||||
self.inner.room().pin_event(&event_id).await.map_err(ClientError::from)
|
||||
}
|
||||
|
||||
/// Adds a new pinned event by sending an updated `m.room.pinned_events`
|
||||
@@ -732,7 +733,7 @@ impl Timeline {
|
||||
/// pinned
|
||||
async fn unpin_event(&self, event_id: String) -> Result<bool, ClientError> {
|
||||
let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
|
||||
self.inner.unpin_event(&event_id).await.map_err(ClientError::from)
|
||||
self.inner.room().unpin_event(&event_id).await.map_err(ClientError::from)
|
||||
}
|
||||
|
||||
pub fn create_message_content(
|
||||
@@ -820,7 +821,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)]
|
||||
@@ -979,12 +980,12 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
|
||||
/// authenticity properties.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum ShieldState {
|
||||
/// A red shield with a tooltip containing the associated message should be
|
||||
/// presented.
|
||||
Red { code: ShieldStateCode, message: String },
|
||||
/// A grey shield with a tooltip containing the associated message should be
|
||||
/// presented.
|
||||
Grey { code: ShieldStateCode, message: String },
|
||||
/// A red shield with a tooltip containing a message appropriate to the
|
||||
/// associated code should be presented.
|
||||
Red { code: TimelineEventShieldStateCode },
|
||||
/// A grey shield with a tooltip containing a message appropriate to the
|
||||
/// associated code should be presented.
|
||||
Grey { code: TimelineEventShieldStateCode },
|
||||
/// No shield should be presented.
|
||||
None,
|
||||
}
|
||||
@@ -992,12 +993,8 @@ pub enum ShieldState {
|
||||
impl From<SdkShieldState> for ShieldState {
|
||||
fn from(value: SdkShieldState) -> Self {
|
||||
match value {
|
||||
SdkShieldState::Red { code, message } => {
|
||||
Self::Red { code, message: message.to_owned() }
|
||||
}
|
||||
SdkShieldState::Grey { code, message } => {
|
||||
Self::Grey { code, message: message.to_owned() }
|
||||
}
|
||||
SdkShieldState::Red { code } => Self::Red { code },
|
||||
SdkShieldState::Grey { code } => Self::Grey { code },
|
||||
SdkShieldState::None => Self::None,
|
||||
}
|
||||
}
|
||||
@@ -1010,6 +1007,8 @@ pub struct EventTimelineItem {
|
||||
event_or_transaction_id: EventOrTransactionId,
|
||||
sender: String,
|
||||
sender_profile: ProfileDetails,
|
||||
forwarder: Option<String>,
|
||||
forwarder_profile: Option<ProfileDetails>,
|
||||
is_own: bool,
|
||||
is_editable: bool,
|
||||
content: TimelineItemContent,
|
||||
@@ -1033,6 +1032,8 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
event_or_transaction_id: item.identifier().into(),
|
||||
sender: item.sender().to_string(),
|
||||
sender_profile: item.sender_profile().clone().into(),
|
||||
forwarder: item.forwarder().map(ToString::to_string),
|
||||
forwarder_profile: item.forwarder_profile().map(Into::into),
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
@@ -1088,6 +1089,21 @@ impl From<TimelineDetails<Profile>> for ProfileDetails {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TimelineDetails<Profile>> for ProfileDetails {
|
||||
fn from(details: &TimelineDetails<Profile>) -> Self {
|
||||
match details {
|
||||
TimelineDetails::Unavailable => Self::Unavailable,
|
||||
TimelineDetails::Pending => Self::Pending,
|
||||
TimelineDetails::Ready(profile) => Self::Ready {
|
||||
display_name: profile.display_name.clone(),
|
||||
display_name_ambiguous: profile.display_name_ambiguous,
|
||||
avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
|
||||
},
|
||||
TimelineDetails::Error(e) => Self::Error { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct PollData {
|
||||
question: String,
|
||||
@@ -1276,8 +1292,8 @@ pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineIt
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LazyTimelineItemProvider {
|
||||
/// Returns the shields for this event timeline item.
|
||||
fn get_shields(&self, strict: bool) -> Option<ShieldState> {
|
||||
self.0.get_shield(strict).map(Into::into)
|
||||
fn get_shields(&self, strict: bool) -> ShieldState {
|
||||
self.0.get_shield(strict).into()
|
||||
}
|
||||
|
||||
/// Returns some debug information for this event timeline item.
|
||||
@@ -1301,7 +1317,7 @@ impl LazyTimelineItemProvider {
|
||||
}
|
||||
|
||||
/// Mimic the [`UiLatestEventValue`] type.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum LatestEventValue {
|
||||
None,
|
||||
Remote {
|
||||
@@ -1311,12 +1327,17 @@ pub enum LatestEventValue {
|
||||
profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
},
|
||||
RemoteInvite {
|
||||
timestamp: Timestamp,
|
||||
inviter: Option<String>,
|
||||
inviter_profile: ProfileDetails,
|
||||
},
|
||||
Local {
|
||||
timestamp: Timestamp,
|
||||
sender: String,
|
||||
profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
is_sending: bool,
|
||||
state: LatestEventValueLocalState,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1333,13 +1354,20 @@ impl From<UiLatestEventValue> for LatestEventValue {
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
UiLatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
|
||||
UiLatestEventValue::RemoteInvite { timestamp, inviter, inviter_profile } => {
|
||||
Self::RemoteInvite {
|
||||
timestamp: timestamp.into(),
|
||||
inviter: inviter.map(|inviter| inviter.to_string()),
|
||||
inviter_profile: inviter_profile.into(),
|
||||
}
|
||||
}
|
||||
UiLatestEventValue::Local { timestamp, sender, profile, content, state } => {
|
||||
Self::Local {
|
||||
timestamp: timestamp.into(),
|
||||
sender: sender.to_string(),
|
||||
profile: profile.into(),
|
||||
content: content.into(),
|
||||
is_sending,
|
||||
state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -255,6 +289,11 @@ pub struct PollAnswer {
|
||||
pub struct ThreadSummary {
|
||||
pub latest_event: EmbeddedEventDetails,
|
||||
pub num_replies: u32,
|
||||
/// The user's own public read receipt event id, for this particular thread.
|
||||
pub public_read_receipt_event_id: Option<String>,
|
||||
/// The user's own private read receipt event id, for this particular
|
||||
/// thread.
|
||||
pub private_read_receipt_event_id: Option<String>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -273,6 +312,10 @@ impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
|
||||
Self {
|
||||
latest_event: EmbeddedEventDetails::from(value.latest_event),
|
||||
num_replies: value.num_replies,
|
||||
public_read_receipt_event_id: value.public_read_receipt_event_id.map(|v| v.to_string()),
|
||||
private_read_receipt_event_id: value
|
||||
.private_read_receipt_event_id
|
||||
.map(|v| v.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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 matrix_sdk::room::{
|
||||
ListThreadsOptions as SdkListThreadsOptions, ThreadSubscription as SdkThreadSubscription,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::threads::{
|
||||
ThreadList as UIThreadList, ThreadListItem as UIThreadListItem,
|
||||
};
|
||||
use ruma::api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads;
|
||||
|
||||
use crate::{
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure wrapping a Thread List endpoint response i.e.
|
||||
/// [`ThreadListItem`]s and the current pagination token.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ThreadList {
|
||||
/// The events that are thread roots in the current batch.
|
||||
pub items: Vec<ThreadListItem>,
|
||||
|
||||
/// Token to paginate backwards in a subsequent query to
|
||||
/// [`Room::list_threads`].
|
||||
pub prev_batch_token: Option<String>,
|
||||
}
|
||||
|
||||
impl From<UIThreadList> for ThreadList {
|
||||
fn from(list: UIThreadList) -> Self {
|
||||
Self {
|
||||
items: list.items.into_iter().map(Into::into).collect(),
|
||||
prev_batch_token: list.prev_batch_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An individual Thread as retrieved from through Thread List API.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ThreadListItem {
|
||||
root_event_id: String,
|
||||
timestamp: Timestamp,
|
||||
sender: String,
|
||||
sender_profile: ProfileDetails,
|
||||
is_own: bool,
|
||||
content: Option<TimelineItemContent>,
|
||||
}
|
||||
|
||||
impl From<UIThreadListItem> for ThreadListItem {
|
||||
fn from(root: UIThreadListItem) -> Self {
|
||||
Self {
|
||||
root_event_id: root.root_event_id.to_string(),
|
||||
timestamp: root.timestamp.into(),
|
||||
sender: root.sender.to_string(),
|
||||
is_own: root.is_own,
|
||||
sender_profile: root.sender_profile.into(),
|
||||
content: root.content.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
package_name = "org.matrix.rustcomponents.sdk"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
android_cleaner = true
|
||||
# Checksums are incorrectly failing for users with 32bit (and sometimes 64bit?) devices at the moment
|
||||
# Remove once https://github.com/mozilla/uniffi-rs/issues/2740 is fixed or JNI is used instead of JNA
|
||||
omit_checksums = true
|
||||
|
||||
@@ -6,13 +6,78 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 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.
|
||||
([#6126](https://github.com/matrix-org/matrix-rust-sdk/pull/6126))
|
||||
|
||||
|
||||
- [**breaking**] `BaseClient::room_info_notable_update_sender` has
|
||||
moved into `BaseStateStore`. `BaseStateStore::derive_from_other`
|
||||
and `BaseStateStore::get_or_create_room` no longer takes a
|
||||
`room_info_notable_update_sender` argument.
|
||||
([#6130](https://github.com/matrix-org/matrix-rust-sdk/pull/6130))
|
||||
- [**breaking**] New `LatestEventValue::LocalHasBeenSent` variant to represent
|
||||
a local event that has been sent successfully.
|
||||
([#5968](https://github.com/matrix-org/matrix-rust-sdk/pull/5968))
|
||||
|
||||
### 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:
|
||||
OwnedEventId` field.
|
||||
([#5977](https://github.com/matrix-org/matrix-rust-sdk/pull/5977))
|
||||
- [**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**] `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
|
||||
been eliminated entirely.
|
||||
([#5963](https://github.com/matrix-org/matrix-rust-sdk/pull/5963))
|
||||
|
||||
## [0.16.0] - 2025-12-04
|
||||
|
||||
### Security Fixes
|
||||
|
||||
- Skip the serialization of custom join rules in the `RoomInfo` which prevented
|
||||
the processing of sync responses containing events with custom join rules.
|
||||
([#5924](https://github.com/matrix-org/matrix-rust-sdk/pull/5924))
|
||||
([#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
|
||||
|
||||
@@ -22,8 +87,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`.
|
||||
@@ -44,11 +109,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
|
||||
@@ -71,6 +138,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`.
|
||||
@@ -117,6 +185,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
|
||||
@@ -154,9 +223,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
|
||||
@@ -192,17 +261,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
|
||||
|
||||
@@ -242,8 +311,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.
|
||||
@@ -256,17 +325,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`.
|
||||
|
||||
@@ -276,8 +345,7 @@ No notable changes in this release.
|
||||
by a custom one.
|
||||
|
||||
- Introduce a `DisplayName` struct which normalizes and sanitizes
|
||||
display names.
|
||||
|
||||
display names.
|
||||
|
||||
### Refactor
|
||||
|
||||
@@ -317,34 +385,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
|
||||
|
||||
@@ -50,9 +50,6 @@ test-send-sync = [
|
||||
"matrix-sdk-crypto?/test-send-sync",
|
||||
]
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
|
||||
# helpers for testing features build upon this
|
||||
testing = [
|
||||
"dep:assert_matches",
|
||||
@@ -73,7 +70,7 @@ assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait.workspace = true
|
||||
bitflags = { workspace = true, features = ["serde"] }
|
||||
decancer = "3.3.3"
|
||||
decancer = { version = "3.3.3", default-features = false }
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
@@ -83,10 +80,8 @@ 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 = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc4186",
|
||||
|
||||
@@ -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,
|
||||
/// );
|
||||
/// ```
|
||||
@@ -112,10 +116,6 @@ pub struct BaseClient {
|
||||
/// Observable of when a user is ignored/unignored.
|
||||
pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
|
||||
|
||||
/// A sender that is used to communicate changes to room information. Each
|
||||
/// tick contains the room ID and the reasons that have generated this tick.
|
||||
pub(crate) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
|
||||
/// The strategy to use for picking recipient devices, when sending an
|
||||
/// encrypted message.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -179,18 +179,6 @@ impl BaseClient {
|
||||
pub fn new(config: StoreConfig, threading_support: ThreadingSupport) -> Self {
|
||||
let store = BaseStateStore::new(config.state_store);
|
||||
|
||||
// Create the channel to receive `RoomInfoNotableUpdate`.
|
||||
//
|
||||
// Let's consider the channel will receive 5 updates for 100 rooms maximum. This
|
||||
// is unrealistic in practise, as the sync mechanism is pretty unlikely to
|
||||
// trigger such amount of updates, it's a safe value.
|
||||
//
|
||||
// Also, note that it must not be
|
||||
// zero, because (i) it will panic, (ii) a new user has no room, but can create
|
||||
// rooms; remember that the channel's capacity is immutable.
|
||||
let (room_info_notable_update_sender, _room_info_notable_update_receiver) =
|
||||
broadcast::channel(500);
|
||||
|
||||
BaseClient {
|
||||
state_store: store,
|
||||
event_cache_store: config.event_cache_store,
|
||||
@@ -200,7 +188,6 @@ impl BaseClient {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
olm_machine: Default::default(),
|
||||
ignore_user_list_changes: Default::default(),
|
||||
room_info_notable_update_sender,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -218,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,16 +224,13 @@ impl BaseClient {
|
||||
crypto_store: self.crypto_store.clone(),
|
||||
olm_machine: self.olm_machine.clone(),
|
||||
ignore_user_list_changes: Default::default(),
|
||||
room_info_notable_update_sender: self.room_info_notable_update_sender.clone(),
|
||||
room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
|
||||
decryption_settings: self.decryption_settings.clone(),
|
||||
handle_verification_events,
|
||||
threading_support: self.threading_support,
|
||||
};
|
||||
|
||||
copy.state_store
|
||||
.derive_from_other(&self.state_store, ©.room_info_notable_update_sender)
|
||||
.await?;
|
||||
copy.state_store.derive_from_other(&self.state_store).await?;
|
||||
|
||||
Ok(copy)
|
||||
}
|
||||
@@ -258,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))
|
||||
}
|
||||
|
||||
@@ -296,11 +278,7 @@ impl BaseClient {
|
||||
/// Lookup the Room for the given RoomId, or create one, if it didn't exist
|
||||
/// yet in the store
|
||||
pub fn get_or_create_room(&self, room_id: &RoomId, room_state: RoomState) -> Room {
|
||||
self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
room_state,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
)
|
||||
self.state_store.get_or_create_room(room_id, room_state)
|
||||
}
|
||||
|
||||
/// Get a reference to the state store.
|
||||
@@ -366,13 +344,7 @@ impl BaseClient {
|
||||
) -> Result<()> {
|
||||
debug!(user_id = ?session_meta.user_id, device_id = ?session_meta.device_id, "Activating the client");
|
||||
|
||||
self.state_store
|
||||
.load_rooms(
|
||||
&session_meta.user_id,
|
||||
room_load_settings,
|
||||
&self.room_info_notable_update_sender,
|
||||
)
|
||||
.await?;
|
||||
self.state_store.load_rooms(&session_meta.user_id, room_load_settings).await?;
|
||||
self.state_store.load_sync_token().await?;
|
||||
self.state_store.set_session_meta(session_meta);
|
||||
|
||||
@@ -418,11 +390,7 @@ impl BaseClient {
|
||||
///
|
||||
/// Update the internal and cached state accordingly. Return the final Room.
|
||||
pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Knocked,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
let room = self.state_store.get_or_create_room(room_id, RoomState::Knocked);
|
||||
|
||||
if room.state() != RoomState::Knocked {
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
@@ -431,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
|
||||
@@ -459,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!();
|
||||
@@ -483,11 +460,7 @@ impl BaseClient {
|
||||
room_id: &RoomId,
|
||||
inviter: Option<OwnedUserId>,
|
||||
) -> Result<Room> {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Joined,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
let room = self.state_store.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// If the state isn't `RoomState::Joined` then this means that we knew about
|
||||
// this room before. Let's modify the existing state now.
|
||||
@@ -495,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();
|
||||
@@ -536,11 +514,7 @@ impl BaseClient {
|
||||
///
|
||||
/// Update the internal and cached state accordingly.
|
||||
pub async fn room_left(&self, room_id: &RoomId) -> Result<()> {
|
||||
let room = self.state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Left,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
);
|
||||
let room = self.state_store.get_or_create_room(room_id, RoomState::Left);
|
||||
|
||||
if room.state() != RoomState::Left {
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
@@ -549,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
|
||||
@@ -605,41 +587,26 @@ impl BaseClient {
|
||||
|
||||
let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
|
||||
|
||||
let user_id = self
|
||||
.session_meta()
|
||||
.expect("Sync shouldn't run without an authenticated user")
|
||||
.user_id
|
||||
.to_owned();
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let olm_machine = self.olm_machine().await;
|
||||
|
||||
let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let to_device = {
|
||||
let processors::e2ee::to_device::Output {
|
||||
processed_to_device_events: to_device,
|
||||
room_key_updates,
|
||||
} = processors::e2ee::to_device::from_sync_v2(
|
||||
let processors::e2ee::to_device::Output { processed_to_device_events: to_device } =
|
||||
processors::e2ee::to_device::from_sync_v2(
|
||||
&response,
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
)
|
||||
.await?;
|
||||
|
||||
processors::latest_event::decrypt_from_rooms(
|
||||
&mut context,
|
||||
room_key_updates
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|room_key_info| self.get_room(&room_key_info.room_id))
|
||||
.collect(),
|
||||
processors::e2ee::E2EE::new(
|
||||
olm_machine.as_ref(),
|
||||
&self.decryption_settings,
|
||||
self.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
to_device
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
let to_device = response
|
||||
.to_device
|
||||
@@ -682,12 +649,18 @@ 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,
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
@@ -699,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?;
|
||||
|
||||
@@ -715,7 +684,6 @@ impl BaseClient {
|
||||
&mut context,
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
@@ -726,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?;
|
||||
|
||||
@@ -741,13 +705,15 @@ impl BaseClient {
|
||||
let invited_room_update = processors::room::sync_v2::update_invited_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
&user_id,
|
||||
invited_room,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
&e2ee_context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -758,13 +724,15 @@ impl BaseClient {
|
||||
let knocked_room_update = processors::room::sync_v2::update_knocked_room(
|
||||
&mut context,
|
||||
&room_id,
|
||||
&user_id,
|
||||
knocked_room,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
processors::notification::Notification::new(
|
||||
&push_rules,
|
||||
&mut notifications,
|
||||
&self.state_store,
|
||||
),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
&e2ee_context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1029,11 +997,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?)
|
||||
}
|
||||
@@ -1120,7 +1090,7 @@ impl BaseClient {
|
||||
///
|
||||
/// Learn more by reading the [`RoomInfoNotableUpdate`] type.
|
||||
pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
|
||||
self.room_info_notable_update_sender.subscribe()
|
||||
self.state_store.room_info_notable_update_sender.subscribe()
|
||||
}
|
||||
|
||||
/// Checks whether the provided `user_id` belongs to an ignored user.
|
||||
@@ -1143,6 +1113,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.
|
||||
@@ -1206,11 +1194,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},
|
||||
@@ -1390,6 +1381,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();
|
||||
|
||||
@@ -1408,19 +1400,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);
|
||||
@@ -1520,7 +1507,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
|
||||
@@ -1582,7 +1569,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
|
||||
@@ -1646,7 +1633,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
|
||||
@@ -1660,23 +1647,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();
|
||||
|
||||
@@ -1720,7 +1697,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,
|
||||
);
|
||||
|
||||
@@ -1780,37 +1757,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
|
||||
@@ -1822,19 +1805,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,13 +14,13 @@
|
||||
|
||||
//! 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, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, UInt,
|
||||
UserId,
|
||||
events::{
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
@@ -71,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")
|
||||
});
|
||||
@@ -88,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")
|
||||
});
|
||||
|
||||
@@ -96,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")
|
||||
});
|
||||
|
||||
@@ -104,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")
|
||||
});
|
||||
|
||||
@@ -486,16 +486,34 @@ impl MemberEvent {
|
||||
self.state_key()
|
||||
}
|
||||
|
||||
/// The value of the `displayname` field in this member event.
|
||||
///
|
||||
/// [`MemberEvent::display_name()`] should be preferred to get the name to
|
||||
/// display for this member event.
|
||||
pub fn displayname_value(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Sync(event) => event.as_original()?.content.displayname.as_deref(),
|
||||
Self::Stripped(event) => event.content.displayname.as_deref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The name that should be displayed for this member event.
|
||||
///
|
||||
/// It there is no `displayname` in the event's content, the localpart or
|
||||
/// the user ID is returned.
|
||||
pub fn display_name(&self) -> DisplayName {
|
||||
DisplayName::new(
|
||||
self.original_content()
|
||||
.and_then(|c| c.displayname.as_deref())
|
||||
.unwrap_or_else(|| self.user_id().localpart()),
|
||||
)
|
||||
DisplayName::new(self.displayname_value().unwrap_or_else(|| self.user_id().localpart()))
|
||||
}
|
||||
|
||||
/// The URL of the avatar in this member event.
|
||||
///
|
||||
/// [`MemberEvent::display_name()`] should be preferred to get the name to
|
||||
/// display for this member event.
|
||||
pub fn avatar_url(&self) -> Option<&MxcUri> {
|
||||
match self {
|
||||
Self::Sync(event) => event.as_original()?.content.avatar_url.as_deref(),
|
||||
Self::Stripped(event) => event.content.avatar_url.as_deref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The optional reason why the membership changed.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
@@ -37,6 +37,12 @@ use crate::event_cache::{Event, Gap};
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
///
|
||||
/// Note that this store is not transactional. This is particularly
|
||||
/// relevant when calling [`EventCacheStore::handle_linked_chunk_updates`],
|
||||
/// which consumes a list of [`Update`]s. When processing this list, if
|
||||
/// one of the [`Update`]s fails, the previous updates in the list
|
||||
/// will not be reversed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStore {
|
||||
inner: Arc<StdRwLock<MemoryStoreInner>>,
|
||||
@@ -88,7 +94,10 @@ impl EventCacheStore for MemoryStore {
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.events.apply_updates(linked_chunk_id, updates);
|
||||
inner
|
||||
.events
|
||||
.apply_updates(linked_chunk_id, updates)
|
||||
.map_err(|e| Self::Error::Backend(Arc::new(e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -179,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)
|
||||
}
|
||||
@@ -195,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());
|
||||
@@ -210,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(
|
||||
@@ -228,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)
|
||||
}
|
||||
|
||||
@@ -271,5 +312,6 @@ mod tests {
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
event_cache_store_integration_tests_time!();
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
//! Utilities for working with events to decide whether they are suitable for
|
||||
//! use as a [crate::Room::latest_event].
|
||||
//! The Latest Event basic types.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
UserId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
call::invite::SyncCallInviteEvent,
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
message::{MessageType, SyncRoomMessageEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
rtc::notification::SyncRtcNotificationEvent,
|
||||
sticker::SyncStickerEvent,
|
||||
},
|
||||
};
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
|
||||
use crate::store::SerializableEventContent;
|
||||
|
||||
/// A latest event value!
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
@@ -34,9 +16,32 @@ pub enum LatestEventValue {
|
||||
/// The latest event represents a remote event.
|
||||
Remote(RemoteLatestEventValue),
|
||||
|
||||
/// The latest event represents an invite, i.e. the current user has been
|
||||
/// invited to join a room.
|
||||
RemoteInvite {
|
||||
/// The ID of the invite event.
|
||||
event_id: Option<OwnedEventId>,
|
||||
|
||||
/// The timestamp of the invite event.
|
||||
timestamp: MilliSecondsSinceUnixEpoch,
|
||||
|
||||
/// The user ID of the inviter.
|
||||
inviter: Option<OwnedUserId>,
|
||||
},
|
||||
|
||||
/// The latest event represents a local event that is sending.
|
||||
LocalIsSending(LocalLatestEventValue),
|
||||
|
||||
/// The latest event represents a local event that has been sent
|
||||
/// successfully. It should come quickly as a [`Self::Remote`].
|
||||
LocalHasBeenSent {
|
||||
/// ID of the sent event.
|
||||
event_id: OwnedEventId,
|
||||
|
||||
/// Value, as for other `Self::Local*` variants.
|
||||
value: LocalLatestEventValue,
|
||||
},
|
||||
|
||||
/// The latest event represents a local event that cannot be sent, either
|
||||
/// because a previous local event, or this local event cannot be sent.
|
||||
LocalCannotBeSent(LocalLatestEventValue),
|
||||
@@ -45,20 +50,28 @@ pub enum LatestEventValue {
|
||||
impl LatestEventValue {
|
||||
/// Get the timestamp of the [`LatestEventValue`].
|
||||
///
|
||||
/// If it's [`None`], it returns `None`. If it's [`Remote`], it returns the
|
||||
/// [`TimelineEvent::timestamp`]. If it's [`LocalIsSending`] or
|
||||
/// [`LocalCannotBeSent`], it returns the
|
||||
/// [`LocalLatestEventValue::timestamp`] value.
|
||||
/// - If it's [`None`], it returns `None`.
|
||||
/// - If it's [`Remote`], it returns the [`TimelineEvent::timestamp`].
|
||||
/// - If it's [`RemoteInvite`], it returns the
|
||||
/// [`SyncOrStrippedState::timestamp`].
|
||||
/// - If it's [`LocalIsSending`],[`LocalHasBeenSent`] or
|
||||
/// [`LocalCannotBeSent`], it returns the
|
||||
/// [`LocalLatestEventValue::timestamp`] value.
|
||||
///
|
||||
/// [`None`]: LatestEventValue::None
|
||||
/// [`Remote`]: LatestEventValue::Remote
|
||||
/// [`RemoteInvite`]: LatestEventValue::RemoteInvite
|
||||
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
|
||||
/// [`LocalHasBeenSent`]: LatestEventValue::LocalHasBeenSent
|
||||
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
|
||||
/// [`SyncOrStrippedState::timestamp`]: crate::deserialized_responses::SyncOrStrippedState::timestamp
|
||||
pub fn timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::Remote(remote_latest_event_value) => remote_latest_event_value.timestamp(),
|
||||
Self::RemoteInvite { timestamp, .. } => Some(*timestamp),
|
||||
Self::LocalIsSending(LocalLatestEventValue { timestamp, .. })
|
||||
| Self::LocalHasBeenSent { value: LocalLatestEventValue { timestamp, .. }, .. }
|
||||
| Self::LocalCannotBeSent(LocalLatestEventValue { timestamp, .. }) => Some(*timestamp),
|
||||
}
|
||||
}
|
||||
@@ -69,9 +82,44 @@ impl LatestEventValue {
|
||||
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
|
||||
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
|
||||
pub fn is_local(&self) -> bool {
|
||||
match self {
|
||||
Self::LocalIsSending(_)
|
||||
| Self::LocalHasBeenSent { .. }
|
||||
| Self::LocalCannotBeSent(_) => true,
|
||||
Self::None | Self::Remote(_) | Self::RemoteInvite { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the [`LatestEventValue`] represents an unsent event, i.e.
|
||||
/// is [`LocalIsSending`] nor [`LocalCannotBeSent`].
|
||||
///
|
||||
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
|
||||
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
|
||||
pub fn is_unsent(&self) -> bool {
|
||||
match self {
|
||||
Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) => true,
|
||||
Self::None | Self::Remote(_) => false,
|
||||
Self::LocalHasBeenSent { .. }
|
||||
| Self::Remote(_)
|
||||
| Self::RemoteInvite { .. }
|
||||
| Self::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the [`LatestEventValue`] is not set, i.e. [`None`].
|
||||
///
|
||||
/// [`None`]: LatestEventValue::None
|
||||
pub fn is_none(&self) -> bool {
|
||||
matches!(self, Self::None)
|
||||
}
|
||||
|
||||
/// Get the event ID (if it exists) of the event representing the
|
||||
/// [`LatestEventValue`].
|
||||
pub fn event_id(&self) -> Option<OwnedEventId> {
|
||||
match self {
|
||||
Self::Remote(event) => event.event_id(),
|
||||
Self::RemoteInvite { event_id, .. } => event_id.clone(),
|
||||
Self::LocalHasBeenSent { event_id, .. } => Some(event_id.clone()),
|
||||
Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) | Self::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +143,7 @@ mod tests_latest_event_value {
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
|
||||
owned_event_id,
|
||||
serde::Raw,
|
||||
uint,
|
||||
};
|
||||
@@ -143,6 +192,22 @@ mod tests_latest_event_value {
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_local_has_been_sent() {
|
||||
let value = LatestEventValue::LocalHasBeenSent {
|
||||
event_id: owned_event_id!("$ev0"),
|
||||
value: LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.unwrap(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_local_cannot_be_sent() {
|
||||
let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
|
||||
@@ -155,601 +220,75 @@ mod tests_latest_event_value {
|
||||
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a decision about whether an event could be stored as the latest
|
||||
/// event in a room. Variants starting with Yes indicate that this message could
|
||||
/// be stored, and provide the inner event information, and those starting with
|
||||
/// a No indicate that it could not, and give a reason.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[derive(Debug)]
|
||||
pub enum PossibleLatestEvent<'a> {
|
||||
/// This message is suitable - it is an m.room.message
|
||||
YesRoomMessage(&'a SyncRoomMessageEvent),
|
||||
/// This message is suitable - it is a sticker
|
||||
YesSticker(&'a SyncStickerEvent),
|
||||
/// This message is suitable - it is a poll
|
||||
YesPoll(&'a SyncUnstablePollStartEvent),
|
||||
|
||||
/// This message is suitable - it is a call invite
|
||||
YesCallInvite(&'a SyncCallInviteEvent),
|
||||
|
||||
/// This message is suitable - it's a call notification
|
||||
YesRtcNotification(&'a SyncRtcNotificationEvent),
|
||||
|
||||
/// This state event is suitable - it's a knock membership change
|
||||
/// that can be handled by the current user.
|
||||
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
|
||||
|
||||
// Later: YesState(),
|
||||
// Later: YesReaction(),
|
||||
/// Not suitable - it's a state event
|
||||
NoUnsupportedEventType,
|
||||
/// Not suitable - it's not a m.room.message or an edit/replacement
|
||||
NoUnsupportedMessageLikeType,
|
||||
/// Not suitable - it's encrypted
|
||||
NoEncrypted,
|
||||
}
|
||||
|
||||
/// Decide whether an event could be stored as the latest event in a room.
|
||||
/// Returns a LatestEvent representing our decision.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub fn is_suitable_for_latest_event<'a>(
|
||||
event: &'a AnySyncTimelineEvent,
|
||||
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
|
||||
) -> PossibleLatestEvent<'a> {
|
||||
match event {
|
||||
// Suitable - we have an m.room.message that was not redacted or edited
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
|
||||
if let Some(original_message) = message.as_original() {
|
||||
// Don't show incoming verification requests
|
||||
if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
}
|
||||
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
let is_replacement =
|
||||
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
relation_type == RelationType::Replacement
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if is_replacement {
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
|
||||
PossibleLatestEvent::YesPoll(poll)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
|
||||
PossibleLatestEvent::YesCallInvite(invite)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RtcNotification(notify)) => {
|
||||
PossibleLatestEvent::YesRtcNotification(notify)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
|
||||
PossibleLatestEvent::YesSticker(sticker)
|
||||
}
|
||||
|
||||
// Encrypted events are not suitable
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
}
|
||||
|
||||
// Later, if we support reactions:
|
||||
// AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Reaction(_))
|
||||
|
||||
// MessageLike, but not one of the types we want to show in message previews, so not
|
||||
// suitable
|
||||
AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
|
||||
|
||||
// We don't currently support most state events
|
||||
AnySyncTimelineEvent::State(state) => {
|
||||
// But we make an exception for knocked state events *if* the current user
|
||||
// can either accept or decline them
|
||||
if let AnySyncStateEvent::RoomMember(member) = state
|
||||
&& matches!(member.membership(), MembershipState::Knock)
|
||||
{
|
||||
let can_accept_or_decline_knocks = match power_levels_info {
|
||||
Some((own_user_id, room_power_levels)) => {
|
||||
room_power_levels.user_can_invite(own_user_id)
|
||||
|| room_power_levels.user_can_kick(own_user_id)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// The current user can act on the knock changes, so they should be
|
||||
// displayed
|
||||
if can_accept_or_decline_knocks {
|
||||
return PossibleLatestEvent::YesKnockedStateEvent(member);
|
||||
}
|
||||
}
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent all information required to represent a latest event in an
|
||||
/// efficient way.
|
||||
///
|
||||
/// ## Implementation details
|
||||
///
|
||||
/// Serialization and deserialization should be a breeze, but we introduced a
|
||||
/// change in the format without realizing, and without a migration. Ideally,
|
||||
/// this would be handled with a `serde(untagged)` enum that would be used to
|
||||
/// deserialize in either the older format, or to the new format. Unfortunately,
|
||||
/// untagged enums don't play nicely with `serde_json::value::RawValue`,
|
||||
/// so we did have to implement a custom `Deserialize` for `LatestEvent`, that
|
||||
/// first deserializes the thing as a raw JSON value, and then deserializes the
|
||||
/// JSON string as one variant or the other.
|
||||
///
|
||||
/// Because of that, `LatestEvent` should only be (de)serialized using
|
||||
/// serde_json.
|
||||
///
|
||||
/// Whenever you introduce new fields to `LatestEvent` make sure to add them to
|
||||
/// `SerializedLatestEvent` too.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LatestEvent {
|
||||
/// The actual event.
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sender_profile: Option<MinimalRoomMemberEvent>,
|
||||
|
||||
/// The name of the event' sender is ambiguous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sender_name_is_ambiguous: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SerializedLatestEvent {
|
||||
/// The actual event.
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sender_profile: Option<MinimalRoomMemberEvent>,
|
||||
|
||||
/// The name of the event' sender is ambiguous.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sender_name_is_ambiguous: Option<bool>,
|
||||
}
|
||||
|
||||
// Note: this deserialize implementation for LatestEvent will *only* work with
|
||||
// serde_json.
|
||||
impl<'de> Deserialize<'de> for LatestEvent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
|
||||
|
||||
let mut variant_errors = Vec::new();
|
||||
|
||||
match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
|
||||
Ok(value) => {
|
||||
return Ok(LatestEvent {
|
||||
event: value.event,
|
||||
sender_profile: value.sender_profile,
|
||||
sender_name_is_ambiguous: value.sender_name_is_ambiguous,
|
||||
});
|
||||
}
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
|
||||
match serde_json::from_str::<TimelineEvent>(raw.get()) {
|
||||
Ok(value) => {
|
||||
return Ok(LatestEvent {
|
||||
event: value,
|
||||
sender_profile: None,
|
||||
sender_name_is_ambiguous: None,
|
||||
});
|
||||
}
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"data did not match any variant of serialized LatestEvent (using serde_json). \
|
||||
Observed errors: {variant_errors:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl LatestEvent {
|
||||
/// Create a new [`LatestEvent`] without the sender's profile.
|
||||
pub fn new(event: TimelineEvent) -> Self {
|
||||
Self { event, sender_profile: None, sender_name_is_ambiguous: None }
|
||||
}
|
||||
|
||||
/// Create a new [`LatestEvent`] with maybe the sender's profile.
|
||||
pub fn new_with_sender_details(
|
||||
event: TimelineEvent,
|
||||
sender_profile: Option<MinimalRoomMemberEvent>,
|
||||
sender_name_is_ambiguous: Option<bool>,
|
||||
) -> Self {
|
||||
Self { event, sender_profile, sender_name_is_ambiguous }
|
||||
}
|
||||
|
||||
/// Transform [`Self`] into an event.
|
||||
pub fn into_event(self) -> TimelineEvent {
|
||||
self.event
|
||||
}
|
||||
|
||||
/// Get a reference to the event.
|
||||
pub fn event(&self) -> &TimelineEvent {
|
||||
&self.event
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the event.
|
||||
pub fn event_mut(&mut self) -> &mut TimelineEvent {
|
||||
&mut self.event
|
||||
}
|
||||
|
||||
/// Get the event ID.
|
||||
pub fn event_id(&self) -> Option<OwnedEventId> {
|
||||
self.event.event_id()
|
||||
}
|
||||
|
||||
/// Check whether [`Self`] has a sender profile.
|
||||
pub fn has_sender_profile(&self) -> bool {
|
||||
self.sender_profile.is_some()
|
||||
}
|
||||
|
||||
/// Return the sender's display name if it was known at the time [`Self`]
|
||||
/// was built.
|
||||
pub fn sender_display_name(&self) -> Option<&str> {
|
||||
self.sender_profile.as_ref().and_then(|profile| {
|
||||
profile.as_original().and_then(|event| event.content.displayname.as_deref())
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `Some(true)` if the sender's name is ambiguous, `Some(false)` if
|
||||
/// it isn't, `None` if ambiguity detection wasn't possible at the time
|
||||
/// [`Self`] was built.
|
||||
pub fn sender_name_ambiguous(&self) -> Option<bool> {
|
||||
self.sender_name_is_ambiguous
|
||||
}
|
||||
|
||||
/// Return the sender's avatar URL if it was known at the time [`Self`] was
|
||||
/// built.
|
||||
pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
|
||||
self.sender_profile.as_ref().and_then(|profile| {
|
||||
profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches::assert_matches;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
events::{
|
||||
SyncMessageLikeEvent,
|
||||
call::{SessionDescription, invite::CallInviteEventContent},
|
||||
poll::{
|
||||
unstable_response::UnstablePollResponseEventContent,
|
||||
unstable_start::{
|
||||
NewUnstablePollStartEventContent, UnstablePollAnswer,
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
relation::Replacement,
|
||||
room::{
|
||||
ImageInfo, MediaSource,
|
||||
encrypted::{
|
||||
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
|
||||
},
|
||||
message::{
|
||||
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
|
||||
Relation, RoomMessageEventContent,
|
||||
},
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
sticker::{StickerEventContent, SyncStickerEvent},
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, user_id,
|
||||
};
|
||||
use ruma::{
|
||||
events::rtc::notification::{NotificationType, RtcNotificationEventContent},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::LatestEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::new(
|
||||
"".to_owned(),
|
||||
MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
|
||||
))))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
fn test_event_id_with_none() {
|
||||
let value = LatestEventValue::None;
|
||||
|
||||
assert_eq!(m.content.msgtype.msgtype(), "m.image");
|
||||
assert!(value.event_id().is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_polls_are_suitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
|
||||
"do you like rust?",
|
||||
vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
|
||||
)))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
fn test_event_id_with_remote() {
|
||||
let event_id = owned_event_id!("$ev0");
|
||||
let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
|
||||
Raw::from_json_string(
|
||||
json!({
|
||||
"content": RoomMessageEventContent::text_plain("raclette"),
|
||||
"type": "m.room.message",
|
||||
"event_id": event_id,
|
||||
"room_id": "!r0",
|
||||
"origin_server_ts": 42,
|
||||
"sender": "@mnt_io:matrix.org",
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
|
||||
assert_eq!(value.event_id(), Some(event_id));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(CallInviteEventContent::new(
|
||||
"call_id".into(),
|
||||
UInt::new(123).unwrap(),
|
||||
SessionDescription::new("".into(), "".into()),
|
||||
VoipVersionId::V1,
|
||||
fn test_event_id_with_local_is_sending() {
|
||||
let value = LatestEventValue::LocalIsSending(LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RtcNotificationEventContent::new(
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
Duration::new(30, 0),
|
||||
NotificationType::Ring,
|
||||
))
|
||||
.into();
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesRtcNotification(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(StickerEventContent::new(
|
||||
"sticker!".to_owned(),
|
||||
ImageInfo::new(),
|
||||
owned_mxc_uri!("mxc://example.com/1"),
|
||||
))
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(UnstablePollResponseEventContent::new(
|
||||
vec![String::from("option1")],
|
||||
owned_event_id!("$1"),
|
||||
))
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.redacted(user_id!("@x:y.za"), RedactedRoomMessageEventContent::new())
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomEncryptedEventContent::new(
|
||||
EncryptedEventScheme::OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content::new(
|
||||
BTreeMap::new(),
|
||||
"".to_owned(),
|
||||
)),
|
||||
None,
|
||||
))
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomTopicEventContent::new("".to_owned()))
|
||||
.state_key("")
|
||||
.into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedEventType
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_replacement_events_are_unsuitable() {
|
||||
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
|
||||
event_content.relates_to = Some(Relation::Replacement(Replacement::new(
|
||||
owned_event_id!("$1"),
|
||||
RoomMessageEventContent::text_plain("Hello, world!").into(),
|
||||
)));
|
||||
|
||||
let event = EventFactory::new().sender(user_id!("@a:b.c")).event(event_content).into();
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_verification_requests_are_unsuitable() {
|
||||
use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
|
||||
|
||||
let event = EventFactory::new()
|
||||
.sender(user_id!("@a:b.c"))
|
||||
.event(RoomMessageEventContent::new(MessageType::VerificationRequest(
|
||||
KeyVerificationRequestEventContent::new(
|
||||
"body".to_owned(),
|
||||
vec![],
|
||||
device_id!("device_id").to_owned(),
|
||||
user_id!("@user_id:example.com").to_owned(),
|
||||
),
|
||||
)))
|
||||
.into();
|
||||
|
||||
assert_let!(
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
assert!(value.event_id().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_latest_event() {
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct TestStruct {
|
||||
latest_event: LatestEvent,
|
||||
}
|
||||
|
||||
let event = TimelineEvent::from_plaintext(
|
||||
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
|
||||
);
|
||||
|
||||
let initial = TestStruct {
|
||||
latest_event: LatestEvent {
|
||||
event: event.clone(),
|
||||
sender_profile: None,
|
||||
sender_name_is_ambiguous: None,
|
||||
fn test_event_id_with_local_has_been_sent() {
|
||||
let event_id = owned_event_id!("$ev0");
|
||||
let value = LatestEventValue::LocalHasBeenSent {
|
||||
event_id: event_id.clone(),
|
||||
value: LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.unwrap(),
|
||||
},
|
||||
};
|
||||
|
||||
// When serialized, LatestEvent always uses the new format.
|
||||
let serialized = serde_json::to_value(&initial).unwrap();
|
||||
assert_eq!(
|
||||
serialized,
|
||||
json!({
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"kind": {
|
||||
"PlainText": {
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thread_summary": "None",
|
||||
"timestamp": null,
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(value.event_id(), Some(event_id));
|
||||
}
|
||||
|
||||
// And it can be properly deserialized from the new format.
|
||||
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
|
||||
assert!(deserialized.latest_event.sender_profile.is_none());
|
||||
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
|
||||
|
||||
// The previous format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"latest_event": {
|
||||
"event": {
|
||||
"encryption_info": null,
|
||||
"event": {
|
||||
"event_id": "$1"
|
||||
}
|
||||
},
|
||||
}
|
||||
#[test]
|
||||
fn test_event_id_with_local_cannot_be_sent() {
|
||||
let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
|
||||
assert!(deserialized.latest_event.sender_profile.is_none());
|
||||
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
|
||||
|
||||
// The even older format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"latest_event": event
|
||||
});
|
||||
|
||||
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
|
||||
assert!(deserialized.latest_event.sender_profile.is_none());
|
||||
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
|
||||
assert!(value.event_id().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 +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, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
};
|
||||
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawStateEventWithKeys};
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test_utils::init_tracing_for_tests!();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Media store and common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
|
||||
//! Media store and common types for [media content](https://spec.matrix.org/latest/client-server-api/#content-repository).
|
||||
|
||||
pub mod store;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@ use crate::Result;
|
||||
/// The returned [`TimelineEvent`] has no push actions set up. It's the
|
||||
/// responsibility of the caller to set them.
|
||||
pub async fn sync_timeline_event(
|
||||
e2ee: E2EE<'_>,
|
||||
e2ee: &E2EE<'_>,
|
||||
event: &TimelineEvent,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
|
||||
@@ -17,9 +17,7 @@ use std::collections::BTreeMap;
|
||||
use matrix_sdk_common::deserialized_responses::{
|
||||
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo, ToDeviceUnableToDecryptReason,
|
||||
};
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings, EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo,
|
||||
};
|
||||
use matrix_sdk_crypto::{DecryptionSettings, EncryptionSyncChanges, OlmMachine};
|
||||
use ruma::{
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
api::client::sync::sync_events::{DeviceLists, v3, v5},
|
||||
@@ -100,10 +98,10 @@ async fn process(
|
||||
// decrypts to-device events, but leaves room events alone.
|
||||
// This makes sure that we have the decryption keys for the room
|
||||
// events at hand.
|
||||
let (events, room_key_updates) =
|
||||
let (events, _room_key_updates) =
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes, decryption_settings).await?;
|
||||
|
||||
Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
Output { processed_to_device_events: events }
|
||||
} else {
|
||||
// If we have no `OlmMachine`, just return the clear events that were passed in.
|
||||
// The encrypted ones are dropped as they are un-usable.
|
||||
@@ -131,12 +129,10 @@ async fn process(
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
room_key_updates: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Output {
|
||||
pub processed_to_device_events: Vec<ProcessedToDeviceEvent>,
|
||||
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
|
||||
}
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
|
||||
use super::{Context, e2ee::E2EE, verification};
|
||||
use crate::{
|
||||
Result, Room,
|
||||
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
|
||||
};
|
||||
|
||||
/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
|
||||
/// [`Room`]s.
|
||||
///
|
||||
/// If we can decrypt them, change [`Room::latest_event`] to reflect what we
|
||||
/// found, and remove any older encrypted events from
|
||||
/// [`Room::latest_encrypted_events`].
|
||||
pub async fn decrypt_from_rooms(
|
||||
context: &mut Context,
|
||||
rooms: Vec<Room>,
|
||||
e2ee: E2EE<'_>,
|
||||
) -> Result<()> {
|
||||
// All functions used by this one expect an `OlmMachine`. Return if there is
|
||||
// none.
|
||||
if e2ee.olm_machine.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for room in rooms {
|
||||
// Try to find a message we can decrypt and is suitable for using as the latest
|
||||
// event. If we found one, set it as the latest and delete any older
|
||||
// encrypted events
|
||||
if let Some((found, found_index)) = find_suitable_and_decrypt(&room, &e2ee).await {
|
||||
room.on_latest_event_decrypted(
|
||||
found,
|
||||
found_index,
|
||||
&mut context.state_changes,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_suitable_and_decrypt(
|
||||
room: &Room,
|
||||
e2ee: &E2EE<'_>,
|
||||
) -> Option<(Box<LatestEvent>, usize)> {
|
||||
let enc_events = room.latest_encrypted_events();
|
||||
let power_levels = room.power_levels().await.ok();
|
||||
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
|
||||
|
||||
// Walk backwards through the encrypted events, looking for one we can decrypt
|
||||
for (i, event) in enc_events.iter().enumerate().rev() {
|
||||
// Size of the `decrypt_sync_room_event` future should not impact this
|
||||
// async fn since it is likely that there aren't even any encrypted
|
||||
// events when calling it.
|
||||
let decrypt_sync_room_event =
|
||||
Box::pin(decrypt_sync_room_event(event, e2ee, room.room_id()));
|
||||
|
||||
if let Ok(decrypted) = decrypt_sync_room_event.await {
|
||||
// We found an event we can decrypt
|
||||
if let Ok(any_sync_event) = decrypted.raw().deserialize() {
|
||||
// We can deserialize it to find its type
|
||||
match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesRtcNotification(_)
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
return Some((Box::new(LatestEvent::new(decrypted)), i));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
|
||||
///
|
||||
/// In the case of a decryption error, returns a [`TimelineEvent`]
|
||||
/// representing the decryption error; in the case of problems with our
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if there is no [`OlmMachine`] in [`E2EE`].
|
||||
async fn decrypt_sync_room_event(
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
e2ee: &E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<TimelineEvent> {
|
||||
let event = match e2ee
|
||||
.olm_machine
|
||||
.expect("An `OlmMachine` is expected")
|
||||
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
// We're fine not setting the push actions for the latest event.
|
||||
|
||||
// TODO: we should use `TimelineEvent::to_decrypted`
|
||||
// but this whole code is about to get soon removed by
|
||||
// https://github.com/matrix-org/matrix-rust-sdk/pull/5624.
|
||||
let event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = event.raw().deserialize() {
|
||||
verification::process_if_relevant(&sync_timeline_event, e2ee.clone(), room_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
// TODO: we should use `TimelineEvent::to_utd`
|
||||
// but this whole code is about to get soon removed by
|
||||
// https://github.com/matrix-org/matrix-rust-sdk/pull/5624.
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
|
||||
};
|
||||
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
|
||||
|
||||
use super::{Context, E2EE, decrypt_from_rooms};
|
||||
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
|
||||
|
||||
#[async_test]
|
||||
async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
|
||||
// Given a room
|
||||
let user_id = user_id!("@u:u.to");
|
||||
let room_id = room_id!("!r:u.to");
|
||||
|
||||
let client = logged_in_base_client(Some(user_id)).await;
|
||||
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
|
||||
let response = sync_builder
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(room_id).add_timeline_event(
|
||||
EventFactory::new()
|
||||
.member(user_id)
|
||||
.display_name("Alice")
|
||||
.membership(MembershipState::Join)
|
||||
.event_id(event_id!("$1")),
|
||||
),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await.unwrap();
|
||||
|
||||
let room = client.get_room(room_id).expect("Just-created room not found!");
|
||||
|
||||
// Sanity: it has no latest_encrypted_events or latest_event
|
||||
assert!(room.latest_encrypted_events().is_empty());
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I tell it to do some decryption
|
||||
let mut context = Context::default();
|
||||
|
||||
decrypt_from_rooms(
|
||||
&mut context,
|
||||
vec![room.clone()],
|
||||
E2EE::new(
|
||||
client.olm_machine().await.as_ref(),
|
||||
&client.decryption_settings,
|
||||
client.handle_verification_events,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Then nothing changed
|
||||
assert!(room.latest_encrypted_events().is_empty());
|
||||
assert!(room.latest_event().is_none());
|
||||
assert!(context.state_changes.room_infos.is_empty());
|
||||
assert!(
|
||||
!context
|
||||
.room_info_notable_updates
|
||||
.get(room_id)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@ pub mod changes;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod e2ee;
|
||||
pub mod ephemeral_events;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod latest_event;
|
||||
pub mod notification;
|
||||
pub mod profiles;
|
||||
pub mod room;
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::RoomId;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
use crate::{RequestedRequiredStates, RoomInfoNotableUpdate, store::ambiguity_map::AmbiguityCache};
|
||||
use crate::{RequestedRequiredStates, store::ambiguity_map::AmbiguityCache};
|
||||
|
||||
pub mod display_name;
|
||||
pub mod msc4186;
|
||||
@@ -24,7 +23,6 @@ pub mod sync_v2;
|
||||
/// A classical set of data used by some processors in this module.
|
||||
pub struct RoomCreationData<'a> {
|
||||
room_id: &'a RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
requested_required_states: &'a RequestedRequiredStates,
|
||||
ambiguity_cache: &'a mut AmbiguityCache,
|
||||
}
|
||||
@@ -32,15 +30,9 @@ pub struct RoomCreationData<'a> {
|
||||
impl<'a> RoomCreationData<'a> {
|
||||
pub fn new(
|
||||
room_id: &'a RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
requested_required_states: &'a RequestedRequiredStates,
|
||||
ambiguity_cache: &'a mut AmbiguityCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
}
|
||||
Self { room_id, requested_required_states, ambiguity_cache }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ use std::collections::BTreeMap;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use as_variant::as_variant;
|
||||
use matrix_sdk_common::timer;
|
||||
use ruma::{
|
||||
JsOption, OwnedRoomId, RoomId, UserId,
|
||||
@@ -29,12 +28,11 @@ use ruma::{
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, StateEventType,
|
||||
room::member::MembershipState,
|
||||
},
|
||||
serde::Raw,
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::super::e2ee;
|
||||
@@ -42,13 +40,11 @@ use super::{
|
||||
super::{Context, notification, state_events, timeline},
|
||||
RoomCreationData,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::StateChanges;
|
||||
use crate::{
|
||||
Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomState,
|
||||
Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdateReasons, RoomState,
|
||||
store::BaseStateStore,
|
||||
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
|
||||
utils::RawStateEventWithKeys,
|
||||
};
|
||||
|
||||
/// Represent any kind of room updates.
|
||||
@@ -70,12 +66,8 @@ pub async fn update_any_room(
|
||||
) -> Result<Option<(RoomInfo, RoomUpdateKind)>> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "update_any_room");
|
||||
|
||||
let RoomCreationData {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
} = room_creation_data;
|
||||
let RoomCreationData { room_id, requested_required_states, ambiguity_cache } =
|
||||
room_creation_data;
|
||||
|
||||
// Read state events from the `required_state` field.
|
||||
//
|
||||
@@ -83,27 +75,35 @@ pub async fn update_any_room(
|
||||
// incomplete or staled already. We must only read state events from
|
||||
// `required_state`.
|
||||
let state = State::from_msc4186(room_response.required_state.clone());
|
||||
let (raw_state_events, state_events) = state.collect(&[]);
|
||||
let mut raw_state_events = state.collect(&[]);
|
||||
|
||||
let state_store = notification.state_store;
|
||||
|
||||
// Find or create the room in the store
|
||||
let is_new_room = !state_store.room_exists(room_id);
|
||||
|
||||
let invite_state_events =
|
||||
let mut raw_invite_state_events =
|
||||
room_response.invite_state.as_ref().map(|events| state_events::stripped::collect(events));
|
||||
|
||||
#[allow(unused_mut)] // Required for some feature flag combinations
|
||||
let (mut room, mut room_info, maybe_room_update_kind) = membership(
|
||||
context,
|
||||
&state_events,
|
||||
&invite_state_events,
|
||||
&mut raw_state_events,
|
||||
raw_invite_state_events.as_deref_mut(),
|
||||
state_store,
|
||||
user_id,
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
|
||||
// If we are not joined to the room, we cannot be waiting for a key bundle:
|
||||
// clear any flag that we are.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
if room_info.state() != RoomState::Joined
|
||||
&& let Some(olm) = e2ee.olm_machine
|
||||
{
|
||||
olm.store().clear_room_pending_key_bundle(room_info.room_id()).await?
|
||||
}
|
||||
|
||||
room_info.mark_state_partially_synced();
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
|
||||
|
||||
@@ -115,23 +115,24 @@ pub async fn update_any_room(
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events, &state_events),
|
||||
raw_state_events,
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
state_store,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
e2ee.clone(),
|
||||
&e2ee,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This will be used for both invited and knocked rooms.
|
||||
if let Some((raw_events, events)) = invite_state_events {
|
||||
if let Some(raw_state_events) = raw_invite_state_events {
|
||||
state_events::stripped::dispatch_invite_or_knock(
|
||||
context,
|
||||
(&raw_events, &events),
|
||||
raw_state_events,
|
||||
&room,
|
||||
&mut room_info,
|
||||
user_id,
|
||||
notification::Notification::new(
|
||||
notification.push_rules,
|
||||
notification.notifications,
|
||||
@@ -150,22 +151,10 @@ pub async fn update_any_room(
|
||||
timeline::builder::Timeline::from(room_response),
|
||||
notification,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
e2ee.clone(),
|
||||
&e2ee,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Cache the latest decrypted event in room_info, and also keep any later
|
||||
// encrypted events, so we can slot them in when we get the keys.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
cache_latest_events(
|
||||
&room,
|
||||
&mut room_info,
|
||||
&timeline.events,
|
||||
Some(&context.state_changes),
|
||||
Some(state_store),
|
||||
)
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
|
||||
e2ee.olm_machine,
|
||||
@@ -218,6 +207,13 @@ pub async fn update_any_room(
|
||||
Ok(Some((room_info, update)))
|
||||
}
|
||||
|
||||
(RoomState::Invited, None) => {
|
||||
Ok(Some((room_info, RoomUpdateKind::Invited(InvitedRoom::default()))))
|
||||
}
|
||||
(RoomState::Knocked, None) => {
|
||||
Ok(Some((room_info, RoomUpdateKind::Knocked(KnockedRoom::default()))))
|
||||
}
|
||||
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -229,12 +225,11 @@ pub async fn update_any_room(
|
||||
/// or knocked room, depending of the membership event (if any).
|
||||
fn membership(
|
||||
context: &mut Context,
|
||||
state_events: &[AnySyncStateEvent],
|
||||
invite_state_events: &Option<(Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>)>,
|
||||
state_events: &mut [RawStateEventWithKeys<AnySyncStateEvent>],
|
||||
invite_state_events: Option<&mut [RawStateEventWithKeys<AnyStrippedStateEvent>]>,
|
||||
store: &BaseStateStore,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
) -> (Room, RoomInfo, Option<RoomUpdateKind>) {
|
||||
// There are invite state events. It means the room can be:
|
||||
//
|
||||
@@ -245,28 +240,30 @@ fn membership(
|
||||
if let Some(state_events) = invite_state_events {
|
||||
// We need to find the membership event since it could be for either an invited
|
||||
// or knocked room.
|
||||
let membership_event = state_events.1.iter().find_map(|event| {
|
||||
if let AnyStrippedStateEvent::RoomMember(membership_event) = event
|
||||
&& membership_event.state_key == user_id
|
||||
let own_membership = state_events.iter_mut().find_map(|raw_event| {
|
||||
if raw_event.event_type == StateEventType::RoomMember
|
||||
&& raw_event.state_key == user_id.as_str()
|
||||
{
|
||||
return Some(membership_event.content.clone());
|
||||
raw_event
|
||||
.deserialize_as(|any_event| {
|
||||
as_variant!(any_event, AnyStrippedStateEvent::RoomMember)
|
||||
})
|
||||
.map(|event| event.content.membership.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
match membership_event {
|
||||
let raw_events = state_events.iter().map(|event| event.raw.clone()).collect();
|
||||
|
||||
match own_membership {
|
||||
// There is a membership event indicating it's a knocked room.
|
||||
Some(RoomMemberEventContent { membership: MembershipState::Knock, .. }) => {
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Knocked,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
Some(MembershipState::Knock) => {
|
||||
let room = store.get_or_create_room(room_id, RoomState::Knocked);
|
||||
let mut room_info = room.clone_info();
|
||||
// Override the room state if the room already exists.
|
||||
room_info.mark_as_knocked();
|
||||
|
||||
let raw_events = state_events.0.clone();
|
||||
let knock_state = assign!(KnockState::default(), { events: raw_events });
|
||||
let knocked_room = assign!(KnockedRoom::default(), { knock_state: knock_state });
|
||||
|
||||
@@ -275,16 +272,11 @@ fn membership(
|
||||
|
||||
// Otherwise, assume it's an invited room because there are invite state events.
|
||||
_ => {
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Invited,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
let room = store.get_or_create_room(room_id, RoomState::Invited);
|
||||
let mut room_info = room.clone_info();
|
||||
// Override the room state if the room already exists.
|
||||
room_info.mark_as_invited();
|
||||
|
||||
let raw_events = state_events.0.clone();
|
||||
let invited_room = InvitedRoom::from(InviteState::from(raw_events));
|
||||
|
||||
(room, room_info, Some(RoomUpdateKind::Invited(invited_room)))
|
||||
@@ -294,61 +286,31 @@ fn membership(
|
||||
// No invite state events. We assume this is a joined room for the moment. See this block to
|
||||
// learn more.
|
||||
else {
|
||||
let room =
|
||||
store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
|
||||
let room = store.get_or_create_room(room_id, RoomState::Joined);
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// We default to considering this room joined if it's not an invite. If it's
|
||||
// actually left (and we remembered to request membership events in
|
||||
// our sync request), then we can find this out from the events in
|
||||
// required_state by calling handle_own_room_membership.
|
||||
// actually left (and we remembered to request membership events in our sync
|
||||
// request), then we can find this out from the events in required_state by
|
||||
// calling handle_own_room_membership.
|
||||
room_info.mark_as_joined();
|
||||
|
||||
// We don't need to do this in a v2 sync, because the membership of a room can
|
||||
// be figured out by whether the room is in the "join", "leave" etc.
|
||||
// property. In sliding sync we only have invite_state,
|
||||
// required_state and timeline, so we must process required_state and timeline
|
||||
// looking for relevant membership events.
|
||||
own_membership(context, user_id, state_events, &mut room_info);
|
||||
// be figured out by whether the room is in the `join`, `leave` etc. property.
|
||||
// In sliding sync we only have `invite_state`, `required_state` and `timeline`,
|
||||
// so we must process `required_state` and `timeline` looking for relevant
|
||||
// membership events.
|
||||
state_events::sync::own_membership_and_update_room_state(
|
||||
context,
|
||||
user_id,
|
||||
state_events,
|
||||
&mut room_info,
|
||||
);
|
||||
|
||||
(room, room_info, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find any `m.room.member` events that refer to the current user, and update
|
||||
/// the state in room_info to reflect the "membership" property.
|
||||
fn own_membership(
|
||||
context: &mut Context,
|
||||
user_id: &UserId,
|
||||
state_events: &[AnySyncStateEvent],
|
||||
room_info: &mut RoomInfo,
|
||||
) {
|
||||
// Start from the last event; the first membership event we see in that order is
|
||||
// the last in the regular order, so that's the only one we need to
|
||||
// consider.
|
||||
for event in state_events.iter().rev() {
|
||||
if let AnySyncStateEvent::RoomMember(member) = &event {
|
||||
// If this event updates the current user's membership, record that in the
|
||||
// room_info.
|
||||
if member.state_key() == user_id.as_str() {
|
||||
let new_state: RoomState = member.membership().into();
|
||||
|
||||
if new_state != room_info.state() {
|
||||
room_info.set_state(new_state);
|
||||
// Update an existing notable update entry or create a new one
|
||||
context
|
||||
.room_info_notable_updates
|
||||
.entry(room_info.room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn properties(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
@@ -418,147 +380,6 @@ fn properties(
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
|
||||
///
|
||||
/// If any encrypted events are found after that one, store them in the RoomInfo
|
||||
/// too so we can use them when we get the relevant keys.
|
||||
///
|
||||
/// It is the responsibility of the caller to update the `RoomInfo` instance
|
||||
/// stored in the `Room`.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) async fn cache_latest_events(
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
events: &[TimelineEvent],
|
||||
changes: Option<&StateChanges>,
|
||||
store: Option<&BaseStateStore>,
|
||||
) {
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::DisplayName,
|
||||
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
|
||||
store::ambiguity_map::is_display_name_ambiguous,
|
||||
};
|
||||
|
||||
let _timer = timer!(tracing::Level::TRACE, "cache_latest_events");
|
||||
|
||||
let mut encrypted_events =
|
||||
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
|
||||
|
||||
// Try to get room power levels from the current changes. If we didn't get any
|
||||
// info, try getting it from local data.
|
||||
let power_levels = match changes.and_then(|changes| changes.power_levels(room_info.room_id())) {
|
||||
Some(power_levels) => Some(power_levels),
|
||||
None => room.power_levels().await.ok(),
|
||||
};
|
||||
|
||||
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
|
||||
|
||||
for event in events.iter().rev() {
|
||||
if let Ok(timeline_event) = event.raw().deserialize() {
|
||||
match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_)
|
||||
| PossibleLatestEvent::YesRtcNotification(_)
|
||||
| PossibleLatestEvent::YesSticker(_)
|
||||
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
|
||||
// We found a suitable latest event. Store it.
|
||||
|
||||
// In order to make the latest event fast to read, we want to keep the
|
||||
// associated sender in cache. This is a best-effort to gather enough
|
||||
// information for creating a user profile as fast as possible. If information
|
||||
// are missing, let's go back on the “slow” path.
|
||||
|
||||
let mut sender_profile = None;
|
||||
let mut sender_name_is_ambiguous = None;
|
||||
|
||||
// First off, look up the sender's profile from the `StateChanges`, they are
|
||||
// likely to be the most recent information.
|
||||
if let Some(changes) = changes {
|
||||
sender_profile = changes
|
||||
.profiles
|
||||
.get(room.room_id())
|
||||
.and_then(|profiles_by_user| {
|
||||
profiles_by_user.get(timeline_event.sender())
|
||||
})
|
||||
.cloned();
|
||||
|
||||
if let Some(sender_profile) = sender_profile.as_ref() {
|
||||
sender_name_is_ambiguous = sender_profile
|
||||
.as_original()
|
||||
.and_then(|profile| profile.content.displayname.as_ref())
|
||||
.and_then(|display_name| {
|
||||
let display_name = DisplayName::new(display_name);
|
||||
|
||||
changes.ambiguity_maps.get(room.room_id()).and_then(
|
||||
|map_for_room| {
|
||||
map_for_room.get(&display_name).map(|users| {
|
||||
is_display_name_ambiguous(&display_name, users)
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, look up the sender's profile from the `Store`.
|
||||
if sender_profile.is_none()
|
||||
&& let Some(store) = store
|
||||
{
|
||||
sender_profile = store
|
||||
.get_profile(room.room_id(), timeline_event.sender())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
// TODO: need to update `sender_name_is_ambiguous`,
|
||||
// but how?
|
||||
}
|
||||
|
||||
let latest_event = Box::new(LatestEvent::new_with_sender_details(
|
||||
event.clone(),
|
||||
sender_profile,
|
||||
sender_name_is_ambiguous,
|
||||
));
|
||||
|
||||
// Store it in the return RoomInfo (it will be saved for us in the room later).
|
||||
room_info.latest_event = Some(latest_event);
|
||||
// We don't need any of the older encrypted events because we have a new
|
||||
// decrypted one.
|
||||
room.latest_encrypted_events.write().unwrap().clear();
|
||||
// We can stop looking through the timeline now because everything else is
|
||||
// older.
|
||||
break;
|
||||
}
|
||||
PossibleLatestEvent::NoEncrypted => {
|
||||
// m.room.encrypted - this might be the latest event later - we can't tell until
|
||||
// we are able to decrypt it, so store it for now
|
||||
//
|
||||
// Check how many encrypted events we have seen. Only store another if we
|
||||
// haven't already stored the maximum number.
|
||||
if encrypted_events.len() < encrypted_events.capacity() {
|
||||
encrypted_events.push(event.raw().clone());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore unsuitable events
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
event_id = ?event.event_id(),
|
||||
"Failed to deserialize event as `AnySyncTimelineEvent`",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Push the encrypted events we found into the Room, in reverse order, so
|
||||
// the latest is last
|
||||
room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev());
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Construct a [`State`] from the state changes for a joined or left room
|
||||
/// from a response of the Simplified Sliding Sync endpoint.
|
||||
|
||||
@@ -15,12 +15,11 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedUserId, RoomId,
|
||||
OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
api::client::sync::sync_events::v3::{
|
||||
InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom, State as RumaState,
|
||||
},
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -30,7 +29,7 @@ use super::{
|
||||
RoomCreationData,
|
||||
};
|
||||
use crate::{
|
||||
Result, RoomInfoNotableUpdate, RoomState,
|
||||
Result, RoomState,
|
||||
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
|
||||
};
|
||||
|
||||
@@ -42,19 +41,14 @@ pub async fn update_joined_room(
|
||||
joined_room: JoinedRoom,
|
||||
updated_members_in_room: &mut BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>>,
|
||||
notification: notification::Notification<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
|
||||
) -> Result<JoinedRoomUpdate> {
|
||||
let RoomCreationData {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
} = room_creation_data;
|
||||
let RoomCreationData { room_id, requested_required_states, ambiguity_cache } =
|
||||
room_creation_data;
|
||||
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room =
|
||||
state_store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
|
||||
let room = state_store.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
@@ -67,17 +61,17 @@ pub async fn update_joined_room(
|
||||
let mut new_user_ids = BTreeSet::new();
|
||||
|
||||
let state = State::from_sync_v2(joined_room.state);
|
||||
let (raw_state_events, state_events) = state.collect(&joined_room.timeline.events);
|
||||
let raw_state_events = state.collect(&joined_room.timeline.events);
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events, &state_events),
|
||||
raw_state_events,
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut new_user_ids,
|
||||
state_store,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
e2ee.clone(),
|
||||
e2ee,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -153,19 +147,17 @@ pub async fn update_left_room(
|
||||
room_creation_data: RoomCreationData<'_>,
|
||||
left_room: LeftRoom,
|
||||
notification: notification::Notification<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
|
||||
) -> Result<LeftRoomUpdate> {
|
||||
let RoomCreationData {
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
requested_required_states,
|
||||
ambiguity_cache,
|
||||
} = room_creation_data;
|
||||
let RoomCreationData { room_id, requested_required_states, ambiguity_cache } =
|
||||
room_creation_data;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
let olm_machine = e2ee.olm_machine;
|
||||
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room =
|
||||
state_store.get_or_create_room(room_id, RoomState::Left, room_info_notable_update_sender);
|
||||
let room = state_store.get_or_create_room(room_id, RoomState::Left);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_left();
|
||||
@@ -173,17 +165,17 @@ pub async fn update_left_room(
|
||||
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
|
||||
|
||||
let state = State::from_sync_v2(left_room.state);
|
||||
let (raw_state_events, state_events) = state.collect(&left_room.timeline.events);
|
||||
let raw_state_events = state.collect(&left_room.timeline.events);
|
||||
|
||||
state_events::sync::dispatch(
|
||||
context,
|
||||
(&raw_state_events, &state_events),
|
||||
raw_state_events,
|
||||
&mut room_info,
|
||||
ambiguity_cache,
|
||||
&mut (),
|
||||
state_store,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
e2ee.clone(),
|
||||
e2ee,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -198,6 +190,13 @@ pub async fn update_left_room(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Since we are no longer joined to the room, we cannot be waiting for a key
|
||||
// bundle: clear any flag that we are.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
if let Some(olm_machine) = olm_machine {
|
||||
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
|
||||
}
|
||||
|
||||
// Save the new `RoomInfo`.
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
@@ -212,19 +211,16 @@ pub async fn update_left_room(
|
||||
pub async fn update_invited_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
invited_room: InvitedRoom,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
notification: notification::Notification<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
|
||||
) -> Result<InvitedRoomUpdate> {
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room = state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Invited,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
let room = state_store.get_or_create_room(room_id, RoomState::Invited);
|
||||
|
||||
let (raw_events, events) = state_events::stripped::collect(&invited_room.invite_state.events);
|
||||
let raw_state_events = state_events::stripped::collect(&invited_room.invite_state.events);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_invited();
|
||||
@@ -232,13 +228,21 @@ pub async fn update_invited_room(
|
||||
|
||||
state_events::stripped::dispatch_invite_or_knock(
|
||||
context,
|
||||
(&raw_events, &events),
|
||||
raw_state_events,
|
||||
&room,
|
||||
&mut room_info,
|
||||
user_id,
|
||||
notification,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Since we are no longer joined to the room, we cannot be waiting for a key
|
||||
// bundle: clear any flag that we are.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
if let Some(olm_machine) = e2ee.olm_machine {
|
||||
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
|
||||
}
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
Ok(invited_room)
|
||||
@@ -248,19 +252,16 @@ pub async fn update_invited_room(
|
||||
pub async fn update_knocked_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
knocked_room: KnockedRoom,
|
||||
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
|
||||
notification: notification::Notification<'_>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
|
||||
) -> Result<KnockedRoomUpdate> {
|
||||
let state_store = notification.state_store;
|
||||
|
||||
let room = state_store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Knocked,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
let room = state_store.get_or_create_room(room_id, RoomState::Knocked);
|
||||
|
||||
let (raw_events, events) = state_events::stripped::collect(&knocked_room.knock_state.events);
|
||||
let raw_state_events = state_events::stripped::collect(&knocked_room.knock_state.events);
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
@@ -268,13 +269,21 @@ pub async fn update_knocked_room(
|
||||
|
||||
state_events::stripped::dispatch_invite_or_knock(
|
||||
context,
|
||||
(&raw_events, &events),
|
||||
raw_state_events,
|
||||
&room,
|
||||
&mut room_info,
|
||||
user_id,
|
||||
notification,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Since we are not joined to the room, we cannot be waiting for a key
|
||||
// bundle: clear any flag that we are.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
if let Some(olm_machine) = e2ee.olm_machine {
|
||||
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
|
||||
}
|
||||
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
Ok(knocked_room)
|
||||
|
||||
@@ -14,40 +14,40 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use as_variant::as_variant;
|
||||
use ruma::{
|
||||
RoomId,
|
||||
events::{
|
||||
AnySyncStateEvent, SyncStateEvent,
|
||||
room::{create::RoomCreateEventContent, tombstone::RoomTombstoneEventContent},
|
||||
},
|
||||
events::{AnySyncStateEvent, SyncStateEvent},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
use tracing::error;
|
||||
|
||||
use super::Context;
|
||||
use crate::store::BaseStateStore;
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
use super::e2ee;
|
||||
use crate::{store::BaseStateStore, utils::RawStateEventWithKeys};
|
||||
|
||||
/// Collect [`AnySyncStateEvent`].
|
||||
pub mod sync {
|
||||
use std::{collections::BTreeSet, iter};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use as_variant::as_variant;
|
||||
use ruma::{
|
||||
OwnedUserId, RoomId, UserId,
|
||||
events::{
|
||||
AnySyncTimelineEvent, SyncStateEvent,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
AnySyncStateEvent, AnySyncTimelineEvent, StateEventType, room::member::MembershipState,
|
||||
},
|
||||
};
|
||||
use tracing::{error, instrument};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{super::profiles, AnySyncStateEvent, Context, Raw};
|
||||
use super::{super::profiles, Context, Raw};
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
use crate::response_processors::e2ee;
|
||||
use crate::{
|
||||
RoomInfo,
|
||||
RoomInfo, RoomInfoNotableUpdateReasons, RoomState,
|
||||
store::{BaseStateStore, Result as StoreResult, ambiguity_map::AmbiguityCache},
|
||||
sync::State,
|
||||
utils::RawStateEventWithKeys,
|
||||
};
|
||||
|
||||
impl State {
|
||||
@@ -58,18 +58,23 @@ pub mod sync {
|
||||
pub(crate) fn collect(
|
||||
&self,
|
||||
timeline: &[Raw<AnySyncTimelineEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
) -> Vec<RawStateEventWithKeys<AnySyncStateEvent>> {
|
||||
match self {
|
||||
Self::Before(events) => {
|
||||
super::collect(events.iter().chain(timeline.iter().filter_map(|raw_event| {
|
||||
// Only state events have a `state_key` field.
|
||||
match raw_event.get_field::<&str>("state_key") {
|
||||
Ok(Some(_)) => Some(raw_event.cast_ref_unchecked()),
|
||||
_ => None,
|
||||
}
|
||||
})))
|
||||
}
|
||||
Self::After(events) => super::collect(events),
|
||||
Self::Before(events) => events
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(RawStateEventWithKeys::try_from_raw_state_event)
|
||||
.chain(
|
||||
timeline
|
||||
.iter()
|
||||
.filter_map(RawStateEventWithKeys::try_from_raw_timeline_event),
|
||||
)
|
||||
.collect(),
|
||||
Self::After(events) => events
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(RawStateEventWithKeys::try_from_raw_state_event)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,59 +91,51 @@ pub mod sync {
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub async fn dispatch<U>(
|
||||
context: &mut Context,
|
||||
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
|
||||
raw_events: Vec<RawStateEventWithKeys<AnySyncStateEvent>>,
|
||||
room_info: &mut RoomInfo,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
new_users: &mut U,
|
||||
state_store: &BaseStateStore,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")] e2ee: e2ee::E2EE<'_>,
|
||||
#[cfg(feature = "experimental-encrypted-state-events")] e2ee: &e2ee::E2EE<'_>,
|
||||
) -> StoreResult<()>
|
||||
where
|
||||
U: NewUsers,
|
||||
{
|
||||
for (raw_event, event) in iter::zip(raw_events, events) {
|
||||
match event {
|
||||
AnySyncStateEvent::RoomMember(member) => {
|
||||
room_info.handle_state_event(event);
|
||||
for mut raw_event in raw_events {
|
||||
match (&raw_event.event_type, raw_event.state_key.as_str()) {
|
||||
(StateEventType::RoomMember, _) => {
|
||||
room_info.handle_state_event(&mut raw_event);
|
||||
|
||||
dispatch_room_member(
|
||||
context,
|
||||
&room_info.room_id,
|
||||
member,
|
||||
&mut raw_event,
|
||||
ambiguity_cache,
|
||||
new_users,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
AnySyncStateEvent::RoomCreate(create) => {
|
||||
let edited_create = super::validate_create_event_predecessor(
|
||||
(StateEventType::RoomCreate, "") => {
|
||||
super::validate_create_event_predecessor(
|
||||
context,
|
||||
room_info.room_id(),
|
||||
create,
|
||||
&room_info.room_id,
|
||||
&mut raw_event,
|
||||
state_store,
|
||||
);
|
||||
|
||||
room_info.handle_state_event(
|
||||
edited_create.map(Into::into).as_ref().unwrap_or(event),
|
||||
);
|
||||
room_info.handle_state_event(&mut raw_event);
|
||||
}
|
||||
|
||||
AnySyncStateEvent::RoomTombstone(tombstone) => {
|
||||
(StateEventType::RoomTombstone, "") => {
|
||||
if super::is_tombstone_event_valid(
|
||||
context,
|
||||
room_info.room_id(),
|
||||
tombstone,
|
||||
&room_info.room_id,
|
||||
&mut raw_event,
|
||||
state_store,
|
||||
) {
|
||||
room_info.handle_state_event(event);
|
||||
room_info.handle_state_event(&mut raw_event);
|
||||
} else {
|
||||
error!(
|
||||
room_id = ?room_info.room_id(),
|
||||
?tombstone,
|
||||
"`m.room.tombstone` event is invalid, it creates a loop"
|
||||
);
|
||||
|
||||
// Do not add the event to `room_info`.
|
||||
// Do not add the event to `context.state_changes.state`.
|
||||
continue;
|
||||
@@ -146,56 +143,18 @@ pub mod sync {
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
AnySyncStateEvent::RoomEncrypted(SyncStateEvent::Original(outer)) => {
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
trace!(event_id = ?outer.event_id, "Received encrypted state event, attempting decryption...");
|
||||
|
||||
let Some(olm_machine) = e2ee.olm_machine else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let decrypted_event = olm_machine
|
||||
.try_decrypt_room_event(
|
||||
raw_event.cast_ref_unchecked(),
|
||||
&room_info.room_id,
|
||||
e2ee.decryption_settings,
|
||||
)
|
||||
.await
|
||||
.expect("OlmMachine was not started");
|
||||
|
||||
// Skip state events that failed to decrypt.
|
||||
let RoomEventDecryptionResult::Decrypted(decrypted_event) = decrypted_event
|
||||
(StateEventType::RoomEncrypted, _) => {
|
||||
let Some(mut raw_event) =
|
||||
super::decrypt_state_event(&mut raw_event, &room_info.room_id, e2ee).await
|
||||
else {
|
||||
warn!(event_id = ?outer.event_id, "Failed to decrypt state event");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Cast to `AnySyncTimelineEvent`, safe since this is a supertype of
|
||||
// `AnyTimelineEvent`.
|
||||
let deserialized_event = match decrypted_event
|
||||
.event
|
||||
.deserialize_as::<AnySyncTimelineEvent>()
|
||||
{
|
||||
Ok(event) => event,
|
||||
Err(err) => {
|
||||
warn!(event_id = ?outer.event_id, "Failed to decrypt state event: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure decrypted event is actually a state event.
|
||||
let AnySyncTimelineEvent::State(event) = deserialized_event else {
|
||||
continue;
|
||||
};
|
||||
|
||||
trace!(event_id = ?outer.event_id, "Decrypted state event successfully.");
|
||||
room_info.handle_state_event(&event);
|
||||
room_info.handle_state_event(&mut raw_event);
|
||||
}
|
||||
|
||||
_ => {
|
||||
room_info.handle_state_event(event);
|
||||
room_info.handle_state_event(&mut raw_event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,9 +163,9 @@ pub mod sync {
|
||||
.state
|
||||
.entry(room_info.room_id.to_owned())
|
||||
.or_default()
|
||||
.entry(event.event_type())
|
||||
.entry(raw_event.event_type)
|
||||
.or_default()
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
.insert(raw_event.state_key, raw_event.raw);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -216,13 +175,19 @@ pub mod sync {
|
||||
async fn dispatch_room_member<U>(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomMemberEventContent>,
|
||||
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
new_users: &mut U,
|
||||
) -> StoreResult<()>
|
||||
where
|
||||
U: NewUsers,
|
||||
{
|
||||
let Some(event) = raw_event
|
||||
.deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomMember))
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
ambiguity_cache.handle_event(&context.state_changes, room_id, event).await?;
|
||||
|
||||
match event.membership() {
|
||||
@@ -252,26 +217,75 @@ pub mod sync {
|
||||
impl NewUsers for () {
|
||||
fn insert(&mut self, _user_id: &UserId) {}
|
||||
}
|
||||
|
||||
/// Find any `m.room.member` events that refer to the current user, and emit
|
||||
/// a membership update accordingly, plus update the state in `room_info` to
|
||||
/// reflect the "membership" property.
|
||||
pub fn own_membership_and_update_room_state(
|
||||
context: &mut Context,
|
||||
user_id: &UserId,
|
||||
state_events: &mut [RawStateEventWithKeys<AnySyncStateEvent>],
|
||||
room_info: &mut RoomInfo,
|
||||
) {
|
||||
// Start from the last event; the first membership event we see in that order is
|
||||
// the last in the regular order, so that's the only one we need to
|
||||
// consider.
|
||||
if let Some(member) = state_events.iter_mut().rev().find_map(|event| {
|
||||
// Find the event that updates the current user's membership.
|
||||
if event.event_type == StateEventType::RoomMember
|
||||
&& event.state_key.as_str() == user_id
|
||||
&& let Some(member) = event.deserialize_as(|any_event| {
|
||||
as_variant!(any_event, AnySyncStateEvent::RoomMember)
|
||||
})
|
||||
{
|
||||
Some(member)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let new_state: RoomState = member.membership().into();
|
||||
|
||||
if new_state != room_info.state() {
|
||||
room_info.set_state(new_state);
|
||||
|
||||
// Update an existing notable update entry or create a new one
|
||||
context
|
||||
.room_info_notable_updates
|
||||
.entry(room_info.room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect [`AnyStrippedStateEvent`].
|
||||
pub mod stripped {
|
||||
use std::{collections::BTreeMap, iter};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{events::AnyStrippedStateEvent, push::Action};
|
||||
use ruma::{
|
||||
RoomId, UserId,
|
||||
events::{AnyStrippedStateEvent, StateEventType},
|
||||
push::Action,
|
||||
};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{
|
||||
super::{notification, timeline},
|
||||
Context, Raw,
|
||||
};
|
||||
use crate::{Result, Room, RoomInfo};
|
||||
use crate::{RawStateEventWithKeys, Result, Room, RoomInfo, RoomInfoNotableUpdateReasons};
|
||||
|
||||
/// Collect [`Raw<AnyStrippedStateEvent>`] to [`AnyStrippedStateEvent`].
|
||||
/// Collect [`Raw<AnyStrippedStateEvent>`] to
|
||||
/// [`RawStateEventWithKeys<AnyStrippedStateEvent>`].
|
||||
pub fn collect(
|
||||
raw_events: &[Raw<AnyStrippedStateEvent>],
|
||||
) -> (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>) {
|
||||
super::collect(raw_events)
|
||||
) -> Vec<RawStateEventWithKeys<AnyStrippedStateEvent>> {
|
||||
raw_events
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(RawStateEventWithKeys::try_from_raw_state_event)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Dispatch the stripped state events.
|
||||
@@ -293,19 +307,23 @@ pub mod stripped {
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub(crate) async fn dispatch_invite_or_knock(
|
||||
context: &mut Context,
|
||||
(raw_events, events): (&[Raw<AnyStrippedStateEvent>], &[AnyStrippedStateEvent]),
|
||||
raw_events: Vec<RawStateEventWithKeys<AnyStrippedStateEvent>>,
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
user_id: &UserId,
|
||||
mut notification: notification::Notification<'_>,
|
||||
) -> Result<()> {
|
||||
let mut state_events = BTreeMap::new();
|
||||
|
||||
for (raw_event, event) in iter::zip(raw_events, events) {
|
||||
room_info.handle_stripped_state_event(event);
|
||||
own_membership(context, room.room_id(), user_id, &raw_events);
|
||||
|
||||
for mut raw_event in raw_events {
|
||||
room_info.handle_stripped_state_event(&mut raw_event);
|
||||
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.entry(raw_event.event_type)
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
.insert(raw_event.state_key, raw_event.raw);
|
||||
}
|
||||
|
||||
context
|
||||
@@ -332,38 +350,52 @@ pub mod stripped {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
|
||||
where
|
||||
I: IntoIterator<Item = &'a Raw<T>>,
|
||||
T: Deserialize<'a> + 'a,
|
||||
{
|
||||
raw_events
|
||||
.into_iter()
|
||||
.filter_map(|raw_event| match raw_event.deserialize() {
|
||||
Ok(event) => Some((raw_event.clone(), event)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't deserialize stripped state event: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.unzip()
|
||||
/// Find any `m.room.member` events that refer to the current user, and emit
|
||||
/// a membership update accordingly.
|
||||
pub fn own_membership(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
raw_state_events: &[RawStateEventWithKeys<AnyStrippedStateEvent>],
|
||||
) {
|
||||
// Start from the last event; the first membership event we see in that order is
|
||||
// the last in the regular order, so that's the only one we need to
|
||||
// consider.
|
||||
if raw_state_events.iter().rev().any(|raw_event| {
|
||||
// Find the event that updates the current user's membership.
|
||||
raw_event.event_type == StateEventType::RoomMember
|
||||
&& raw_event.state_key == user_id.as_str()
|
||||
}) {
|
||||
// Update an existing notable update entry or create a new one
|
||||
context
|
||||
.room_info_notable_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the `predecessor` in `m.room.create` isn't creating a loop of
|
||||
/// rooms.
|
||||
///
|
||||
/// If it is, we return a clone of the event with the predecessor removed.
|
||||
/// If it is, we edit the cached event in `raw_event` to remove the predecessor.
|
||||
pub fn validate_create_event_predecessor(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomCreateEventContent>,
|
||||
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
|
||||
state_store: &BaseStateStore,
|
||||
) -> Option<SyncStateEvent<RoomCreateEventContent>> {
|
||||
) {
|
||||
let mut already_seen = BTreeSet::new();
|
||||
already_seen.insert(room_id.to_owned());
|
||||
|
||||
let Some(event) =
|
||||
raw_event.deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomCreate))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Redacted and non-redacted create events use the same content type.
|
||||
let content = match event {
|
||||
SyncStateEvent::Original(event) => &event.content,
|
||||
@@ -374,7 +406,7 @@ pub fn validate_create_event_predecessor(
|
||||
content.predecessor.as_ref().map(|predecessor| predecessor.room_id.clone())
|
||||
else {
|
||||
// No predecessor = no problem here.
|
||||
return None;
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
@@ -391,7 +423,9 @@ pub fn validate_create_event_predecessor(
|
||||
SyncStateEvent::Redacted(event) => event.content.predecessor.take(),
|
||||
};
|
||||
|
||||
return Some(event);
|
||||
raw_event.set_cached_event(event.into());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
already_seen.insert(predecessor_room_id.clone());
|
||||
@@ -415,32 +449,34 @@ pub fn validate_create_event_predecessor(
|
||||
|
||||
predecessor_room_id = next_predecessor_room_id;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if `m.room.tombstone` isn't creating a loop of rooms.
|
||||
pub fn is_tombstone_event_valid(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomTombstoneEventContent>,
|
||||
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
|
||||
state_store: &BaseStateStore,
|
||||
) -> bool {
|
||||
let mut already_seen = BTreeSet::new();
|
||||
already_seen.insert(room_id.to_owned());
|
||||
|
||||
let Some(mut successor_room_id) =
|
||||
event.as_original().map(|event| event.content.replacement_room.clone())
|
||||
let Some(tombstone) = raw_event
|
||||
.deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomTombstone))
|
||||
.and_then(|event| Some(&event.as_original()?.content))
|
||||
else {
|
||||
// `true` means no problem. No successor = no problem here.
|
||||
return true;
|
||||
};
|
||||
|
||||
let mut successor_room_id = tombstone.replacement_room.clone();
|
||||
|
||||
loop {
|
||||
// We must check immediately if the `successor_room_id` is in `already_seen` in
|
||||
// case of a room is created and tombstones itself in a single sync.
|
||||
if already_seen.contains(AsRef::<RoomId>::as_ref(&successor_room_id)) {
|
||||
// Ahhh, there is a loop with `m.room.tombstone` events!
|
||||
error!(?room_id, ?tombstone, "`m.room.tombstone` event is invalid, it creates a loop");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -451,7 +487,7 @@ pub fn is_tombstone_event_valid(
|
||||
.state_changes
|
||||
.room_infos
|
||||
.get(&successor_room_id)
|
||||
.and_then(|room_info| Some(room_info.tombstone()?.replacement_room.clone()))
|
||||
.and_then(|room_info| room_info.tombstone()?.replacement_room.clone())
|
||||
.or_else(|| {
|
||||
state_store
|
||||
.room(&successor_room_id)
|
||||
@@ -468,12 +504,71 @@ pub fn is_tombstone_event_valid(
|
||||
true
|
||||
}
|
||||
|
||||
/// Attempt to decrypt the given state event.
|
||||
///
|
||||
/// Returns `Some(_)` if the state event was successfully decrypted and
|
||||
/// its keys were deserialized.
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
async fn decrypt_state_event(
|
||||
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
|
||||
room_id: &RoomId,
|
||||
e2ee: &e2ee::E2EE<'_>,
|
||||
) -> Option<RawStateEventWithKeys<AnySyncStateEvent>> {
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::OwnedEventId;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
let event_id = match raw_event.raw.get_field::<OwnedEventId>("event_id") {
|
||||
Ok(Some(event_id)) => event_id,
|
||||
Ok(None) => {
|
||||
warn!("Couldn't deserialize encrypted state event's ID: missing `event_id` field");
|
||||
return None;
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(?error, "Couldn't deserialize encrypted state event's ID");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
trace!(?event_id, "Received encrypted state event, attempting decryption...");
|
||||
|
||||
let olm_machine = e2ee.olm_machine?;
|
||||
|
||||
let decrypted_event = olm_machine
|
||||
.try_decrypt_room_event(
|
||||
raw_event.raw.cast_ref_unchecked(),
|
||||
room_id,
|
||||
e2ee.decryption_settings,
|
||||
)
|
||||
.await
|
||||
.expect("OlmMachine was not started");
|
||||
|
||||
// Skip state events that failed to decrypt.
|
||||
let RoomEventDecryptionResult::Decrypted(decrypted_event) = decrypted_event else {
|
||||
warn!(?event_id, "Failed to decrypt state event");
|
||||
return None;
|
||||
};
|
||||
|
||||
// Cast to `AnySync*Event`, safe since this is a supertype of
|
||||
// `AnyTimelineEvent`.
|
||||
match RawStateEventWithKeys::try_from_raw_state_event(decrypted_event.event.cast_unchecked()) {
|
||||
Some(event) => {
|
||||
trace!(?event_id, "Decrypted state event successfully.");
|
||||
Some(event)
|
||||
}
|
||||
None => {
|
||||
warn!(?event_id, "Failed to decrypt state event: decrypted state event is invalid");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches2::assert_matches;
|
||||
use matrix_sdk_test::{
|
||||
DEFAULT_TEST_ROOM_ID, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, TestResult,
|
||||
async_test, event_factory::EventFactory,
|
||||
DEFAULT_TEST_ROOM_ID, JoinedRoomBuilder, SyncResponseBuilder, TestResult, async_test,
|
||||
event_factory::EventFactory,
|
||||
};
|
||||
use ruma::{RoomVersionId, event_id, room_id, user_id};
|
||||
|
||||
@@ -1151,8 +1246,9 @@ mod tests {
|
||||
let client = logged_in_base_client(Some(user_id)).await;
|
||||
let mut sync_builder = SyncResponseBuilder::new();
|
||||
|
||||
let room_name = EventFactory::new()
|
||||
.sender(user_id)
|
||||
let event_factory = EventFactory::new().sender(user_id);
|
||||
|
||||
let room_name = event_factory
|
||||
.room_topic("this is the test topic in the timeline")
|
||||
.event_id(event_id!("$2"))
|
||||
.into_raw_sync();
|
||||
@@ -1161,8 +1257,8 @@ mod tests {
|
||||
.add_joined_room(
|
||||
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID)
|
||||
.add_timeline_event(room_name)
|
||||
.add_state_event(StateTestEvent::Create)
|
||||
.add_state_event(StateTestEvent::PowerLevels),
|
||||
.add_state_event(event_factory.create(user_id, RoomVersionId::V1))
|
||||
.add_state_event(event_factory.default_power_levels()),
|
||||
)
|
||||
.build_sync_response();
|
||||
client.receive_sync_response(response).await?;
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
room_info: &mut RoomInfo,
|
||||
timeline_inputs: builder::Timeline,
|
||||
mut notification: notification::Notification<'notification>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'e2ee>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'e2ee>,
|
||||
) -> Result<Timeline> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "build a timeline from sync");
|
||||
|
||||
@@ -95,7 +95,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
) => {
|
||||
if let Some(decrypted_timeline_event) =
|
||||
Box::pin(e2ee::decrypt::sync_timeline_event(
|
||||
e2ee.clone(),
|
||||
e2ee,
|
||||
&timeline_event,
|
||||
room_id,
|
||||
))
|
||||
@@ -108,7 +108,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
_ => {
|
||||
Box::pin(verification::process_if_relevant(
|
||||
&sync_timeline_event,
|
||||
e2ee.clone(),
|
||||
e2ee,
|
||||
room_id,
|
||||
))
|
||||
.await?;
|
||||
|
||||
@@ -36,7 +36,7 @@ use crate::Result;
|
||||
/// processing the response with [`OlmMachine::mark_request_as_sent`].
|
||||
pub async fn process_if_relevant(
|
||||
event: &AnySyncTimelineEvent,
|
||||
e2ee: E2EE<'_>,
|
||||
e2ee: &E2EE<'_>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if !e2ee.verification_is_allowed {
|
||||
|
||||
@@ -12,10 +12,34 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::OwnedUserId;
|
||||
use ruma::{OwnedUserId, events::rtc::notification::CallIntent};
|
||||
|
||||
use super::Room;
|
||||
|
||||
/// Represents the consensus state of call intent among room members.
|
||||
/// Call members can advertise their intent to use audio or video, clients can
|
||||
/// use this in the UI and also to decide to start camera or not when joining.
|
||||
///
|
||||
/// This enum distinguishes between full consensus (all members advertise and
|
||||
/// agree), partial consensus (only some members advertise, but those who do
|
||||
/// agree), and no consensus (either no one advertises or advertisers disagree).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CallIntentConsensus {
|
||||
/// All members advertise and agree
|
||||
Full(CallIntent),
|
||||
/// Some members advertise and agree
|
||||
Partial {
|
||||
/// The call intent that advertising members agree on.
|
||||
intent: CallIntent,
|
||||
/// Number of members advertising and agreeing on this intent.
|
||||
agreeing_count: u64,
|
||||
/// Total number of members in the call.
|
||||
total_count: u64,
|
||||
},
|
||||
/// No consensus. No one advertises or advertisers disagree.
|
||||
None,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// Is there a non expired membership with application `m.call` and scope
|
||||
/// `m.room` in this room.
|
||||
@@ -34,6 +58,12 @@ impl Room {
|
||||
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
|
||||
self.info.read().active_room_call_participants()
|
||||
}
|
||||
|
||||
/// Get the consensus call intent for the current call, based on what
|
||||
/// members are advertising.
|
||||
pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
|
||||
self.info.read().active_room_call_consensus_intent()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -51,15 +81,20 @@ mod tests {
|
||||
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
|
||||
LegacyMembershipDataInit, LivekitFocus,
|
||||
},
|
||||
rtc::notification::CallIntent,
|
||||
},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
time::SystemTime,
|
||||
user_id,
|
||||
};
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::super::{Room, RoomState};
|
||||
use crate::store::MemoryStore;
|
||||
use super::{
|
||||
super::{Room, RoomState},
|
||||
CallIntentConsensus,
|
||||
};
|
||||
use crate::{store::MemoryStore, utils::RawStateEventWithKeys};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
@@ -99,7 +134,7 @@ mod tests {
|
||||
memberships: Vec<LegacyMembershipData>,
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
) -> AnySyncStateEvent {
|
||||
) -> Raw<AnySyncStateEvent> {
|
||||
let content = CallMemberEventContent::new_legacy(memberships);
|
||||
EventFactory::new()
|
||||
.sender(user_id)
|
||||
@@ -121,11 +156,23 @@ mod tests {
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
init_data: Option<InitData<'_>>,
|
||||
) -> AnySyncStateEvent {
|
||||
let application = Application::Call(CallApplicationContent::new(
|
||||
) -> Raw<AnySyncStateEvent> {
|
||||
session_member_state_event_with_intent(ev_id, user_id, init_data, None)
|
||||
}
|
||||
|
||||
fn session_member_state_event_with_intent(
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
init_data: Option<InitData<'_>>,
|
||||
call_intent: Option<CallIntent>,
|
||||
) -> Raw<AnySyncStateEvent> {
|
||||
let mut app_content = CallApplicationContent::new(
|
||||
"my_call_id_1".to_owned(),
|
||||
ruma::events::call::member::CallScope::Room,
|
||||
));
|
||||
);
|
||||
app_content.call_intent = call_intent;
|
||||
|
||||
let application = Application::Call(app_content);
|
||||
let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
|
||||
"my_call_foci_alias".to_owned(),
|
||||
"https://lk.org".to_owned(),
|
||||
@@ -178,11 +225,14 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
fn receive_state_events(room: &Room, events: Vec<Raw<AnySyncStateEvent>>) {
|
||||
room.info.update_if(|info| {
|
||||
let mut res = false;
|
||||
for ev in events {
|
||||
res |= info.handle_state_event(ev);
|
||||
res |= info.handle_state_event(
|
||||
&mut RawStateEventWithKeys::try_from_raw_state_event(ev)
|
||||
.expect("generated state event should be valid"),
|
||||
);
|
||||
}
|
||||
res
|
||||
});
|
||||
@@ -207,7 +257,7 @@ mod tests {
|
||||
let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
|
||||
|
||||
// Intentionally use a non time sorted receive order.
|
||||
receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
|
||||
receive_state_events(&room, vec![c_two, a_empty, b_one]);
|
||||
|
||||
room
|
||||
}
|
||||
@@ -238,7 +288,7 @@ mod tests {
|
||||
Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
|
||||
);
|
||||
// Intentionally use a non time sorted receive order1
|
||||
receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
|
||||
receive_state_events(&room, vec![m_c1, m_c2, a_empty, b_one]);
|
||||
|
||||
room
|
||||
}
|
||||
@@ -272,10 +322,127 @@ mod tests {
|
||||
let c_empty_membership =
|
||||
legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
|
||||
|
||||
receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
|
||||
receive_state_events(&room, vec![b_empty_membership, c_empty_membership]);
|
||||
|
||||
// We have no active call anymore after emptying the memberships
|
||||
assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
|
||||
assert!(!room.has_active_room_call());
|
||||
}
|
||||
|
||||
fn consensus_setup(
|
||||
alice_intent: Option<CallIntent>,
|
||||
bob_intent: Option<CallIntent>,
|
||||
call_intent: Option<CallIntent>,
|
||||
) -> Vec<Raw<AnySyncStateEvent>> {
|
||||
let alice_membership = session_member_state_event_with_intent(
|
||||
event_id!("$1"),
|
||||
user_id!("@alice:server.name"),
|
||||
InitData { device_id: device_id!("AAA0"), minutes_ago: 1 }.into(),
|
||||
alice_intent,
|
||||
);
|
||||
let bob_membership = session_member_state_event_with_intent(
|
||||
event_id!("$1"),
|
||||
user_id!("@bob:server.name"),
|
||||
InitData { device_id: device_id!("BAA0"), minutes_ago: 1 }.into(),
|
||||
bob_intent,
|
||||
);
|
||||
let carl_membership = session_member_state_event_with_intent(
|
||||
event_id!("$2"),
|
||||
user_id!("@carl:server.name"),
|
||||
InitData { device_id: device_id!("CAA0"), minutes_ago: 1 }.into(),
|
||||
call_intent,
|
||||
);
|
||||
vec![alice_membership, bob_membership, carl_membership]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consensus_intent() {
|
||||
let test_cases = vec![
|
||||
// (alice_intent, bob_intent, carl_intent, expected_consensus, description)
|
||||
(None, None, None, CallIntentConsensus::None, "no intents"),
|
||||
(
|
||||
Some(CallIntent::Audio),
|
||||
None,
|
||||
None,
|
||||
CallIntentConsensus::Partial {
|
||||
intent: CallIntent::Audio,
|
||||
agreeing_count: 1,
|
||||
total_count: 3,
|
||||
},
|
||||
"one intent 1",
|
||||
),
|
||||
(
|
||||
None,
|
||||
Some(CallIntent::Audio),
|
||||
None,
|
||||
CallIntentConsensus::Partial {
|
||||
intent: CallIntent::Audio,
|
||||
agreeing_count: 1,
|
||||
total_count: 3,
|
||||
},
|
||||
"one intent 2",
|
||||
),
|
||||
(
|
||||
None,
|
||||
None,
|
||||
Some(CallIntent::Audio),
|
||||
CallIntentConsensus::Partial {
|
||||
intent: CallIntent::Audio,
|
||||
agreeing_count: 1,
|
||||
total_count: 3,
|
||||
},
|
||||
"one intent 3",
|
||||
),
|
||||
(
|
||||
None,
|
||||
None,
|
||||
Some(CallIntent::Video),
|
||||
CallIntentConsensus::Partial {
|
||||
intent: CallIntent::Video,
|
||||
agreeing_count: 1,
|
||||
total_count: 3,
|
||||
},
|
||||
"one intent 4",
|
||||
),
|
||||
(
|
||||
None,
|
||||
Some(CallIntent::Video),
|
||||
Some(CallIntent::Video),
|
||||
CallIntentConsensus::Partial {
|
||||
intent: CallIntent::Video,
|
||||
agreeing_count: 2,
|
||||
total_count: 3,
|
||||
},
|
||||
"two matching intents",
|
||||
),
|
||||
(
|
||||
Some(CallIntent::Video),
|
||||
Some(CallIntent::Video),
|
||||
Some(CallIntent::Video),
|
||||
CallIntentConsensus::Full(CallIntent::Video),
|
||||
"all agree",
|
||||
),
|
||||
(
|
||||
Some(CallIntent::Video),
|
||||
None,
|
||||
Some(CallIntent::Audio),
|
||||
CallIntentConsensus::None,
|
||||
"disagreement",
|
||||
),
|
||||
(
|
||||
Some(CallIntent::Video),
|
||||
Some(CallIntent::Video),
|
||||
Some(CallIntent::Audio),
|
||||
CallIntentConsensus::None,
|
||||
"disagreement 2",
|
||||
),
|
||||
];
|
||||
|
||||
for (alice, bob, carl, expected, description) in test_cases {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
receive_state_events(&room, consensus_setup(alice, bob, carl));
|
||||
let consensus_intent = room.active_room_call_consensus_intent();
|
||||
assert_eq!(expected, consensus_intent, "Failed case: {}", description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
|
||||
use ruma::{
|
||||
OwnedUserId, RoomVersionId, assign,
|
||||
events::{
|
||||
EmptyStateKey, RedactContent, RedactedStateEventContent, StateEventType,
|
||||
EmptyStateKey, PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StateEventType, StaticEventContent,
|
||||
macros::EventContent,
|
||||
room::create::{PreviousRoom, RoomCreateEventContent},
|
||||
},
|
||||
@@ -135,10 +136,10 @@ impl RoomCreateWithCreatorEventContent {
|
||||
pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
|
||||
|
||||
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
|
||||
type StateKey = EmptyStateKey;
|
||||
type StateKey = <RoomCreateWithCreatorEventContent as StateEventContent>::StateKey;
|
||||
|
||||
fn event_type(&self) -> StateEventType {
|
||||
StateEventType::RoomCreate
|
||||
RoomCreateWithCreatorEventContent::TYPE.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,3 +157,11 @@ impl RedactContent for RoomCreateWithCreatorEventContent {
|
||||
fn default_create_room_version_id() -> RoomVersionId {
|
||||
RoomVersionId::V1
|
||||
}
|
||||
|
||||
impl PossiblyRedactedStateEventContent for RoomCreateWithCreatorEventContent {
|
||||
type StateKey = <RoomCreateWithCreatorEventContent as StateEventContent>::StateKey;
|
||||
|
||||
fn event_type(&self) -> StateEventType {
|
||||
RoomCreateWithCreatorEventContent::TYPE.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ impl Room {
|
||||
/// If you need a variant that's sync (but with the drawback that it returns
|
||||
/// an `Option`), consider using [`Room::cached_display_name`].
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
/// [spec]: <https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room>
|
||||
pub async fn display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
if let Some(name) = self.cached_display_name() {
|
||||
Ok(name)
|
||||
@@ -99,7 +99,7 @@ impl Room {
|
||||
/// or [`Room::display_name`] (async, always returns a value), which should
|
||||
/// be preferred in general.
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
/// [spec]: <https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room>
|
||||
pub(crate) async fn compute_display_name(&self) -> StoreResult<UpdatedRoomDisplayName> {
|
||||
enum DisplayNameOrSummary {
|
||||
Summary(RoomSummary),
|
||||
@@ -407,7 +407,7 @@ pub struct RoomHero {
|
||||
const NUM_HEROES: usize = 5;
|
||||
|
||||
/// The name of the room, either from the metadata or calculated
|
||||
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
|
||||
/// according to [matrix specification](https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room)
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RoomDisplayName {
|
||||
/// The room has been named explicitly as
|
||||
@@ -544,12 +544,14 @@ mod tests {
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
canonical_alias::{
|
||||
PossiblyRedactedRoomCanonicalAliasEventContent, RoomCanonicalAliasEventContent,
|
||||
},
|
||||
member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
|
||||
name::RoomNameEventContent,
|
||||
name::{PossiblyRedactedRoomNameEventContent, RoomNameEventContent},
|
||||
},
|
||||
},
|
||||
room_alias_id, room_id,
|
||||
owned_room_alias_id, owned_user_id, room_alias_id, room_id,
|
||||
serde::Raw,
|
||||
user_id,
|
||||
};
|
||||
@@ -557,8 +559,7 @@ mod tests {
|
||||
|
||||
use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
|
||||
use crate::{
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomHero, RoomState, StateChanges,
|
||||
StateStore, store::MemoryStore,
|
||||
MinimalStateEvent, RoomHero, RoomState, StateChanges, StateStore, store::MemoryStore,
|
||||
};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
@@ -584,22 +585,22 @@ mod tests {
|
||||
}
|
||||
|
||||
fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
|
||||
MinimalStateEvent::Original(OriginalMinimalStateEvent {
|
||||
content: assign!(RoomCanonicalAliasEventContent::new(), {
|
||||
alias: Some(room_alias_id!("#test:example.com").to_owned()),
|
||||
MinimalStateEvent {
|
||||
content: assign!(PossiblyRedactedRoomCanonicalAliasEventContent::new(), {
|
||||
alias: Some(owned_room_alias_id!("#test:example.com")),
|
||||
}),
|
||||
event_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn make_name_event_with(name: &str) -> MinimalStateEvent<RoomNameEventContent> {
|
||||
MinimalStateEvent::Original(OriginalMinimalStateEvent {
|
||||
content: RoomNameEventContent::new(name.to_owned()),
|
||||
fn make_name_event_with(name: &str) -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
|
||||
MinimalStateEvent {
|
||||
content: RoomNameEventContent::new(name.to_owned()).into(),
|
||||
event_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
|
||||
fn make_name_event() -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
|
||||
make_name_event_with("Test Room")
|
||||
}
|
||||
|
||||
@@ -697,10 +698,7 @@ mod tests {
|
||||
async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
|
||||
let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
|
||||
content: RoomNameEventContent::new(String::new()),
|
||||
event_id: None,
|
||||
});
|
||||
let room_name = make_name_event_with("");
|
||||
room.info.update(|info| info.base_info.name = Some(room_name));
|
||||
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
@@ -1037,27 +1035,27 @@ mod tests {
|
||||
None,
|
||||
vec![
|
||||
RoomHero {
|
||||
user_id: user_id!("@alice:example.org").to_owned(),
|
||||
user_id: owned_user_id!("@alice:example.org"),
|
||||
display_name: Some("Alice".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@bob:example.org").to_owned(),
|
||||
user_id: owned_user_id!("@bob:example.org"),
|
||||
display_name: Some("Bob".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@carol:example.org").to_owned(),
|
||||
user_id: owned_user_id!("@carol:example.org"),
|
||||
display_name: Some("Carol".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@denis:example.org").to_owned(),
|
||||
user_id: owned_user_id!("@denis:example.org"),
|
||||
display_name: Some("Denis".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@erica:example.org").to_owned(),
|
||||
user_id: owned_user_id!("@erica:example.org"),
|
||||
display_name: Some("Erica".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::events::room::encryption::RoomEncryptionEventContent;
|
||||
use ruma::events::room::encryption::PossiblyRedactedRoomEncryptionEventContent;
|
||||
|
||||
use super::Room;
|
||||
|
||||
@@ -24,7 +24,7 @@ impl Room {
|
||||
|
||||
/// Get the `m.room.encryption` content that enabled end to end encryption
|
||||
/// in the room.
|
||||
pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
|
||||
pub fn encryption_settings(&self) -> Option<PossiblyRedactedRoomEncryptionEventContent> {
|
||||
self.info.read().base_info.encryption.clone()
|
||||
}
|
||||
}
|
||||
@@ -90,12 +90,13 @@ mod tests {
|
||||
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, event_id,
|
||||
events::{AnySyncStateEvent, room::encryption::RoomEncryptionEventContent},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
time::SystemTime,
|
||||
user_id,
|
||||
};
|
||||
|
||||
use super::{EncryptionState, Room};
|
||||
use crate::{RoomState, store::MemoryStore};
|
||||
use crate::{RoomState, store::MemoryStore, utils::RawStateEventWithKeys};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
@@ -113,11 +114,14 @@ mod tests {
|
||||
.expect("date out of range")
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
fn receive_state_events(room: &Room, events: Vec<Raw<AnySyncStateEvent>>) {
|
||||
room.info.update_if(|info| {
|
||||
let mut res = false;
|
||||
for ev in events {
|
||||
res |= info.handle_state_event(ev);
|
||||
res |= info.handle_state_event(
|
||||
&mut RawStateEventWithKeys::try_from_raw_state_event(ev)
|
||||
.expect("generated state event should be valid"),
|
||||
);
|
||||
}
|
||||
res
|
||||
});
|
||||
@@ -140,7 +144,7 @@ mod tests {
|
||||
// in the roomInfo
|
||||
.server_ts(timestamp(0))
|
||||
.into();
|
||||
receive_state_events(&room, vec![&encryption_event]);
|
||||
receive_state_events(&room, vec![encryption_event]);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
|
||||
}
|
||||
|
||||
@@ -12,297 +12,27 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::{collections::BTreeMap, num::NonZeroUsize};
|
||||
|
||||
use ruma::MilliSecondsSinceUnixEpoch;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
|
||||
use super::Room;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::RoomInfoNotableUpdateReasons;
|
||||
use crate::latest_event::{LatestEvent, LatestEventValue};
|
||||
use crate::latest_event::LatestEventValue;
|
||||
|
||||
impl Room {
|
||||
/// The size of the latest_encrypted_events RingBuffer
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(super) const MAX_ENCRYPTED_EVENTS: NonZeroUsize = NonZeroUsize::new(10).unwrap();
|
||||
|
||||
/// Return the last event in this room, if one has been cached during
|
||||
/// sliding sync.
|
||||
pub fn latest_event(&self) -> Option<LatestEvent> {
|
||||
self.info.read().latest_event.as_deref().cloned()
|
||||
}
|
||||
|
||||
/// Return the [`LatestEventValue`] of this room.
|
||||
///
|
||||
/// Note that it clones the [`LatestEventValue`]! This can be add pressure
|
||||
/// Note that it clones the [`LatestEventValue`]! This can add pressure
|
||||
/// on the memory if used in a hot path.
|
||||
pub fn new_latest_event(&self) -> LatestEventValue {
|
||||
self.info.read().new_latest_event.clone()
|
||||
pub fn latest_event(&self) -> LatestEventValue {
|
||||
self.info.read().latest_event_value.clone()
|
||||
}
|
||||
|
||||
/// Return the value of [`LatestEventValue::timestamp`].
|
||||
pub fn new_latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
self.info.read().new_latest_event.timestamp()
|
||||
pub fn latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
self.info.read().latest_event_value.timestamp()
|
||||
}
|
||||
|
||||
/// Return the value of [`LatestEventValue::is_local`].
|
||||
pub fn new_latest_event_is_local(&self) -> bool {
|
||||
self.info.read().new_latest_event.is_local()
|
||||
}
|
||||
|
||||
/// Return the most recent few encrypted events. When the keys come through
|
||||
/// to decrypt these, the most recent relevant one will replace
|
||||
/// latest_event. (We can't tell which one is relevant until
|
||||
/// they are decrypted.)
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
|
||||
self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// Replace our latest_event with the supplied event, and delete it and all
|
||||
/// older encrypted events from latest_encrypted_events, given that the
|
||||
/// new event was at the supplied index in the latest_encrypted_events
|
||||
/// list.
|
||||
///
|
||||
/// Panics if index is not a valid index in the latest_encrypted_events
|
||||
/// list.
|
||||
///
|
||||
/// It is the responsibility of the caller to apply the changes into the
|
||||
/// state store after calling this function.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn on_latest_event_decrypted(
|
||||
&self,
|
||||
latest_event: Box<LatestEvent>,
|
||||
index: usize,
|
||||
changes: &mut crate::StateChanges,
|
||||
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
|
||||
) {
|
||||
self.latest_encrypted_events.write().unwrap().drain(0..=index);
|
||||
|
||||
let room_info = changes
|
||||
.room_infos
|
||||
.entry(self.room_id().to_owned())
|
||||
.or_insert_with(|| self.clone_info());
|
||||
|
||||
room_info.latest_event = Some(latest_event);
|
||||
|
||||
room_info_notable_updates
|
||||
.entry(self.room_id().to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::LATEST_EVENT);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "e2e-encryption"))]
|
||||
mod tests_with_e2e_encryption {
|
||||
use std::sync::Arc;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{room_id, serde::Raw, user_id};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
BaseClient, Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
|
||||
SessionMeta, StateChanges,
|
||||
client::ThreadingSupport,
|
||||
latest_event::LatestEvent,
|
||||
response_processors as processors,
|
||||
store::{MemoryStore, RoomLoadSettings, StoreConfig},
|
||||
};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
let user_id = user_id!("@me:example.org");
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
|
||||
// Given a room,
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
client
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// That has an encrypted event,
|
||||
add_encrypted_event(&room, "$A");
|
||||
// Sanity: it has no latest_event
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I set up an observer on the latest_event,
|
||||
let mut room_info_notable_update = client.room_info_notable_update_receiver();
|
||||
|
||||
// And I provide a decrypted event to replace the encrypted one,
|
||||
let event = make_latest_event("$A");
|
||||
|
||||
let mut context = processors::Context::default();
|
||||
room.on_latest_event_decrypted(
|
||||
event.clone(),
|
||||
0,
|
||||
&mut context.state_changes,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
|
||||
assert!(context.room_info_notable_updates.contains_key(room_id));
|
||||
|
||||
// The subscriber isn't notified at this point.
|
||||
assert!(room_info_notable_update.is_empty());
|
||||
|
||||
// Then updating the room info will store the event,
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
|
||||
// And wake up the subscriber.
|
||||
assert_matches!(
|
||||
room_info_notable_update.recv().await,
|
||||
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
|
||||
assert_eq!(received_room_id, room_id);
|
||||
assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Given a room with an encrypted event
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
add_encrypted_event(&room, "$A");
|
||||
// Sanity: it has no latest_event
|
||||
assert!(room.latest_event().is_none());
|
||||
|
||||
// When I provide a decrypted event to replace the encrypted one
|
||||
let event = make_latest_event("$A");
|
||||
let mut changes = StateChanges::default();
|
||||
let mut room_info_notable_updates = BTreeMap::new();
|
||||
room.on_latest_event_decrypted(
|
||||
event.clone(),
|
||||
0,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
);
|
||||
room.set_room_info(
|
||||
changes.room_infos.get(room.room_id()).cloned().unwrap(),
|
||||
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
|
||||
);
|
||||
|
||||
// Then is it stored
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
|
||||
// Given a room with some encrypted events and a latest event
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.info.update(|info| info.latest_event = Some(make_latest_event("$A")));
|
||||
add_encrypted_event(&room, "$0");
|
||||
add_encrypted_event(&room, "$1");
|
||||
add_encrypted_event(&room, "$2");
|
||||
add_encrypted_event(&room, "$3");
|
||||
|
||||
// When I provide a latest event
|
||||
let new_event = make_latest_event("$1");
|
||||
let new_event_index = 1;
|
||||
let mut changes = StateChanges::default();
|
||||
let mut room_info_notable_updates = BTreeMap::new();
|
||||
room.on_latest_event_decrypted(
|
||||
new_event.clone(),
|
||||
new_event_index,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
);
|
||||
room.set_room_info(
|
||||
changes.room_infos.get(room.room_id()).cloned().unwrap(),
|
||||
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
|
||||
);
|
||||
|
||||
// Then the encrypted events list is shortened to only newer events
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
assert_eq!(enc_evs.len(), 2);
|
||||
assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
|
||||
assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
|
||||
|
||||
// And the event is stored
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_replacing_the_newest_event_leaves_none_left() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Given a room with some encrypted events
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
add_encrypted_event(&room, "$0");
|
||||
add_encrypted_event(&room, "$1");
|
||||
add_encrypted_event(&room, "$2");
|
||||
add_encrypted_event(&room, "$3");
|
||||
|
||||
// When I provide a latest event and say it was the very latest
|
||||
let new_event = make_latest_event("$3");
|
||||
let new_event_index = 3;
|
||||
let mut changes = StateChanges::default();
|
||||
let mut room_info_notable_updates = BTreeMap::new();
|
||||
room.on_latest_event_decrypted(
|
||||
new_event,
|
||||
new_event_index,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
);
|
||||
room.set_room_info(
|
||||
changes.room_infos.get(room.room_id()).cloned().unwrap(),
|
||||
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
|
||||
);
|
||||
|
||||
// Then the encrypted events list ie empty
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
assert_eq!(enc_evs.len(), 0);
|
||||
}
|
||||
|
||||
fn add_encrypted_event(room: &Room, event_id: &str) {
|
||||
room.latest_encrypted_events
|
||||
.write()
|
||||
.unwrap()
|
||||
.push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
|
||||
}
|
||||
|
||||
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
|
||||
Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
|
||||
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
|
||||
)))
|
||||
/// Return the value of [`LatestEventValue::is_unsent`].
|
||||
pub fn latest_event_is_unsent(&self) -> bool {
|
||||
self.info.read().latest_event_value.is_unsent()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,9 +259,9 @@ impl RoomMember {
|
||||
/// Get the display name of the member if there is one.
|
||||
pub fn display_name(&self) -> Option<&str> {
|
||||
if let Some(p) = self.profile.as_ref() {
|
||||
p.as_original().and_then(|e| e.content.displayname.as_deref())
|
||||
p.content.displayname.as_deref()
|
||||
} else {
|
||||
self.event.original_content()?.displayname.as_deref()
|
||||
self.event.displayname_value()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,9 +276,9 @@ impl RoomMember {
|
||||
/// Get the avatar url of the member, if there is one.
|
||||
pub fn avatar_url(&self) -> Option<&MxcUri> {
|
||||
if let Some(p) = self.profile.as_ref() {
|
||||
p.as_original().and_then(|e| e.content.avatar_url.as_deref())
|
||||
p.content.avatar_url.as_deref()
|
||||
} else {
|
||||
self.event.original_content()?.avatar_url.as_deref()
|
||||
self.event.avatar_url()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user