Compare commits
758 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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"
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
@@ -92,8 +95,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
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
components: clippy
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.2
|
||||
uses: qmaru/wasm-pack-action@v0.5.3
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.13.1
|
||||
@@ -270,7 +270,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
uses: crate-ci/typos@v1.44.0
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -313,7 +313,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-10-01
|
||||
toolchain: nightly-2026-02-26
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -322,7 +322,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
- name: Store coverage report in artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v47.0.0
|
||||
uses: tj-actions/changed-files@v47.0.5
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
|
||||
@@ -9,4 +9,4 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@72602674bc341ca927683caddbf578672c352476
|
||||
uses: bnjbvr/cargo-machete@8a8e0cd1046f54206f6d2e050cf028e6db9dbc75
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-10-01
|
||||
toolchain: nightly-2026-02-26
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
- run: cargo hack check --rust-version --workspace --all-targets
|
||||
|
||||
@@ -53,7 +53,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@v5
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
|
||||
+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
+120
-195
@@ -92,55 +92,12 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
@@ -211,9 +168,9 @@ checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a"
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.13.1"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7"
|
||||
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"itoa",
|
||||
@@ -224,9 +181,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.13.1"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac"
|
||||
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
@@ -241,9 +198,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f"
|
||||
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -316,6 +273,12 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-once-cell"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
|
||||
|
||||
[[package]]
|
||||
name = "async-rx"
|
||||
version = "0.1.3"
|
||||
@@ -328,9 +291,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
@@ -339,9 +302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
||||
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -503,15 +466,6 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
@@ -601,17 +555,6 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
@@ -638,9 +581,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
@@ -650,18 +593,18 @@ checksum = "00f4369ba008f82b968b1acbe31715ec37bd45236fa0726605a36cc3060ea256"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
|
||||
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
|
||||
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -831,7 +774,6 @@ version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
@@ -857,40 +799,42 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "codspeed"
|
||||
version = "3.0.5"
|
||||
version = "4.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35584c5fcba8059780748866387fb97c5a203bcfc563fc3d0790af406727a117"
|
||||
checksum = "5f0d98d97fd75ca4489a1a0997820a6521531085e7c8a98941bd0e1264d567dd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"cc",
|
||||
"colored",
|
||||
"getrandom 0.2.15",
|
||||
"glob",
|
||||
"libc",
|
||||
"nix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"statrs",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-criterion-compat"
|
||||
version = "3.0.5"
|
||||
version = "4.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78f6c1c6bed5fd84d319e8b0889da051daa361c79b7709c9394dfe1a882bba67"
|
||||
checksum = "d16fe2db207123f7b3a3b5cfff0c22f99469f7534145f3573f57f4c8a5653c2c"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codspeed",
|
||||
"codspeed-criterion-compat-walltime",
|
||||
"colored",
|
||||
"futures",
|
||||
"regex",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-criterion-compat-walltime"
|
||||
version = "3.0.5"
|
||||
version = "4.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c989289ce6b1cbde72ed560496cb8fbf5aa14d5ef5666f168e7f87751038352e"
|
||||
checksum = "b035c7f9846b143aeabb3833f5b584023eb97b43ecbff3d997db74c4372df2bd"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
@@ -942,12 +886,6 @@ dependencies = [
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.2.0"
|
||||
@@ -1269,28 +1207,45 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
|
||||
dependencies = [
|
||||
"deadpool-runtime",
|
||||
"deadpool-runtime 0.1.4",
|
||||
"lazy_static",
|
||||
"num_cpus",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04"
|
||||
dependencies = [
|
||||
"deadpool-runtime 0.3.1",
|
||||
"num_cpus",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-runtime"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-runtime"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool-sync"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04"
|
||||
checksum = "e385cc95d3d582c328b36d1ff90feac061102b001894b555e6b465a2e0eaabbf"
|
||||
dependencies = [
|
||||
"deadpool-runtime",
|
||||
"deadpool-runtime 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1308,10 +1263,6 @@ name = "decancer"
|
||||
version = "3.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9244323129647178bf41ac861a2cdb9d9c81b9b09d3d0d1de9cd302b33b8a1d"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "delegate-display"
|
||||
@@ -2782,12 +2733,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
@@ -2900,6 +2845,9 @@ name = "language-tags"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
@@ -3150,6 +3098,7 @@ dependencies = [
|
||||
"assert_matches",
|
||||
"assert_matches2",
|
||||
"async-channel",
|
||||
"async-once-cell",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3184,7 +3133,6 @@ dependencies = [
|
||||
"mime",
|
||||
"mime2ext",
|
||||
"oauth2",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"proptest",
|
||||
@@ -3241,7 +3189,6 @@ dependencies = [
|
||||
"matrix-sdk-store-encryption",
|
||||
"matrix-sdk-test",
|
||||
"matrix-sdk-test-utils",
|
||||
"once_cell",
|
||||
"proptest",
|
||||
"regex",
|
||||
"ruma",
|
||||
@@ -3311,7 +3258,6 @@ dependencies = [
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"http",
|
||||
"indoc",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"js_option",
|
||||
@@ -3378,6 +3324,7 @@ version = "0.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compat",
|
||||
"chrono",
|
||||
"console_error_panic_hook",
|
||||
"extension-trait",
|
||||
"eyeball-im",
|
||||
@@ -3392,7 +3339,6 @@ dependencies = [
|
||||
"matrix-sdk-ui",
|
||||
"mime",
|
||||
"oauth2",
|
||||
"once_cell",
|
||||
"paranoid-android",
|
||||
"ruma",
|
||||
"sentry",
|
||||
@@ -3400,6 +3346,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar-asserts",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3482,7 +3429,6 @@ dependencies = [
|
||||
"matrix-sdk-test",
|
||||
"matrix-sdk-test-utils",
|
||||
"matrix-sdk-ui",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
@@ -3534,7 +3480,7 @@ dependencies = [
|
||||
"as_variant",
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
"deadpool",
|
||||
"deadpool 0.13.0",
|
||||
"deadpool-sync",
|
||||
"glob",
|
||||
"itertools 0.14.0",
|
||||
@@ -3545,7 +3491,6 @@ dependencies = [
|
||||
"matrix-sdk-test",
|
||||
"matrix-sdk-test-utils",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"rmp-serde",
|
||||
"ruma",
|
||||
"rusqlite",
|
||||
@@ -3592,10 +3537,10 @@ dependencies = [
|
||||
"insta",
|
||||
"matrix-sdk-common",
|
||||
"matrix-sdk-test-macros",
|
||||
"once_cell",
|
||||
"ruma",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
"vodozemac",
|
||||
@@ -3650,7 +3595,6 @@ dependencies = [
|
||||
"matrix-sdk-test",
|
||||
"matrix-sdk-test-utils",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"proptest",
|
||||
"ruma",
|
||||
@@ -3863,9 +3807,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
@@ -3894,9 +3838,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
@@ -3964,9 +3908,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oneshot"
|
||||
version = "0.1.11"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
|
||||
checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -4112,7 +4056,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4364,9 +4307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.92"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -4799,13 +4742,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "070c1de99c5b1fe78827abfce71b858104a2d1dae8f19029ee52cd876e8d2654"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"js_int",
|
||||
"js_option",
|
||||
"language-tags",
|
||||
"ruma-client-api",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
@@ -4817,9 +4760,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2353d8d42b3aa0b58ed4187ef40fba3ed66d12c47201fd201803a122262985c"
|
||||
version = "0.22.1"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
@@ -4841,9 +4783,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d9223155004bd089d840d87a6b77dc17378657d0f597087eb0eb541a28c37e0"
|
||||
version = "0.17.1"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64",
|
||||
@@ -4852,7 +4793,6 @@ dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"http",
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"js_int",
|
||||
"konst",
|
||||
"percent-encoding",
|
||||
@@ -4875,26 +4815,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69ad82efa481b458c37ed3bdf47404bd41c8611d15336da387a0dd1e0f088689"
|
||||
version = "0.32.1"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
"js_int",
|
||||
"js_option",
|
||||
"percent-encoding",
|
||||
"language-tags",
|
||||
"pulldown-cmark",
|
||||
"regex",
|
||||
"ruma-common",
|
||||
"ruma-html",
|
||||
"ruma-identifiers-validation",
|
||||
"ruma-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"url",
|
||||
"web-time",
|
||||
"wildmatch",
|
||||
"zeroize",
|
||||
@@ -4902,9 +4838,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c464d081bcd3a677b1eef2e0f5db6b796acb7f5eed0a99ba501ca1423d2590ed"
|
||||
version = "0.13.1"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"headers",
|
||||
"http",
|
||||
@@ -4925,8 +4860,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-html"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6dcd6e9823e177d15460d3cd3a413f38a2beea381f26aca1001c05cd6954ff"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"html5ever",
|
||||
@@ -4937,8 +4871,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c6b5643060beec0fc9d7acfb41d2c5d91e1591db440ff62361d178e77c35fe"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror 2.0.17",
|
||||
@@ -4946,10 +4879,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffb3fa639bd851add9ca8a89200e6b8e00dd54cea9b21d39e8884e71e377633"
|
||||
version = "0.17.1"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"cfg-if",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
@@ -4963,8 +4896,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146ace2cd59b60ec80d3e801a84e7e6a91e3e01d18a9f5d896ea7ca16a6b8e08"
|
||||
source = "git+https://github.com/ruma/ruma?rev=2455c71ec8bd16318397865d9c6e231e792cb15e#2455c71ec8bd16318397865d9c6e231e792cb15e"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"ed25519-dalek",
|
||||
@@ -5507,10 +5439,6 @@ name = "similar"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar-asserts"
|
||||
@@ -5993,9 +5921,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.44"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
@@ -6003,22 +5931,22 @@ dependencies = [
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.24"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
|
||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@@ -6484,9 +6412,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "uniffi"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c866f627c3f04c3df068b68bb2d725492caaa539dd313e2a9d26bb85b1a32f4e"
|
||||
checksum = "b8c6dec3fc6645f71a16a3fa9ff57991028153bd194ca97f4b55e610c73ce66a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -6508,9 +6436,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_bindgen"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c8ca600167641ebe7c8ba9254af40492dda3397c528cc3b2f511bd23e8541a5"
|
||||
checksum = "4ed0150801958d4825da56a41c71f000a457ac3a4613fa9647df78ac4b6b6881"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
@@ -6534,9 +6462,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_build"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e55c05228f4858bb258f651d21d743fcc1fe5a2ec20d3c0f9daefddb105ee4d"
|
||||
checksum = "b78fd9271a4c2e85bd2c266c5a9ede1fac676eb39fd77f636c27eaf67426fd5f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -6545,9 +6473,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_core"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e7a5a038ebffe8f4cf91416b154ef3c2468b18e828b7009e01b1b99938089f9"
|
||||
checksum = "b0ef62e69762fbb9386dcb6c87cd3dd05d525fa8a3a579a290892e60ddbda47e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compat",
|
||||
@@ -6558,9 +6486,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_internal_macros"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c2a6f93e7b73726e2015696ece25ca0ac5a5f1cf8d6a7ab5214dd0a01d2edf"
|
||||
checksum = "98f51ebca0d9a4b2aa6c644d5ede45c56f73906b96403c08a1985e75ccb64a01"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
@@ -6571,9 +6499,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_macros"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64c6309fc36c7992afc03bc0c5b059c656bccbef3f2a4bc362980017f8936141"
|
||||
checksum = "db9d12529f1223d014fd501e5f29ca0884d15d6ed5ddddd9f506e55350327dc3"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"fs-err",
|
||||
@@ -6588,21 +6516,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_meta"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a138823392dba19b0aa494872689f97d0ee157de5852e2bec157ce6de9cdc22"
|
||||
checksum = "9df6d413db2827c68588f8149d30d49b71d540d46539e435b23a7f7dbd4d4f86"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"siphasher 0.3.11",
|
||||
"siphasher 1.0.1",
|
||||
"uniffi_internal_macros",
|
||||
"uniffi_pipeline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_pipeline"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c27c4b515d25f8e53cc918e238c39a79c3144a40eaf2e51c4a7958973422c29"
|
||||
checksum = "a806dddc8208f22efd7e95a5cdf88ed43d0f3271e8f63b47e757a8bbdb43b63a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
@@ -6613,9 +6541,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_udl"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0adacdd848aeed7af4f5af7d2f621d5e82531325d405e29463482becfdeafca"
|
||||
checksum = "0d1a7339539bf6f6fa3e9b534dece13f778bda2d54b1a6d4e40b4d6090ac26e7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"textwrap",
|
||||
@@ -6713,12 +6641,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
@@ -7035,9 +6957,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "2.3.4"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3928939971918220fed093266b809d1ee4ec6c1a2d72692ff6876898f3b16c19"
|
||||
checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
@@ -7379,7 +7301,7 @@ checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"base64",
|
||||
"deadpool",
|
||||
"deadpool 0.12.3",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body-util",
|
||||
@@ -7447,13 +7369,16 @@ name = "xtask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo_metadata",
|
||||
"chrono",
|
||||
"clap",
|
||||
"fs_extra",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uniffi_bindgen",
|
||||
"url",
|
||||
"xshell",
|
||||
]
|
||||
|
||||
|
||||
+99
-69
@@ -11,63 +11,68 @@ 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"
|
||||
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.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"
|
||||
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"
|
||||
itertools = { version = "0.14.0", default-features = false, features = ["use_std"] }
|
||||
js-sys = { version = "0.3.82", default-features = false, features = ["std"] }
|
||||
mime = { version = "0.3.17", default-features = false }
|
||||
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
|
||||
once_cell = "1.21.3"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.16"
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
pin-project-lite = { version = "0.2.16", default-features = false }
|
||||
proc-macro2 = { version = "1.0.106", default-features = false }
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
regex = "1.12.2"
|
||||
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.12.24", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
ruma = { version = "0.14.0", features = [
|
||||
rmp-serde = { version = "1.3.0", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "2455c71ec8bd16318397865d9c6e231e792cb15e", features = [
|
||||
"client-api-c",
|
||||
"compat-unset-avatar",
|
||||
"compat-upload-signatures",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
@@ -89,33 +94,35 @@ ruma = { version = "0.14.0", features = [
|
||||
"unstable-msc4310",
|
||||
] }
|
||||
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 +174,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 +207,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,7 +15,7 @@ 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-base.workspace = true
|
||||
@@ -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,134 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- `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
|
||||
|
||||
- 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))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**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", "native-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,11 @@ 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 }
|
||||
|
||||
[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,28 @@ 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?;
|
||||
}
|
||||
CrossProcessLockConfig::SingleProcess => {
|
||||
client.inner.oauth();
|
||||
}
|
||||
|
||||
client
|
||||
.inner
|
||||
.oauth()
|
||||
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(session_delegate) = session_delegate {
|
||||
@@ -725,6 +789,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 +968,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 +1074,7 @@ impl Client {
|
||||
listener.on_notification(
|
||||
NotificationItem {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
@@ -1131,14 +1222,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 +1245,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 +1313,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 +1612,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 +1977,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 +2237,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 +2338,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 +2360,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels.events = value
|
||||
.events
|
||||
.iter()
|
||||
@@ -2272,7 +2368,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
(event_type, (*power_level).into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels
|
||||
}
|
||||
}
|
||||
@@ -2299,6 +2394,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 +2450,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 +2662,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 +2990,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,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 UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
@@ -6,6 +20,7 @@ use std::{num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
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()? {
|
||||
@@ -498,15 +501,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 +619,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()
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
|
||||
+78
-24
@@ -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,22 @@ 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;
|
||||
|
||||
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 +181,7 @@ type ReloadHandle = Handle<
|
||||
Layered<EnvFilter, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
SizeAndDateRollingWriter,
|
||||
>,
|
||||
Layered<EnvFilter, Registry>,
|
||||
>;
|
||||
@@ -218,21 +246,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 +278,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 +330,7 @@ enum LogTarget {
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkLatestEvents,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkSendQueue,
|
||||
MatrixSdkSlidingSync,
|
||||
@@ -319,6 +361,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 +384,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 +416,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 +445,11 @@ impl TraceLogPacks {
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
TraceLogPacks::LatestEvents => &[
|
||||
LogTarget::MatrixSdkLatestEvents,
|
||||
LogTarget::MatrixSdkSendQueue,
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,7 +599,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
|
||||
}
|
||||
@@ -746,6 +797,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,
|
||||
@@ -791,6 +843,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,
|
||||
@@ -837,6 +890,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;
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::{
|
||||
@@ -236,15 +251,26 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +342,8 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
SecureChannelError::Utf8(_)
|
||||
| SecureChannelError::MessageDecode(_)
|
||||
| SecureChannelError::Json(_)
|
||||
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
|
||||
| SecureChannelError::RendezvousChannel(_)
|
||||
| SecureChannelError::UnsupportedQrCodeType => HumanQrLoginError::Unknown,
|
||||
SecureChannelError::SecureChannelMessage { .. }
|
||||
| SecureChannelError::Ecies(_)
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
@@ -371,23 +398,40 @@ 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,
|
||||
}
|
||||
|
||||
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 +439,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(_)
|
||||
| SecureChannelError::UnsupportedQrCodeType => Self::Unknown(e.to_string()),
|
||||
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};
|
||||
@@ -5,8 +19,8 @@ use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
TryFromReportedContentScoreError,
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges,
|
||||
ListThreadsOptions as SdkListThreadsOptions, Room as SdkRoom, RoomMemberRole,
|
||||
},
|
||||
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
@@ -21,6 +35,7 @@ use matrix_sdk_ui::{
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads,
|
||||
assign,
|
||||
events::{
|
||||
receipt::ReceiptThread,
|
||||
@@ -53,8 +68,7 @@ use crate::{
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
AbstractProgress, EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
|
||||
UploadSource,
|
||||
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
|
||||
},
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
@@ -258,10 +272,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 +317,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 +358,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 +483,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(())
|
||||
}
|
||||
@@ -1233,6 +1242,41 @@ 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
|
||||
/// [`ThreadRoots::prev_batch_token`]. This token can be passed to the next
|
||||
/// call to this function, through the `from` field of
|
||||
/// [`ListThreadsOptions`].
|
||||
pub async fn list_threads(&self, opts: ListThreadsOptions) -> Result<ThreadRoots, ClientError> {
|
||||
let inner_opts = SdkListThreadsOptions {
|
||||
include_threads: match opts.include_threads {
|
||||
IncludeThreads::All => SdkIncludeThreads::All,
|
||||
IncludeThreads::Participated => SdkIncludeThreads::Participated,
|
||||
},
|
||||
from: opts.from,
|
||||
limit: opts.limit.and_then(ruma::UInt::new),
|
||||
};
|
||||
|
||||
let roots = self.inner.list_threads(inner_opts).await?;
|
||||
|
||||
Ok(ThreadRoots {
|
||||
chunk: roots
|
||||
.chunk
|
||||
.into_iter()
|
||||
.filter_map(|timeline_event| {
|
||||
timeline_event
|
||||
.raw()
|
||||
.deserialize()
|
||||
.ok()
|
||||
.map(|any_timeline_event| TimelineEvent(Box::new(any_timeline_event)))
|
||||
})
|
||||
.collect(),
|
||||
prev_batch_token: roots.prev_batch_token,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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(
|
||||
@@ -1258,6 +1302,60 @@ pub struct ThreadSubscription {
|
||||
automatic: bool,
|
||||
}
|
||||
|
||||
/// Options for [Room::list_threads].
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct ListThreadsOptions {
|
||||
/// An extra filter to select which threads should be returned.
|
||||
pub include_threads: IncludeThreads,
|
||||
|
||||
/// The token to start returning events from.
|
||||
///
|
||||
/// This token can be obtained from a [`ThreadRoots::prev_batch_token`]
|
||||
/// returned by a previous call to [`Room::list_threads()`].
|
||||
///
|
||||
/// If `from` isn't provided the homeserver shall return a list of thread
|
||||
/// roots from end of the timeline history.
|
||||
pub from: Option<String>,
|
||||
|
||||
/// The maximum number of events to return.
|
||||
///
|
||||
/// Default: 10.
|
||||
pub limit: Option<u64>,
|
||||
}
|
||||
|
||||
/// Which threads to include in the response.
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum IncludeThreads {
|
||||
/// `all`
|
||||
///
|
||||
/// Include all thread roots found in the room.
|
||||
///
|
||||
/// This is the default.
|
||||
All,
|
||||
|
||||
/// `participated`
|
||||
///
|
||||
/// Only include thread roots for threads where
|
||||
/// [`current_user_participated`] is `true`.
|
||||
///
|
||||
/// [`current_user_participated`]: https://spec.matrix.org/latest/client-server-api/#server-side-aggregation-of-mthread-relationships
|
||||
Participated,
|
||||
}
|
||||
|
||||
/// The result of a [`Room::list_threads`] query.
|
||||
///
|
||||
/// This is a wrapper around the Ruma equivalent, with events decrypted if needs
|
||||
/// be.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct ThreadRoots {
|
||||
/// The events that are thread roots in the current batch.
|
||||
pub chunk: Vec<TimelineEvent>,
|
||||
|
||||
/// Token to paginate backwards in a subsequent query to
|
||||
/// [`Room::list_threads`].
|
||||
pub prev_batch_token: Option<String>,
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
|
||||
@@ -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,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::{EncryptionState, RoomState};
|
||||
@@ -35,6 +49,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,6 +63,7 @@ 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>,
|
||||
@@ -119,6 +135,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 +155,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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -472,6 +486,7 @@ impl RoomListDynamicEntriesController {
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Identifiers { identifiers: Vec<String> },
|
||||
NonSpace,
|
||||
Space,
|
||||
NonLeft,
|
||||
@@ -482,6 +497,7 @@ pub enum RoomListEntriesDynamicFilterKind {
|
||||
Favourite,
|
||||
LowPriority,
|
||||
NonLowPriority,
|
||||
NonFavorite,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
None,
|
||||
@@ -516,6 +532,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 +543,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::{
|
||||
@@ -510,6 +512,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 +570,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 +609,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 +646,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 +681,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()?,
|
||||
@@ -907,6 +934,7 @@ pub struct LocationContent {
|
||||
pub enum AssetType {
|
||||
Sender,
|
||||
Pin,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<AssetType> for RumaAssetType {
|
||||
@@ -914,6 +942,17 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -93,6 +96,28 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
error: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
Content::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();
|
||||
|
||||
TimelineItemContent::LiveLocation {
|
||||
content: LiveLocationContent {
|
||||
is_live: state.is_live(),
|
||||
description: state.description().map(ToOwned::to_owned),
|
||||
timeout_ms: state.timeout().as_millis() as u64,
|
||||
asset_type: state.asset_type().into(),
|
||||
locations,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,6 +207,13 @@ pub enum TimelineItemContent {
|
||||
state_key: String,
|
||||
error: String,
|
||||
},
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -242,36 +274,112 @@ 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: Option<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: Option<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: Option<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,
|
||||
|
||||
/// 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,
|
||||
@@ -330,17 +438,56 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
|
||||
Content::RoomPowerLevels(c) => match c {
|
||||
FullContent::Original { content, prev_content } => 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: Some(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 }
|
||||
}
|
||||
FullContent::Redacted(_) => Self::RoomPowerLevels {
|
||||
events: Default::default(),
|
||||
previous_events: None,
|
||||
users: Default::default(),
|
||||
previous_users: None,
|
||||
thresholds: None,
|
||||
previous_thresholds: None,
|
||||
},
|
||||
},
|
||||
Content::RoomServerAcl(_) => Self::RoomServerAcl,
|
||||
Content::RoomThirdPartyInvite(c) => {
|
||||
|
||||
@@ -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};
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +255,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 +278,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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,68 @@ 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 `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))
|
||||
|
||||
### 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 +77,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 +99,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 +128,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 +175,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 +213,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 +251,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 +301,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 +315,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 +335,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 +375,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,
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ pub fn make_test_event_with_event_id(
|
||||
let encryption_info = Arc::new(EncryptionInfo {
|
||||
sender: (*ALICE).into(),
|
||||
sender_device: None,
|
||||
forwarder: None,
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: "1337".to_owned(),
|
||||
sender_claimed_keys: Default::default(),
|
||||
@@ -136,14 +137,53 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// the store.
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
|
||||
|
||||
/// Test that the next and previous fields only reference chunks that
|
||||
/// already exist in the store.
|
||||
async fn test_linked_chunk_exists_before_referenced(&self);
|
||||
|
||||
/// Test loading the last chunk in a linked chunk from the store.
|
||||
async fn test_load_last_chunk(&self);
|
||||
|
||||
/// Test that cycles are detected when loading the last chunk in a linked
|
||||
/// chunk from the store.
|
||||
async fn test_load_last_chunk_with_a_cycle(&self);
|
||||
|
||||
/// Test loading the previous chunk in a linked chunk from the store.
|
||||
async fn test_load_previous_chunk(&self);
|
||||
|
||||
/// Test loading a linked chunk incrementally (chunk by chunk) from the
|
||||
/// store.
|
||||
async fn test_linked_chunk_incremental_loading(&self);
|
||||
|
||||
/// Test removing a chunk.
|
||||
async fn test_linked_chunk_remove_chunk(&self);
|
||||
|
||||
/// Test replacing an item in a linked chunk.
|
||||
async fn test_linked_chunk_replace_item(&self);
|
||||
|
||||
/// Test remove an item from a linked chunk.
|
||||
async fn test_linked_chunk_remove_item(&self);
|
||||
|
||||
/// Test detaching last items from a linked chunk.
|
||||
async fn test_linked_chunk_detach_last_items(&self);
|
||||
|
||||
/// Test that start reattach and end reattach items does nothing.
|
||||
async fn test_linked_chunk_start_end_reattach_items(&self);
|
||||
|
||||
/// Test clearing a linked chunk.
|
||||
async fn test_linked_chunk_clear(&self);
|
||||
|
||||
/// Test clearing a linked chunk and re-inserting a past event.
|
||||
async fn test_linked_chunk_clear_and_reinsert(&self);
|
||||
|
||||
/// Test that rebuilding a linked chunk from an empty store doesn't return
|
||||
/// anything.
|
||||
async fn test_rebuild_empty_linked_chunk(&self);
|
||||
|
||||
/// Test that linked chunks are only accessible through their enclosing
|
||||
/// room.
|
||||
async fn test_linked_chunk_multiple_rooms(&self);
|
||||
|
||||
/// Test that loading a linked chunk's metadata works as intended.
|
||||
async fn test_load_all_chunks_metadata(&self);
|
||||
|
||||
@@ -156,6 +196,9 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// Test that filtering duplicated events works as expected.
|
||||
async fn test_filter_duplicated_events(&self);
|
||||
|
||||
/// Test that filtering duplicated events works with an empty filter.
|
||||
async fn test_filter_duplicate_events_no_events(&self);
|
||||
|
||||
/// Test that an event can be found or not.
|
||||
async fn test_find_event(&self);
|
||||
|
||||
@@ -199,7 +242,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "parmesan".to_owned() },
|
||||
gap: Gap { token: "parmesan".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -240,7 +283,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(second.identifier(), CId::new(1));
|
||||
|
||||
assert_matches!(second.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "parmesan");
|
||||
assert_eq!(gap.token, "parmesan");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,6 +300,61 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_exists_before_referenced(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
// Fails to add the chunk because previous chunk is not in the self
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![Update::NewItemsChunk {
|
||||
previous: Some(CId::new(41)),
|
||||
new: CId::new(42),
|
||||
next: None,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Fails to add the chunk because next chunk is not in the self
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![Update::NewItemsChunk {
|
||||
previous: None,
|
||||
new: CId::new(42),
|
||||
next: Some(CId::new(43)),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Fails to add the chunk because previous chunk is not in the self
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![Update::NewGapChunk {
|
||||
previous: Some(CId::new(41)),
|
||||
new: CId::new(42),
|
||||
next: None,
|
||||
gap: Gap { token: "gap".to_owned() },
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Fails to add the chunk because next chunk is not in the self
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![Update::NewGapChunk {
|
||||
previous: None,
|
||||
new: CId::new(42),
|
||||
next: Some(CId::new(43)),
|
||||
gap: Gap { token: "gap".to_owned() },
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
async fn test_load_all_chunks_metadata(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
@@ -279,7 +377,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "parmesan".to_owned() },
|
||||
gap: Gap { token: "parmesan".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -323,6 +421,182 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(metas[3].num_items, 0);
|
||||
}
|
||||
|
||||
async fn test_load_last_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
// Case #1: no last chunk.
|
||||
{
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert!(last_chunk.is_none());
|
||||
assert_eq!(chunk_identifier_generator.current(), 0);
|
||||
}
|
||||
|
||||
// Case #2: only one chunk is present.
|
||||
{
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![event("saucisse de morteau"), event("comté")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_matches!(last_chunk, Some(last_chunk) => {
|
||||
assert_eq!(last_chunk.identifier, 42);
|
||||
assert!(last_chunk.previous.is_none());
|
||||
assert!(last_chunk.next.is_none());
|
||||
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
|
||||
assert_eq!(items.len(), 2);
|
||||
check_test_event(&items[0], "saucisse de morteau");
|
||||
check_test_event(&items[1], "comté");
|
||||
});
|
||||
});
|
||||
assert_eq!(chunk_identifier_generator.current(), 42);
|
||||
}
|
||||
|
||||
// Case #3: more chunks are present.
|
||||
{
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(7),
|
||||
next: None,
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(7), 0),
|
||||
items: vec![event("fondue"), event("gruyère"), event("mont d'or")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_matches!(last_chunk, Some(last_chunk) => {
|
||||
assert_eq!(last_chunk.identifier, 7);
|
||||
assert_matches!(last_chunk.previous, Some(previous) => {
|
||||
assert_eq!(previous, 42);
|
||||
});
|
||||
assert!(last_chunk.next.is_none());
|
||||
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
|
||||
assert_eq!(items.len(), 3);
|
||||
check_test_event(&items[0], "fondue");
|
||||
check_test_event(&items[1], "gruyère");
|
||||
check_test_event(&items[2], "mont d'or");
|
||||
});
|
||||
});
|
||||
assert_eq!(chunk_identifier_generator.current(), 42);
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_load_last_chunk_with_a_cycle(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::NewItemsChunk {
|
||||
// Because `previous` connects to chunk #0, it will create a cycle.
|
||||
// Chunk #0 will have a `next` set to chunk #1! Consequently, the last chunk
|
||||
// **does not exist**. We have to detect this cycle.
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: Some(CId::new(0)),
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.load_last_chunk(linked_chunk_id).await.unwrap_err();
|
||||
}
|
||||
|
||||
async fn test_load_previous_chunk(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
// Case #1: no chunk at all, equivalent to having an nonexistent
|
||||
// `before_chunk_identifier`.
|
||||
{
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, CId::new(153)).await.unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
|
||||
// Case #2: there is one chunk only: we request the previous on this
|
||||
// one, it doesn't exist.
|
||||
{
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![Update::NewItemsChunk { previous: None, new: CId::new(42), next: None }],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, CId::new(42)).await.unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
|
||||
// Case #3: there are two chunks.
|
||||
{
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
// new chunk before the one that exists.
|
||||
Update::NewItemsChunk {
|
||||
previous: None,
|
||||
new: CId::new(7),
|
||||
next: Some(CId::new(42)),
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(7), 0),
|
||||
items: vec![event("brigand du jorat"), event("morbier")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(linked_chunk_id, CId::new(42)).await.unwrap();
|
||||
|
||||
assert_matches!(previous_chunk, Some(previous_chunk) => {
|
||||
assert_eq!(previous_chunk.identifier, 7);
|
||||
assert!(previous_chunk.previous.is_none());
|
||||
assert_matches!(previous_chunk.next, Some(next) => {
|
||||
assert_eq!(next, 42);
|
||||
});
|
||||
assert_matches!(previous_chunk.content, ChunkContent::Items(items) => {
|
||||
assert_eq!(items.len(), 2);
|
||||
check_test_event(&items[0], "brigand du jorat");
|
||||
check_test_event(&items[1], "morbier");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_incremental_loading(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
@@ -352,7 +626,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "morbier".to_owned() },
|
||||
gap: Gap { token: "morbier".to_owned() },
|
||||
},
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -430,7 +704,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert_eq!(chunk.lazy_previous(), Some(CId::new(0)));
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
assert_eq!(gap.token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -468,7 +742,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
assert_eq!(gap.token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -519,7 +793,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
assert_eq!(gap.token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,6 +814,293 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_remove_chunk(&self) {
|
||||
let room_id = &DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewGapChunk {
|
||||
previous: None,
|
||||
new: CId::new(42),
|
||||
next: None,
|
||||
gap: Gap { token: "raclette".to_owned() },
|
||||
},
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(43),
|
||||
next: None,
|
||||
gap: Gap { token: "fondue".to_owned() },
|
||||
},
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(43)),
|
||||
new: CId::new(44),
|
||||
next: None,
|
||||
gap: Gap { token: "tartiflette".to_owned() },
|
||||
},
|
||||
Update::RemoveChunk(CId::new(43)),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = self.load_all_chunks(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunks.len(), 2);
|
||||
|
||||
// Chunks are ordered from smaller to bigger IDs.
|
||||
let c = chunks.remove(0);
|
||||
assert_eq!(c.identifier, CId::new(42));
|
||||
assert_eq!(c.previous, None);
|
||||
assert_eq!(c.next, Some(CId::new(44)));
|
||||
assert_matches!(c.content, ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.token, "raclette");
|
||||
});
|
||||
|
||||
let c = chunks.remove(0);
|
||||
assert_eq!(c.identifier, CId::new(44));
|
||||
assert_eq!(c.previous, Some(CId::new(42)));
|
||||
assert_eq!(c.next, None);
|
||||
assert_matches!(c.content, ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.token, "tartiflette");
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_replace_item(&self) {
|
||||
let room_id = &DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_id = event_id!("$world");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![
|
||||
make_test_event(room_id, "hello"),
|
||||
make_test_event_with_event_id(room_id, "world", Some(event_id)),
|
||||
],
|
||||
},
|
||||
Update::ReplaceItem {
|
||||
at: Position::new(CId::new(42), 1),
|
||||
item: make_test_event_with_event_id(room_id, "yolo", Some(event_id)),
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = self.load_all_chunks(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunks.len(), 1);
|
||||
|
||||
let c = chunks.remove(0);
|
||||
assert_eq!(c.identifier, CId::new(42));
|
||||
assert_eq!(c.previous, None);
|
||||
assert_eq!(c.next, None);
|
||||
assert_matches!(c.content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "hello");
|
||||
check_test_event(&events[1], "yolo");
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_remove_item(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![
|
||||
make_test_event(room_id, "one"),
|
||||
make_test_event(room_id, "two"),
|
||||
make_test_event(room_id, "three"),
|
||||
make_test_event(room_id, "four"),
|
||||
make_test_event(room_id, "five"),
|
||||
make_test_event(room_id, "six"),
|
||||
],
|
||||
},
|
||||
Update::RemoveItem { at: Position::new(CId::new(42), 2) /* "three" */ },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = self.load_all_chunks(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunks.len(), 1);
|
||||
|
||||
let c = chunks.remove(0);
|
||||
assert_eq!(c.identifier, CId::new(42));
|
||||
assert_eq!(c.previous, None);
|
||||
assert_eq!(c.next, None);
|
||||
assert_matches!(c.content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 5);
|
||||
check_test_event(&events[0], "one");
|
||||
check_test_event(&events[1], "two");
|
||||
check_test_event(&events[2], "four");
|
||||
check_test_event(&events[3], "five");
|
||||
check_test_event(&events[4], "six");
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_detach_last_items(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![
|
||||
make_test_event(room_id, "hello"),
|
||||
make_test_event(room_id, "world"),
|
||||
make_test_event(room_id, "howdy"),
|
||||
],
|
||||
},
|
||||
Update::DetachLastItems { at: Position::new(CId::new(42), 1) },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = self.load_all_chunks(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunks.len(), 1);
|
||||
|
||||
let c = chunks.remove(0);
|
||||
assert_eq!(c.identifier, CId::new(42));
|
||||
assert_eq!(c.previous, None);
|
||||
assert_eq!(c.next, None);
|
||||
assert_matches!(c.content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
check_test_event(&events[0], "hello");
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_start_end_reattach_items(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
|
||||
// Same updates and checks as test_linked_chunk_push_items, but with extra
|
||||
// `StartReattachItems` and `EndReattachItems` updates, which must have no
|
||||
// effects.
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![
|
||||
make_test_event(room_id, "hello"),
|
||||
make_test_event(room_id, "world"),
|
||||
make_test_event(room_id, "howdy"),
|
||||
],
|
||||
},
|
||||
Update::StartReattachItems,
|
||||
Update::EndReattachItems,
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = self.load_all_chunks(linked_chunk_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunks.len(), 1);
|
||||
|
||||
let c = chunks.remove(0);
|
||||
assert_eq!(c.identifier, CId::new(42));
|
||||
assert_eq!(c.previous, None);
|
||||
assert_eq!(c.next, None);
|
||||
assert_matches!(c.content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "hello");
|
||||
check_test_event(&events[1], "world");
|
||||
check_test_event(&events[2], "howdy");
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_clear(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_0 = make_test_event(room_id, "hello");
|
||||
let event_1 = make_test_event(room_id, "world");
|
||||
let event_2 = make_test_event(room_id, "howdy");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(54),
|
||||
next: None,
|
||||
gap: Gap { token: "fondue".to_owned() },
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![event_0.clone(), event_1, event_2],
|
||||
},
|
||||
Update::Clear,
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chunks = self.load_all_chunks(linked_chunk_id).await.unwrap();
|
||||
assert!(chunks.is_empty());
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_clear_and_reinsert(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let event_0 = make_test_event(room_id, "hello");
|
||||
let event_1 = make_test_event(room_id, "world");
|
||||
let event_2 = make_test_event(room_id, "howdy");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(54),
|
||||
next: None,
|
||||
gap: Gap { token: "fondue".to_owned() },
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![event_0.clone(), event_1, event_2],
|
||||
},
|
||||
Update::Clear,
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chunks = self.load_all_chunks(linked_chunk_id).await.unwrap();
|
||||
assert!(chunks.is_empty());
|
||||
|
||||
// It's okay to re-insert a past event.
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(42), 0), items: vec![event_0] },
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn test_rebuild_empty_linked_chunk(&self) {
|
||||
// When I rebuild a linked chunk from an empty store, it's empty.
|
||||
let linked_chunk = lazy_loader::from_all_chunks::<3, _, _>(
|
||||
@@ -549,6 +1110,66 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(linked_chunk.is_none());
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_multiple_rooms(&self) {
|
||||
let room1 = room_id!("!realcheeselovers:raclette.fr");
|
||||
let linked_chunk_id1 = LinkedChunkId::Room(room1);
|
||||
let room2 = room_id!("!realcheeselovers:fondue.ch");
|
||||
let linked_chunk_id2 = LinkedChunkId::Room(room2);
|
||||
|
||||
// Check that applying updates to one room doesn't affect the others.
|
||||
// Use the same chunk identifier in both rooms to battle-test search.
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id1,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![
|
||||
make_test_event(room1, "best cheese is raclette"),
|
||||
make_test_event(room1, "obviously"),
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
linked_chunk_id2,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(42), 0),
|
||||
items: vec![make_test_event(room1, "beaufort is the best")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check chunks from room 1.
|
||||
let mut chunks_room1 = self.load_all_chunks(linked_chunk_id1).await.unwrap();
|
||||
assert_eq!(chunks_room1.len(), 1);
|
||||
|
||||
let c = chunks_room1.remove(0);
|
||||
assert_matches!(c.content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "best cheese is raclette");
|
||||
check_test_event(&events[1], "obviously");
|
||||
});
|
||||
|
||||
// Check chunks from room 2.
|
||||
let mut chunks_room2 = self.load_all_chunks(linked_chunk_id2).await.unwrap();
|
||||
assert_eq!(chunks_room2.len(), 1);
|
||||
|
||||
let c = chunks_room2.remove(0);
|
||||
assert_matches!(c.content, ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 1);
|
||||
check_test_event(&events[0], "beaufort is the best");
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_clear_all_linked_chunks(&self) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let linked_chunk_id0 = LinkedChunkId::Room(r0);
|
||||
@@ -582,7 +1203,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
|
||||
gap: Gap { token: "bleu d'auvergne".to_owned() },
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
@@ -709,7 +1330,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "brillat-savarin".to_owned() },
|
||||
gap: Gap { token: "brillat-savarin".to_owned() },
|
||||
},
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
Update::PushItems {
|
||||
@@ -740,12 +1361,12 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
self.filter_duplicated_events(
|
||||
linked_chunk_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
event_raclette.event_id().unwrap().to_owned(),
|
||||
event_morbier.event_id().unwrap().to_owned(),
|
||||
event_gruyere.event_id().unwrap().to_owned(),
|
||||
event_tome.event_id().unwrap().to_owned(),
|
||||
event_mont_dor.event_id().unwrap().to_owned(),
|
||||
event_comte.event_id().unwrap(),
|
||||
event_raclette.event_id().unwrap(),
|
||||
event_morbier.event_id().unwrap(),
|
||||
event_gruyere.event_id().unwrap(),
|
||||
event_tome.event_id().unwrap(),
|
||||
event_mont_dor.event_id().unwrap(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -768,6 +1389,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_filter_duplicate_events_no_events(&self) {
|
||||
let room_id = *DEFAULT_TEST_ROOM_ID;
|
||||
let linked_chunk_id = LinkedChunkId::Room(room_id);
|
||||
let duplicates = self.filter_duplicated_events(linked_chunk_id, Vec::new()).await.unwrap();
|
||||
assert!(duplicates.is_empty());
|
||||
}
|
||||
|
||||
async fn test_find_event(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
@@ -1198,10 +1826,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
let dups = self
|
||||
.filter_duplicated_events(
|
||||
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
|
||||
vec![
|
||||
thread1_ev.event_id().unwrap().to_owned(),
|
||||
room_ev.event_id().unwrap().to_owned(),
|
||||
],
|
||||
vec![thread1_ev.event_id().unwrap(), room_ev.event_id().unwrap()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1298,6 +1923,34 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_exists_before_referenced() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_exists_before_referenced().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_last_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_load_last_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_last_chunk_with_a_cycle() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_load_last_chunk_with_a_cycle().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_previous_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_load_previous_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_incremental_loading() {
|
||||
let event_cache_store =
|
||||
@@ -1305,6 +1958,55 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_linked_chunk_incremental_loading().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_remove_chunk() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_remove_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_replace_item() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_replace_item().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_remove_item() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_remove_item().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_detach_last_items() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_detach_last_items().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_start_end_reattach_items() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_start_end_reattach_items().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_clear() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_clear().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_clear_and_reinsert() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_clear_and_reinsert().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_rebuild_empty_linked_chunk() {
|
||||
let event_cache_store =
|
||||
@@ -1312,6 +2014,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_rebuild_empty_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_multiple_rooms() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_multiple_rooms().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_all_chunks_metadata() {
|
||||
let event_cache_store =
|
||||
@@ -1340,6 +2049,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_filter_duplicated_events().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_filter_duplicate_events_no_events() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_filter_duplicate_events_no_events().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event() {
|
||||
let event_cache_store =
|
||||
|
||||
@@ -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(Box::new(e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -271,5 +280,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() }
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
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};
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -53,13 +53,14 @@ mod tests {
|
||||
},
|
||||
},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
time::SystemTime,
|
||||
user_id,
|
||||
};
|
||||
use similar_asserts::assert_eq;
|
||||
|
||||
use super::super::{Room, RoomState};
|
||||
use crate::store::MemoryStore;
|
||||
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 +100,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,7 +122,7 @@ mod tests {
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
init_data: Option<InitData<'_>>,
|
||||
) -> AnySyncStateEvent {
|
||||
) -> Raw<AnySyncStateEvent> {
|
||||
let application = Application::Call(CallApplicationContent::new(
|
||||
"my_call_id_1".to_owned(),
|
||||
ruma::events::call::member::CallScope::Room,
|
||||
@@ -178,11 +179,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 +211,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 +242,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,7 +276,7 @@ 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());
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,8 @@ mod state;
|
||||
mod tags;
|
||||
mod tombstone;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::RwLock as SyncRwLock;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
collections::{BTreeMap, BTreeSet, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -39,13 +37,11 @@ pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
|
||||
pub use encryption::EncryptionState;
|
||||
use eyeball::{AsyncLock, SharedObservable};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::ring_buffer::RingBuffer;
|
||||
pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
|
||||
pub(crate) use room_info::SyncInfo;
|
||||
pub use room_info::{
|
||||
BaseRoomInfo, InviteAcceptanceDetails, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomRecencyStamp, apply_redaction,
|
||||
BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomRecencyStamp,
|
||||
apply_redaction,
|
||||
};
|
||||
use ruma::{
|
||||
EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
|
||||
@@ -63,8 +59,6 @@ use ruma::{
|
||||
},
|
||||
room::RoomType,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use state::{RoomState, RoomStateFilter};
|
||||
pub(crate) use tags::RoomNotableTags;
|
||||
@@ -73,7 +67,7 @@ pub use tombstone::{PredecessorRoom, SuccessorRoom};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use crate::{
|
||||
Error, MinimalStateEvent,
|
||||
Error,
|
||||
deserialized_responses::MemberEvent,
|
||||
notification_settings::RoomNotificationMode,
|
||||
read_receipts::RoomReadReceipts,
|
||||
@@ -92,20 +86,14 @@ pub struct Room {
|
||||
pub(super) own_user_id: OwnedUserId,
|
||||
|
||||
pub(super) info: SharedObservable<RoomInfo>,
|
||||
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
pub(super) store: Arc<DynStateStore>,
|
||||
|
||||
/// 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.)
|
||||
/// A clone of the [`BaseStateStore::room_info_notable_update_sender`].
|
||||
///
|
||||
/// Currently, these are held in Room rather than RoomInfo, because we were
|
||||
/// not sure whether holding too many of them might make the cache too
|
||||
/// slow to load on startup. Keeping them here means they are not cached
|
||||
/// to disk but held in memory.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
|
||||
/// [`BaseStateStore::room_info_notable_update_sender`]: crate::store::BaseStateStore::room_info_notable_update_sender
|
||||
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
|
||||
/// A clone of the state store.
|
||||
pub(super) store: Arc<DynStateStore>,
|
||||
|
||||
/// A map for ids of room membership events in the knocking state linked to
|
||||
/// the user id of the user affected by the member event, that the current
|
||||
@@ -141,10 +129,6 @@ impl Room {
|
||||
room_id: room_info.room_id.clone(),
|
||||
store,
|
||||
info: SharedObservable::new(room_info),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
|
||||
Self::MAX_ENCRYPTED_EVENTS,
|
||||
))),
|
||||
room_info_notable_update_sender,
|
||||
seen_knock_request_ids_map: SharedObservable::new_async(None),
|
||||
room_member_updates_sender,
|
||||
@@ -266,10 +250,7 @@ impl Room {
|
||||
/// redacted, all fields except `creator` will be set to their default
|
||||
/// value.
|
||||
pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
|
||||
match self.info.read().base_info.create.as_ref()? {
|
||||
MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
|
||||
MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
|
||||
}
|
||||
Some(self.info.read().base_info.create.as_ref()?.content.clone())
|
||||
}
|
||||
|
||||
/// Is this room considered a direct message.
|
||||
@@ -361,6 +342,11 @@ impl Room {
|
||||
self.info.read().base_info.max_power_level
|
||||
}
|
||||
|
||||
/// Get the service members in this room, if available.
|
||||
pub fn service_members(&self) -> Option<BTreeSet<OwnedUserId>> {
|
||||
self.info.read().service_members().cloned()
|
||||
}
|
||||
|
||||
/// Get the current power levels of this room.
|
||||
pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
|
||||
let power_levels_content = self
|
||||
@@ -496,23 +482,12 @@ impl Room {
|
||||
self.info.read().recency_stamp
|
||||
}
|
||||
|
||||
/// Returns the details about an invite to this room if the invite has been
|
||||
/// accepted by this specific client.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Some` if an invite has been accepted by this specific client.
|
||||
/// - `None` if we didn't join this room using an invite or the invite
|
||||
/// wasn't accepted by this client.
|
||||
pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
|
||||
self.info.read().invite_acceptance_details.clone()
|
||||
}
|
||||
|
||||
/// Get a `Stream` of loaded pinned events for this room.
|
||||
/// If no pinned events are found a single empty `Vec` will be returned.
|
||||
pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
|
||||
self.info
|
||||
.subscribe()
|
||||
.map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
|
||||
.map(|i| i.base_info.pinned_events.and_then(|c| c.pinned).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns the current pinned event ids for this room.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,7 @@ bitflags! {
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
|
||||
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
events::tag::{TagInfo, TagName, Tags},
|
||||
@@ -92,7 +93,7 @@ mod tests {
|
||||
async fn test_is_favourite() {
|
||||
// Given a room,
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
@@ -188,7 +189,7 @@ mod tests {
|
||||
async fn test_is_low_priority() {
|
||||
// Given a room,
|
||||
let client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
|
||||
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
|
||||
ThreadingSupport::Disabled,
|
||||
);
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::ops::Not;
|
||||
|
||||
use ruma::{OwnedRoomId, events::room::tombstone::RoomTombstoneEventContent};
|
||||
use ruma::{OwnedRoomId, events::room::tombstone::PossiblyRedactedRoomTombstoneEventContent};
|
||||
|
||||
use super::Room;
|
||||
|
||||
@@ -36,7 +34,7 @@ impl Room {
|
||||
/// event has been received. It's faster than using this method.
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn tombstone_content(&self) -> Option<RoomTombstoneEventContent> {
|
||||
pub fn tombstone_content(&self) -> Option<PossiblyRedactedRoomTombstoneEventContent> {
|
||||
self.info.read().tombstone().cloned()
|
||||
}
|
||||
|
||||
@@ -48,9 +46,11 @@ impl Room {
|
||||
///
|
||||
/// [`m.room.tombstone`]: https://spec.matrix.org/v1.14/client-server-api/#mroomtombstone
|
||||
pub fn successor_room(&self) -> Option<SuccessorRoom> {
|
||||
self.tombstone_content().map(|tombstone_event| SuccessorRoom {
|
||||
room_id: tombstone_event.replacement_room,
|
||||
reason: tombstone_event.body.is_empty().not().then_some(tombstone_event.body),
|
||||
self.tombstone_content().and_then(|tombstone_event| {
|
||||
Some(SuccessorRoom {
|
||||
room_id: tombstone_event.replacement_room?,
|
||||
reason: tombstone_event.body.filter(|body| !body.is_empty()),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ impl BaseClient {
|
||||
|
||||
let olm_machine = self.olm_machine().await;
|
||||
|
||||
let mut context = processors::Context::default();
|
||||
let context = processors::Context::default();
|
||||
|
||||
let processors::e2ee::to_device::Output { processed_to_device_events, room_key_updates } =
|
||||
let processors::e2ee::to_device::Output { processed_to_device_events } =
|
||||
processors::e2ee::to_device::from_msc4186(
|
||||
to_device,
|
||||
e2ee,
|
||||
@@ -74,21 +74,6 @@ impl BaseClient {
|
||||
)
|
||||
.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?;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&self.state_store,
|
||||
@@ -143,7 +128,7 @@ impl BaseClient {
|
||||
|
||||
let user_id = self
|
||||
.session_meta()
|
||||
.expect("Sliding sync shouldn't run without an authenticated user.")
|
||||
.expect("Sliding sync shouldn't run without an authenticated user")
|
||||
.user_id
|
||||
.to_owned();
|
||||
|
||||
@@ -153,7 +138,6 @@ impl BaseClient {
|
||||
&user_id,
|
||||
processors::room::RoomCreationData::new(
|
||||
room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
@@ -320,31 +304,21 @@ impl BaseClient {
|
||||
#[cfg(all(test, not(target_family = "wasm")))]
|
||||
mod tests {
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::{Arc, RwLock as SyncRwLock};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{UnableToDecryptInfo, UnableToDecryptReason},
|
||||
ring_buffer::RingBuffer,
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
JsOption, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
|
||||
api::client::sync::sync_events::UnreadNotificationsCount,
|
||||
assign, event_id,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent, GlobalAccountDataEventContent,
|
||||
StateEventContent, StateEventType,
|
||||
GlobalAccountDataEventContent, StateEventContent, StateEventType,
|
||||
direct::{DirectEventContent, DirectUserIdentifier, OwnedDirectUserIdentifier},
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
encryption::RoomEncryptionEventContent,
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
message::SyncRoomMessageEvent,
|
||||
name::RoomNameEventContent,
|
||||
pinned_events::RoomPinnedEventsEventContent,
|
||||
},
|
||||
@@ -356,8 +330,6 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::http;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::processors::room::msc4186::cache_latest_events;
|
||||
use crate::{
|
||||
BaseClient, EncryptionState, RequestedRequiredStates, RoomInfoNotableUpdate, RoomState,
|
||||
SessionMeta,
|
||||
@@ -366,8 +338,67 @@ mod tests {
|
||||
store::{RoomLoadSettings, StoreConfig},
|
||||
test_utils::logged_in_base_client,
|
||||
};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::{Room, store::MemoryStore};
|
||||
|
||||
#[async_test]
|
||||
async fn test_invited_state_without_update_emits_invited_room() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!invite:e.uk");
|
||||
let user_id = client.session_meta().unwrap().user_id.to_owned();
|
||||
|
||||
let mut room = http::response::Room::new();
|
||||
room.invite_state = Some(invite_state_for(&user_id, MembershipState::Invite));
|
||||
|
||||
let response = response_with_room(room_id, room);
|
||||
|
||||
let sync_resp = client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(sync_resp.rooms.invited.contains_key(room_id));
|
||||
}
|
||||
|
||||
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
|
||||
use ruma::events::AnyStrippedStateEvent;
|
||||
|
||||
fn invite_state_for(
|
||||
user_id: &UserId,
|
||||
membership: MembershipState,
|
||||
) -> Vec<Raw<AnyStrippedStateEvent>> {
|
||||
let content = RoomMemberEventContent::new(membership);
|
||||
|
||||
let raw: Raw<AnyStrippedStateEvent> = Raw::from_json_string(
|
||||
serde_json::json!({
|
||||
"type": "m.room.member",
|
||||
"state_key": user_id,
|
||||
"content": content,
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
vec![raw]
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_knocked_state_emits_invited_room() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!knock:e.uk");
|
||||
let user_id = client.session_meta().unwrap().user_id.to_owned();
|
||||
|
||||
let mut room = http::response::Room::new();
|
||||
room.invite_state = Some(invite_state_for(&user_id, MembershipState::Knock));
|
||||
|
||||
let response = response_with_room(room_id, room);
|
||||
|
||||
let sync_resp = client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// ✅ Knocked rooms surface as invited rooms in Sliding Sync
|
||||
assert!(sync_resp.rooms.invited.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_notification_count_set() {
|
||||
@@ -927,7 +958,7 @@ mod tests {
|
||||
// When I send sliding sync response containing a room with an avatar
|
||||
let room = {
|
||||
let mut room = http::response::Room::new();
|
||||
room.avatar = JsOption::from_option(Some(mxc_uri!("mxc://e.uk/med1").to_owned()));
|
||||
room.avatar = JsOption::from_option(Some(owned_mxc_uri!("mxc://e.uk/med1")));
|
||||
|
||||
room
|
||||
};
|
||||
@@ -956,7 +987,7 @@ mod tests {
|
||||
// When I send sliding sync response containing a room with an avatar
|
||||
let room = {
|
||||
let mut room = http::response::Room::new();
|
||||
room.avatar = JsOption::from_option(Some(mxc_uri!("mxc://e.uk/med1").to_owned()));
|
||||
room.avatar = JsOption::from_option(Some(owned_mxc_uri!("mxc://e.uk/med1")));
|
||||
|
||||
room
|
||||
};
|
||||
@@ -1040,6 +1071,8 @@ mod tests {
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
let mut room_info_notable_update = client.room_info_notable_update_receiver();
|
||||
|
||||
// When I send sliding sync response containing an invited room
|
||||
let mut room = http::response::Room::new();
|
||||
set_room_invited(&mut room, user_id, user_id);
|
||||
@@ -1057,6 +1090,70 @@ mod tests {
|
||||
// And it is added to the list of invited rooms, not the joined ones
|
||||
assert!(!sync_resp.rooms.invited[room_id].invite_state.is_empty());
|
||||
assert!(!sync_resp.rooms.joined.contains_key(room_id));
|
||||
|
||||
assert_matches!(
|
||||
room_info_notable_update.recv().await,
|
||||
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
|
||||
assert_eq!(received_room_id, room_id);
|
||||
// The reason we are looking for :-].
|
||||
assert!(reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
|
||||
}
|
||||
);
|
||||
assert_matches!(
|
||||
room_info_notable_update.recv().await,
|
||||
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
|
||||
assert_eq!(received_room_id, room_id);
|
||||
// The reason we are looking for :-].
|
||||
assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
|
||||
}
|
||||
);
|
||||
assert!(room_info_notable_update.is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_knock_room_is_added_to_client_and_knock_list() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
let mut room_info_notable_update = client.room_info_notable_update_receiver();
|
||||
|
||||
// When I send sliding sync response containing an invited room
|
||||
let mut room = http::response::Room::new();
|
||||
set_room_knocked(&mut room, user_id);
|
||||
let response = response_with_room(room_id, room);
|
||||
let sync_resp = client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// Then the room is added to the client
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(client_room.room_id(), room_id);
|
||||
assert_eq!(client_room.state(), RoomState::Knocked);
|
||||
|
||||
// And it is added to the list of invited rooms, not the joined ones
|
||||
assert!(!sync_resp.rooms.knocked[room_id].knock_state.is_empty());
|
||||
assert!(!sync_resp.rooms.joined.contains_key(room_id));
|
||||
|
||||
assert_matches!(
|
||||
room_info_notable_update.recv().await,
|
||||
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
|
||||
assert_eq!(received_room_id, room_id);
|
||||
// The reason we are looking for :-].
|
||||
assert!(reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
|
||||
}
|
||||
);
|
||||
assert_matches!(
|
||||
room_info_notable_update.recv().await,
|
||||
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
|
||||
assert_eq!(received_room_id, room_id);
|
||||
// The reason we are looking for :-].
|
||||
assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
|
||||
}
|
||||
);
|
||||
assert!(room_info_notable_update.is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -1178,7 +1275,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let client = {
|
||||
let store = StoreConfig::new("cross-process-foo".to_owned());
|
||||
let store = StoreConfig::new(CrossProcessLockConfig::SingleProcess);
|
||||
state_store = store.state_store.clone();
|
||||
|
||||
let client = BaseClient::new(store, ThreadingSupport::Disabled);
|
||||
@@ -1210,7 +1307,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let client = {
|
||||
let mut store = StoreConfig::new("cross-process-foo".to_owned());
|
||||
let mut store = StoreConfig::new(CrossProcessLockConfig::SingleProcess);
|
||||
store.state_store = state_store;
|
||||
let client = BaseClient::new(store, ThreadingSupport::Disabled);
|
||||
client
|
||||
@@ -1236,8 +1333,8 @@ mod tests {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let gordon = user_id!("@gordon:e.uk").to_owned();
|
||||
let alice = user_id!("@alice:e.uk").to_owned();
|
||||
let gordon = owned_user_id!("@gordon:e.uk");
|
||||
let alice = owned_user_id!("@alice:e.uk");
|
||||
|
||||
// When I send sliding sync response containing a room (with identifiable data
|
||||
// in `heroes`)
|
||||
@@ -1280,502 +1377,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_last_event_from_sliding_sync_is_cached() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let event_a = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"type":"m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content":{"body":"A", "msgtype": "m.text"}
|
||||
});
|
||||
let event_b = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"type":"m.room.message",
|
||||
"event_id": "$idb",
|
||||
"origin_server_ts": 12344447,
|
||||
"content":{"body":"B", "msgtype": "m.text"}
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let events = &[event_a, event_b.clone()];
|
||||
let room = room_with_timeline(events);
|
||||
let response = response_with_room(room_id, room);
|
||||
client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// Then the room holds the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(
|
||||
ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
"$idb"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_last_knock_event_from_sliding_sync_is_cached_if_user_has_permissions() {
|
||||
let own_user_id = user_id!("@me:e.uk");
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(Some(own_user_id)).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// The room create event.
|
||||
let create = json!({
|
||||
"sender":"@ignacio:example.com",
|
||||
"state_key":"",
|
||||
"type":"m.room.create",
|
||||
"event_id": "$idc",
|
||||
"origin_server_ts": 12344415,
|
||||
"content":{ "room_version": "11" },
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// Give the current user invite or kick permissions in this room
|
||||
let power_levels = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"",
|
||||
"type":"m.room.power_levels",
|
||||
"event_id": "$idb",
|
||||
"origin_server_ts": 12344445,
|
||||
"content":{ "invite": 100, "kick": 100, "users": { own_user_id: 100 } },
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// And a knock member state event
|
||||
let knock_event = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"@alice:example.com",
|
||||
"type":"m.room.member",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content":{"membership": "knock"},
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let events = &[knock_event];
|
||||
let mut room = room_with_timeline(events);
|
||||
room.required_state.extend([
|
||||
Raw::new(&create).unwrap().cast_unchecked(),
|
||||
Raw::new(&power_levels).unwrap().cast_unchecked(),
|
||||
]);
|
||||
let response = response_with_room(room_id, room);
|
||||
client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// Then the room holds the latest knock state event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(
|
||||
ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
"$ida"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_last_knock_event_from_sliding_sync_is_not_cached_without_permissions() {
|
||||
let own_user_id = user_id!("@me:e.uk");
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(Some(own_user_id)).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// Set the user as a user with no permission to invite or kick other users in
|
||||
// this room
|
||||
let power_levels = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"",
|
||||
"type":"m.room.power_levels",
|
||||
"event_id": "$idb",
|
||||
"origin_server_ts": 12344445,
|
||||
"content":{ "invite": 50, "kick": 50, "users": { own_user_id: 0 } },
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// And a knock member state event
|
||||
let knock_event = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"@alice:example.com",
|
||||
"type":"m.room.member",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content":{"membership": "knock"},
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let events = &[knock_event];
|
||||
let mut room = room_with_timeline(events);
|
||||
room.required_state.push(Raw::new(&power_levels).unwrap().cast_unchecked());
|
||||
let response = response_with_room(room_id, room);
|
||||
client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// Then the room doesn't hold the knock state event as the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert!(client_room.latest_event().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_last_non_knock_member_state_event_from_sliding_sync_is_not_cached() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
// And a join member state event
|
||||
let join_event = json!({
|
||||
"sender":"@alice:example.com",
|
||||
"state_key":"@alice:example.com",
|
||||
"type":"m.room.member",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content":{"membership": "join"},
|
||||
"room_id": room_id,
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let events = &[join_event];
|
||||
let room = room_with_timeline(events);
|
||||
let response = response_with_room(room_id, room);
|
||||
client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// Then the room doesn't hold the join state event as the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert!(client_room.latest_event().is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_cached_latest_event_can_be_redacted() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let event_a = json!({
|
||||
"sender": "@alice:example.com",
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
});
|
||||
|
||||
// When the sliding sync response contains a timeline
|
||||
let room = room_with_timeline(&[event_a]);
|
||||
let response = response_with_room(room_id, room);
|
||||
client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// Then the room holds the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(
|
||||
ev_id(client_room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
"$ida"
|
||||
);
|
||||
|
||||
let redaction = json!({
|
||||
"sender": "@alice:example.com",
|
||||
"type": "m.room.redaction",
|
||||
"event_id": "$idb",
|
||||
"redacts": "$ida",
|
||||
"origin_server_ts": 12344448,
|
||||
"content": {},
|
||||
});
|
||||
|
||||
// When a redaction for that event is received
|
||||
let room = room_with_timeline(&[redaction]);
|
||||
let response = response_with_room(room_id, room);
|
||||
client
|
||||
.process_sliding_sync(&response, &RequestedRequiredStates::default())
|
||||
.await
|
||||
.expect("Failed to process sync");
|
||||
|
||||
// Then the room still holds the latest event
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
let latest_event = client_room.latest_event().unwrap();
|
||||
assert_eq!(latest_event.event_id().unwrap(), "$ida");
|
||||
|
||||
// But it's now redacted
|
||||
assert_matches!(
|
||||
latest_event.event().raw().deserialize().unwrap(),
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Redacted(_)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_when_no_events_we_dont_cache_any() {
|
||||
let events = &[];
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
assert!(chosen.is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_when_only_one_event_we_cache_it() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let events = std::slice::from_ref(&event1);
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
assert_eq!(ev_id(chosen), rawev_id(event1));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_with_multiple_events_we_cache_the_last_one() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
let events = &[event1, event2.clone()];
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
assert_eq!(ev_id(chosen), rawev_id(event2));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
let event3 = make_event("m.room.powerlevels", "$3");
|
||||
let event4 = make_event("m.room.powerlevels", "$5");
|
||||
let events = &[event1, event2.clone(), event3, event4];
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
assert_eq!(ev_id(chosen), rawev_id(event2));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_prefer_to_cache_nothing_rather_than_irrelevant_events() {
|
||||
let event1 = make_event("m.room.power_levels", "$1");
|
||||
let events = &[event1];
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
assert!(chosen.is_none());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_cache_encrypted_events_that_are_after_latest_message() {
|
||||
// Given two message events followed by two encrypted
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
let event3 = make_encrypted_event("$3");
|
||||
let event4 = make_encrypted_event("$4");
|
||||
let events = &[event1, event2.clone(), event3.clone(), event4.clone()];
|
||||
|
||||
// When I ask to cache events
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
|
||||
// The latest message is stored
|
||||
assert_eq!(
|
||||
ev_id(room_info.latest_event.as_ref().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(event2.clone())
|
||||
);
|
||||
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
assert_eq!(
|
||||
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(event2)
|
||||
);
|
||||
|
||||
// And also the two encrypted ones
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event4]));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_dont_cache_encrypted_events_that_are_before_latest_message() {
|
||||
// Given an encrypted event before and after the message
|
||||
let event1 = make_encrypted_event("$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
let event3 = make_encrypted_event("$3");
|
||||
let events = &[event1, event2.clone(), event3.clone()];
|
||||
|
||||
// When I ask to cache events
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
|
||||
// The latest message is stored
|
||||
assert_eq!(
|
||||
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(event2)
|
||||
);
|
||||
|
||||
// And also the encrypted one that was after it, but not the one before
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3]));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_skip_irrelevant_events_eg_receipts_even_if_after_message() {
|
||||
// Given two message events followed by two encrypted, with a receipt in the
|
||||
// middle
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
let event3 = make_encrypted_event("$3");
|
||||
let event4 = make_event("m.read", "$4");
|
||||
let event5 = make_encrypted_event("$5");
|
||||
let events = &[event1, event2.clone(), event3.clone(), event4, event5.clone()];
|
||||
|
||||
// When I ask to cache events
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
|
||||
// The latest message is stored, ignoring the receipt
|
||||
assert_eq!(
|
||||
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(event2)
|
||||
);
|
||||
|
||||
// The two encrypted ones are stored, but not the receipt
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event5]));
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_only_store_the_max_number_of_encrypted_events() {
|
||||
// Given two message events followed by lots of encrypted and other irrelevant
|
||||
// events
|
||||
let evente = make_event("m.room.message", "$e");
|
||||
let eventd = make_event("m.room.message", "$d");
|
||||
let eventc = make_encrypted_event("$c");
|
||||
let event9 = make_encrypted_event("$9");
|
||||
let event8 = make_encrypted_event("$8");
|
||||
let event7 = make_encrypted_event("$7");
|
||||
let eventb = make_event("m.read", "$b");
|
||||
let event6 = make_encrypted_event("$6");
|
||||
let event5 = make_encrypted_event("$5");
|
||||
let event4 = make_encrypted_event("$4");
|
||||
let event3 = make_encrypted_event("$3");
|
||||
let event2 = make_encrypted_event("$2");
|
||||
let eventa = make_event("m.read", "$a");
|
||||
let event1 = make_encrypted_event("$1");
|
||||
let event0 = make_encrypted_event("$0");
|
||||
let events = &[
|
||||
evente,
|
||||
eventd.clone(),
|
||||
eventc,
|
||||
event9.clone(),
|
||||
event8.clone(),
|
||||
event7.clone(),
|
||||
eventb,
|
||||
event6.clone(),
|
||||
event5.clone(),
|
||||
event4.clone(),
|
||||
event3.clone(),
|
||||
event2.clone(),
|
||||
eventa,
|
||||
event1.clone(),
|
||||
event0.clone(),
|
||||
];
|
||||
|
||||
// When I ask to cache events
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
|
||||
// The latest message is stored, ignoring encrypted and receipts
|
||||
assert_eq!(
|
||||
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(eventd)
|
||||
);
|
||||
|
||||
// Only 10 encrypted are stored, even though there were more
|
||||
assert_eq!(
|
||||
rawevs_ids(&room.latest_encrypted_events),
|
||||
evs_ids(&[
|
||||
event9, event8, event7, event6, event5, event4, event3, event2, event1, event0
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_dont_overflow_capacity_if_previous_encrypted_events_exist() {
|
||||
// Given a RoomInfo with lots of encrypted events already inside it
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(
|
||||
&room,
|
||||
&mut room_info,
|
||||
&[
|
||||
make_encrypted_event("$0"),
|
||||
make_encrypted_event("$1"),
|
||||
make_encrypted_event("$2"),
|
||||
make_encrypted_event("$3"),
|
||||
make_encrypted_event("$4"),
|
||||
make_encrypted_event("$5"),
|
||||
make_encrypted_event("$6"),
|
||||
make_encrypted_event("$7"),
|
||||
make_encrypted_event("$8"),
|
||||
make_encrypted_event("$9"),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
|
||||
// Sanity: room_info has 10 encrypted events inside it
|
||||
assert_eq!(room.latest_encrypted_events.read().unwrap().len(), 10);
|
||||
|
||||
// When I ask to cache more encrypted events
|
||||
let eventa = make_encrypted_event("$a");
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, &[eventa], None, None).await;
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
|
||||
// The oldest event is gone
|
||||
assert!(!rawevs_ids(&room.latest_encrypted_events).contains(&"$0".to_owned()));
|
||||
|
||||
// The newest event is last in the list
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events)[9], "$a");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_existing_encrypted_events_are_deleted_if_we_receive_unencrypted() {
|
||||
// Given a RoomInfo with some encrypted events already inside it
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(
|
||||
&room,
|
||||
&mut room_info,
|
||||
&[make_encrypted_event("$0"), make_encrypted_event("$1"), make_encrypted_event("$2")],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
room.set_room_info(room_info.clone(), RoomInfoNotableUpdateReasons::empty());
|
||||
|
||||
// When I ask to cache an unencrypted event, and some more encrypted events
|
||||
let eventa = make_event("m.room.message", "$a");
|
||||
let eventb = make_encrypted_event("$b");
|
||||
cache_latest_events(&room, &mut room_info, &[eventa, eventb], None, None).await;
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
|
||||
// The only encrypted events stored are the ones after the decrypted one
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), &["$b"]);
|
||||
|
||||
// The decrypted one is stored as the latest
|
||||
assert_eq!(rawev_id(room.latest_event().unwrap().event().clone()), "$a");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_recency_stamp_is_found_when_processing_sliding_sync_response() {
|
||||
// Given a logged-in client
|
||||
@@ -2507,93 +2108,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
async fn choose_event_to_cache(events: &[TimelineEvent]) -> Option<TimelineEvent> {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info, RoomInfoNotableUpdateReasons::empty());
|
||||
room.latest_event().map(|latest_event| latest_event.event().clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn rawev_id(event: TimelineEvent) -> String {
|
||||
event.event_id().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn ev_id(event: Option<TimelineEvent>) -> String {
|
||||
event.unwrap().event_id().unwrap().to_string()
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn rawevs_ids(events: &Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>) -> Vec<String> {
|
||||
events.read().unwrap().iter().map(|e| e.get_field("event_id").unwrap().unwrap()).collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn evs_ids(events: &[TimelineEvent]) -> Vec<String> {
|
||||
events.iter().map(|e| e.event_id().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_room() -> Room {
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
Room::new(
|
||||
user_id!("@u:e.co"),
|
||||
Arc::new(MemoryStore::new()),
|
||||
room_id!("!r:e.co"),
|
||||
RoomState::Joined,
|
||||
sender,
|
||||
)
|
||||
}
|
||||
|
||||
fn make_raw_event(event_type: &str, id: &str) -> Raw<AnySyncTimelineEvent> {
|
||||
Raw::from_json_string(
|
||||
json!({
|
||||
"type": event_type,
|
||||
"event_id": id,
|
||||
"content": { "msgtype": "m.text", "body": "my msg" },
|
||||
"sender": "@u:h.uk",
|
||||
"origin_server_ts": 12344445,
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_event(event_type: &str, id: &str) -> TimelineEvent {
|
||||
TimelineEvent::from_plaintext(make_raw_event(event_type, id))
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_encrypted_event(id: &str) -> TimelineEvent {
|
||||
TimelineEvent::from_utd(
|
||||
Raw::from_json_string(
|
||||
json!({
|
||||
"type": "m.room.encrypted",
|
||||
"event_id": id,
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"ciphertext": "",
|
||||
"sender_key": "",
|
||||
"device_id": "",
|
||||
"session_id": "",
|
||||
},
|
||||
"sender": "@u:h.uk",
|
||||
"origin_server_ts": 12344445,
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
UnableToDecryptInfo {
|
||||
session_id: Some("".to_owned()),
|
||||
reason: UnableToDecryptReason::MissingMegolmSession { withheld_code: None },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn membership(
|
||||
client: &BaseClient,
|
||||
room_id: &RoomId,
|
||||
@@ -2725,17 +2239,6 @@ mod tests {
|
||||
room
|
||||
}
|
||||
|
||||
fn room_with_timeline(events: &[serde_json::Value]) -> http::response::Room {
|
||||
let mut room = http::response::Room::new();
|
||||
room.timeline.extend(
|
||||
events
|
||||
.iter()
|
||||
.map(|e| Raw::from_json_string(e.to_string()).unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
room
|
||||
}
|
||||
|
||||
fn set_room_name(room: &mut http::response::Room, sender: &UserId, name: String) {
|
||||
room.required_state.push(make_state_event(
|
||||
sender,
|
||||
|
||||
@@ -193,18 +193,15 @@ impl AmbiguityCache {
|
||||
let display_name = if let Some(d) = changes
|
||||
.profiles
|
||||
.get(room_id)
|
||||
.and_then(|p| p.get(user_id)?.as_original()?.content.displayname.as_deref())
|
||||
.and_then(|p| p.get(user_id)?.content.displayname.as_deref())
|
||||
{
|
||||
Some(d.to_owned())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, user_id)
|
||||
.await?
|
||||
.and_then(|p| p.into_original()?.content.displayname)
|
||||
} else if let Some(d) =
|
||||
self.store.get_profile(room_id, user_id).await?.and_then(|p| p.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
old_event.original_content().and_then(|c| c.displayname.clone())
|
||||
old_event.displayname_value().map(ToOwned::to_owned)
|
||||
};
|
||||
|
||||
Ok(Some(display_name.unwrap_or_else(|| user_id.localpart().to_owned())))
|
||||
@@ -315,7 +312,7 @@ mod test {
|
||||
"displayname": display_name,
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": EventId::new(server_name),
|
||||
"event_id": EventId::new_v1(server_name),
|
||||
"origin_server_ts": 152037280,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
//! Trait and macro of integration tests for StateStore implementations.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use growable_bloom_filter::GrowableBloomBuilder;
|
||||
use matrix_sdk_test::{TestResult, event_factory::EventFactory, test_json};
|
||||
use matrix_sdk_test::{TestResult, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
api::{
|
||||
FeatureFlag, MatrixVersion,
|
||||
client::discovery::discover_homeserver::{HomeserverInfo, RtcFocusInfo},
|
||||
@@ -26,17 +29,20 @@ use ruma::{
|
||||
},
|
||||
message::RoomMessageEventContent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
tag::{TagInfo, TagName, Tags, UserTagName},
|
||||
},
|
||||
owned_event_id, owned_mxc_uri,
|
||||
mxc_uri, owned_event_id, owned_mxc_uri,
|
||||
presence::PresenceState,
|
||||
push::Ruleset,
|
||||
room_id,
|
||||
room_version_rules::AuthorizationRules,
|
||||
serde::Raw,
|
||||
uint, user_id,
|
||||
};
|
||||
use serde_json::{json, value::Value as JsonValue};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
DependentQueuedRequestKind, DisplayName, DynStateStore, RoomLoadSettings,
|
||||
@@ -49,6 +55,7 @@ use crate::{
|
||||
ChildTransactionId, QueueWedgeError, SerializableEventContent, StateStoreExt,
|
||||
StoredThreadSubscription, ThreadSubscriptionStatus,
|
||||
},
|
||||
utils::RawStateEventWithKeys,
|
||||
};
|
||||
|
||||
/// `StateStore` integration tests.
|
||||
@@ -109,13 +116,12 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_get_room_infos(&self) -> TestResult;
|
||||
/// Test loading thread subscriptions.
|
||||
async fn test_thread_subscriptions(&self) -> TestResult;
|
||||
/// Test thread subscription bumpstamp semantics.
|
||||
async fn test_thread_subscriptions_bumpstamps(&self) -> TestResult;
|
||||
/// Test thread subscriptions bulk upsert, including bumpstamp semantics.
|
||||
async fn test_thread_subscriptions_bulk_upsert(&self) -> TestResult;
|
||||
}
|
||||
|
||||
impl StateStoreIntegrationTests for DynStateStore {
|
||||
async fn populate(&self) -> TestResult {
|
||||
let f = EventFactory::new();
|
||||
let mut changes = StateChanges::default();
|
||||
|
||||
let user_id = user_id();
|
||||
@@ -125,11 +131,18 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
changes.sync_token = Some("t392-516_47314_0_7_1_1_1_11444_1".to_owned());
|
||||
|
||||
let presence_json: &JsonValue = &test_json::PRESENCE;
|
||||
let presence_raw = serde_json::from_value::<Raw<PresenceEvent>>(presence_json.clone())?;
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let presence_raw: Raw<PresenceEvent> = f
|
||||
.presence(PresenceState::Online)
|
||||
.avatar_url(mxc_uri!("mxc://localhost/wefuiwegh8742w"))
|
||||
.currently_active(false)
|
||||
.last_active_ago(1)
|
||||
.status_msg("Making cupcakes")
|
||||
.into();
|
||||
let presence_event = presence_raw.deserialize()?;
|
||||
changes.add_presence_event(presence_event, presence_raw);
|
||||
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let pushrules_raw: Raw<AnyGlobalAccountDataEvent> =
|
||||
f.push_rules(Ruleset::server_default(user_id)).into();
|
||||
let pushrules_event = pushrules_raw.deserialize()?;
|
||||
@@ -138,51 +151,24 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let mut room = RoomInfo::new(room_id, RoomState::Joined);
|
||||
room.mark_as_left();
|
||||
|
||||
let tag_json: &JsonValue = &test_json::TAG;
|
||||
let tag_raw = serde_json::from_value::<Raw<AnyRoomAccountDataEvent>>(tag_json.clone())?;
|
||||
let f = EventFactory::new().sender(user_id).room(room_id);
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::Favorite, TagInfo::new());
|
||||
tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
|
||||
let tag_raw: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
|
||||
let tag_event = tag_raw.deserialize()?;
|
||||
changes.add_room_account_data(room_id, tag_event, tag_raw);
|
||||
|
||||
let name_json: &JsonValue = &test_json::NAME;
|
||||
let name_raw = serde_json::from_value::<Raw<AnySyncStateEvent>>(name_json.clone())?;
|
||||
let f = EventFactory::new().sender(user_id).room(room_id);
|
||||
let name_raw: Raw<AnySyncStateEvent> = f.room_name("room name").into();
|
||||
let name_event = name_raw.deserialize()?;
|
||||
room.handle_state_event(&name_event);
|
||||
room.handle_state_event(
|
||||
&mut RawStateEventWithKeys::try_from_raw_state_event(name_raw.clone())
|
||||
.expect("generated state event should be valid"),
|
||||
);
|
||||
changes.add_state_event(room_id, name_event, name_raw);
|
||||
|
||||
let topic_json: &JsonValue = &test_json::TOPIC;
|
||||
let topic_raw = serde_json::from_value::<Raw<AnySyncStateEvent>>(topic_json.clone())?;
|
||||
let topic_event = topic_raw.deserialize()?;
|
||||
room.handle_state_event(&topic_event);
|
||||
changes.add_state_event(room_id, topic_event, topic_raw);
|
||||
|
||||
let mut room_ambiguity_map = HashMap::new();
|
||||
let mut room_profiles = BTreeMap::new();
|
||||
|
||||
let member_json: &JsonValue = &test_json::MEMBER;
|
||||
let member_event: SyncRoomMemberEvent = serde_json::from_value(member_json.clone())?;
|
||||
let displayname = DisplayName::new(
|
||||
member_event.as_original().unwrap().content.displayname.as_ref().unwrap(),
|
||||
);
|
||||
room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()]));
|
||||
room_profiles.insert(user_id.to_owned(), (&member_event).into());
|
||||
|
||||
let member_state_raw =
|
||||
serde_json::from_value::<Raw<AnySyncStateEvent>>(member_json.clone())?;
|
||||
let member_state_event = member_state_raw.deserialize()?;
|
||||
changes.add_state_event(room_id, member_state_event, member_state_raw);
|
||||
|
||||
let invited_member_json: &JsonValue = &test_json::MEMBER_INVITE;
|
||||
// FIXME: Should be stripped room member event
|
||||
let invited_member_event: SyncRoomMemberEvent =
|
||||
serde_json::from_value(invited_member_json.clone())?;
|
||||
room_ambiguity_map.entry(displayname).or_default().insert(invited_user_id.to_owned());
|
||||
room_profiles.insert(invited_user_id.to_owned(), (&invited_member_event).into());
|
||||
|
||||
let invited_member_state_raw =
|
||||
serde_json::from_value::<Raw<AnySyncStateEvent>>(invited_member_json.clone())?;
|
||||
let invited_member_state_event = invited_member_state_raw.deserialize()?;
|
||||
changes.add_state_event(room_id, invited_member_state_event, invited_member_state_raw);
|
||||
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let receipt_content = f
|
||||
.room(room_id)
|
||||
.read_receipts()
|
||||
@@ -190,33 +176,80 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
.into_content();
|
||||
changes.add_receipts(room_id, receipt_content);
|
||||
|
||||
let topic_event_id = topic_event_id();
|
||||
let topic_raw: Raw<AnySyncStateEvent> = EventFactory::new()
|
||||
.room(room_id)
|
||||
.sender(user_id)
|
||||
.room_topic("😀")
|
||||
.event_id(topic_event_id)
|
||||
.prev_content(RoomTopicEventContent::new("test".to_owned()))
|
||||
.into_raw_sync_state();
|
||||
let topic_event = topic_raw.deserialize()?;
|
||||
room.handle_state_event(
|
||||
&mut RawStateEventWithKeys::try_from_raw_state_event(topic_raw.clone())
|
||||
.expect("generated state event should be valid"),
|
||||
);
|
||||
changes.add_state_event(room_id, topic_event, topic_raw);
|
||||
|
||||
let mut room_ambiguity_map = HashMap::new();
|
||||
let mut room_profiles = BTreeMap::new();
|
||||
|
||||
let f = EventFactory::new().sender(user_id).room(room_id);
|
||||
let member_raw: Raw<SyncRoomMemberEvent> =
|
||||
f.member(user_id).display_name("example").previous(MembershipState::Invite).into();
|
||||
let member_event: SyncRoomMemberEvent = member_raw.deserialize()?;
|
||||
let displayname = DisplayName::new(
|
||||
member_event.as_original().unwrap().content.displayname.as_ref().unwrap(),
|
||||
);
|
||||
room_ambiguity_map.insert(displayname.clone(), BTreeSet::from([user_id.to_owned()]));
|
||||
room_profiles.insert(user_id.to_owned(), (&member_event).into());
|
||||
|
||||
let member_raw: Raw<AnySyncStateEvent> = member_raw.cast_unchecked();
|
||||
let member_state_event = member_raw.deserialize()?;
|
||||
changes.add_state_event(room_id, member_state_event, member_raw);
|
||||
|
||||
let f = EventFactory::new().sender(user_id).room(room_id);
|
||||
let invited_member_raw: Raw<SyncRoomMemberEvent> = f
|
||||
.member(invited_user_id)
|
||||
.invited(invited_user_id)
|
||||
.display_name("example")
|
||||
.avatar_url(mxc_uri!("mxc://localhost/SEsfnsuifSDFSSEF"))
|
||||
.reason("Looking for support")
|
||||
.into();
|
||||
// FIXME: Should be stripped room member event
|
||||
let invited_member_event: SyncRoomMemberEvent = invited_member_raw.deserialize()?;
|
||||
room_ambiguity_map.entry(displayname).or_default().insert(invited_user_id.to_owned());
|
||||
room_profiles.insert(invited_user_id.to_owned(), (&invited_member_event).into());
|
||||
|
||||
let invited_member_raw: Raw<AnySyncStateEvent> = invited_member_raw.cast_unchecked();
|
||||
let invited_member_state_event = invited_member_raw.deserialize()?;
|
||||
changes.add_state_event(room_id, invited_member_state_event, invited_member_raw);
|
||||
|
||||
changes.ambiguity_maps.insert(room_id.to_owned(), room_ambiguity_map);
|
||||
changes.profiles.insert(room_id.to_owned(), room_profiles);
|
||||
changes.add_room(room);
|
||||
|
||||
let mut stripped_room = RoomInfo::new(stripped_room_id, RoomState::Invited);
|
||||
|
||||
let stripped_name_json: &JsonValue = &test_json::NAME_STRIPPED;
|
||||
let stripped_name_raw =
|
||||
serde_json::from_value::<Raw<AnyStrippedStateEvent>>(stripped_name_json.clone())?;
|
||||
let stripped_name_event = stripped_name_raw.deserialize()?;
|
||||
stripped_room.handle_stripped_state_event(&stripped_name_event);
|
||||
let f = EventFactory::new().sender(user_id).room(stripped_room_id);
|
||||
let stripped_name_raw: Raw<AnyStrippedStateEvent> = f.room_name("room name").into();
|
||||
let mut stripped_name_event =
|
||||
RawStateEventWithKeys::try_from_raw_state_event(stripped_name_raw).unwrap();
|
||||
stripped_room.handle_stripped_state_event(&mut stripped_name_event);
|
||||
changes.stripped_state.insert(
|
||||
stripped_room_id.to_owned(),
|
||||
BTreeMap::from([(
|
||||
stripped_name_event.event_type(),
|
||||
BTreeMap::from([(
|
||||
stripped_name_event.state_key().to_owned(),
|
||||
stripped_name_raw.clone(),
|
||||
)]),
|
||||
stripped_name_event.event_type,
|
||||
BTreeMap::from([(stripped_name_event.state_key, stripped_name_event.raw)]),
|
||||
)]),
|
||||
);
|
||||
|
||||
changes.add_room(stripped_room);
|
||||
|
||||
let stripped_member_json: &JsonValue = &test_json::MEMBER_STRIPPED;
|
||||
let stripped_member_event = Raw::new(&stripped_member_json.clone())?.cast_unchecked();
|
||||
changes.add_stripped_member(stripped_room_id, user_id, stripped_member_event);
|
||||
let f = EventFactory::new().sender(user_id).room(stripped_room_id);
|
||||
let stripped_member_raw: Raw<StrippedRoomMemberEvent> =
|
||||
f.member(user_id).display_name("example").into();
|
||||
changes.add_stripped_member(stripped_room_id, user_id, stripped_member_raw);
|
||||
|
||||
self.save_changes(&changes).await?;
|
||||
|
||||
@@ -225,6 +258,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
async fn test_topic_redaction(&self) -> TestResult {
|
||||
let room_id = room_id();
|
||||
let f = EventFactory::new();
|
||||
self.populate().await?;
|
||||
|
||||
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
|
||||
@@ -245,12 +279,11 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
|
||||
let redaction_json: &JsonValue = &test_json::TOPIC_REDACTION;
|
||||
let redaction_evt: Raw<_> = serde_json::from_value(redaction_json.clone())
|
||||
.expect("topic redaction event making works");
|
||||
let redacted_event_id: OwnedEventId = redaction_evt.get_field("redacts")?.unwrap();
|
||||
let topic_event_id = topic_event_id();
|
||||
let redaction_evt: Raw<SyncRoomRedactionEvent> =
|
||||
f.room(room_id).sender(user_id()).redaction(topic_event_id).into_raw();
|
||||
|
||||
changes.add_redaction(room_id, &redacted_event_id, redaction_evt);
|
||||
changes.add_redaction(room_id, topic_event_id, redaction_evt);
|
||||
self.save_changes(&changes).await?;
|
||||
|
||||
let redacted_event = self
|
||||
@@ -1220,7 +1253,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
// The profile for the invited user has been updated.
|
||||
let invited_member_event = self.get_profile(room_id, invited_user_id).await?.unwrap();
|
||||
assert_eq!(
|
||||
invited_member_event.as_original().unwrap().content.displayname.as_deref(),
|
||||
invited_member_event.content.displayname.as_deref(),
|
||||
Some("example after update")
|
||||
);
|
||||
assert!(self.get_member_event(room_id, invited_user_id).await?.is_some());
|
||||
@@ -1842,24 +1875,24 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
assert!(maybe_sub.is_none());
|
||||
|
||||
// Setting the thread subscription works.
|
||||
self.upsert_thread_subscription(
|
||||
self.upsert_thread_subscriptions(vec![(
|
||||
room_id(),
|
||||
first_thread,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: None,
|
||||
},
|
||||
)
|
||||
)])
|
||||
.await?;
|
||||
|
||||
self.upsert_thread_subscription(
|
||||
self.upsert_thread_subscriptions(vec![(
|
||||
room_id(),
|
||||
second_thread,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: false },
|
||||
bump_stamp: None,
|
||||
},
|
||||
)
|
||||
)])
|
||||
.await?;
|
||||
|
||||
// Now, reading the thread subscription returns the expected status.
|
||||
@@ -1882,14 +1915,14 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
);
|
||||
|
||||
// We can override the thread subscription status.
|
||||
self.upsert_thread_subscription(
|
||||
self.upsert_thread_subscriptions(vec![(
|
||||
room_id(),
|
||||
first_thread,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: None,
|
||||
},
|
||||
)
|
||||
)])
|
||||
.await?;
|
||||
|
||||
// And it's correctly reflected.
|
||||
@@ -1935,72 +1968,179 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_thread_subscriptions_bumpstamps(&self) -> TestResult {
|
||||
let thread = event_id!("$fred");
|
||||
async fn test_thread_subscriptions_bulk_upsert(&self) -> TestResult {
|
||||
let threads = [
|
||||
event_id!("$t1"),
|
||||
event_id!("$t2"),
|
||||
event_id!("$t3"),
|
||||
event_id!("$t4"),
|
||||
event_id!("$t5"),
|
||||
event_id!("$t6"),
|
||||
];
|
||||
// Helper for building the input for `upsert_thread_subscriptions()`,
|
||||
// which is of the type: Vec<(&RoomId, &EventId, StoredThreadSubscription)>
|
||||
let build_subscription_updates = |subs: &[StoredThreadSubscription]| {
|
||||
threads
|
||||
.iter()
|
||||
.zip(subs)
|
||||
.map(|(&event_id, &sub)| (room_id(), event_id, sub))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
// At first, there is no thread subscription.
|
||||
let sub = self.load_thread_subscription(room_id(), thread).await?;
|
||||
assert!(sub.is_none());
|
||||
|
||||
// Setting the thread subscription with some bumpstamp works.
|
||||
self.upsert_thread_subscription(
|
||||
room_id(),
|
||||
thread,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(42),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sub = self.load_thread_subscription(room_id(), thread).await?.unwrap();
|
||||
assert_eq!(
|
||||
sub,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(42),
|
||||
}
|
||||
);
|
||||
|
||||
// Storing a subscription with an older bumpstamp has no effect.
|
||||
self.upsert_thread_subscription(
|
||||
room_id(),
|
||||
thread,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: false },
|
||||
bump_stamp: Some(41),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sub = self.load_thread_subscription(room_id(), thread).await?.unwrap();
|
||||
assert_eq!(
|
||||
sub,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(42),
|
||||
}
|
||||
);
|
||||
|
||||
// Storing with no bumpstamps keeps the previous one.
|
||||
self.upsert_thread_subscription(
|
||||
room_id(),
|
||||
thread,
|
||||
// Test bump_stamp logic
|
||||
let initial_subscriptions = build_subscription_updates(&[
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sub = self.load_thread_subscription(room_id(), thread).await?.unwrap();
|
||||
assert_eq!(
|
||||
sub,
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(42),
|
||||
}
|
||||
);
|
||||
bump_stamp: Some(14),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: None,
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(210),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(5),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(100),
|
||||
},
|
||||
]);
|
||||
|
||||
let update_subscriptions = build_subscription_updates(&[
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: None,
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: None,
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(1101),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(222),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(1),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(100),
|
||||
},
|
||||
]);
|
||||
|
||||
let expected_subscriptions = build_subscription_updates(&[
|
||||
// Status should be updated, because prev and new bump_stamp are both None
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: None,
|
||||
},
|
||||
// Status should be updated, but keep initial bump_stamp (new is None)
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(14),
|
||||
},
|
||||
// Status should be updated and also bump_stamp should be updated (initial was None)
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(1101),
|
||||
},
|
||||
// Status should be updated and also bump_stamp should be updated (initial was lower)
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(222),
|
||||
},
|
||||
// Status shouldn't change, as new bump_stamp is lower
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(5),
|
||||
},
|
||||
// Status shouldn't change, as bump_stamp is equal to the previous one
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(100),
|
||||
},
|
||||
]);
|
||||
|
||||
// Set the initial subscriptions
|
||||
self.upsert_thread_subscriptions(initial_subscriptions.clone()).await?;
|
||||
|
||||
// Assert the subscriptions have been added
|
||||
for (room_id, thread_id, expected_sub) in &initial_subscriptions {
|
||||
let stored_subscription = self.load_thread_subscription(room_id, thread_id).await?;
|
||||
assert_eq!(stored_subscription, Some(*expected_sub));
|
||||
}
|
||||
|
||||
// Update subscriptions
|
||||
self.upsert_thread_subscriptions(update_subscriptions).await?;
|
||||
|
||||
// Assert the expected subscriptions and bump_stamps
|
||||
for (room_id, thread_id, expected_sub) in &expected_subscriptions {
|
||||
let stored_subscription = self.load_thread_subscription(room_id, thread_id).await?;
|
||||
assert_eq!(stored_subscription, Some(*expected_sub));
|
||||
}
|
||||
|
||||
// Test just state changes, but first remove previous subscriptions
|
||||
for (room_id, thread_id, _) in &expected_subscriptions {
|
||||
self.remove_thread_subscription(room_id, thread_id).await?;
|
||||
}
|
||||
|
||||
let initial_subscriptions = build_subscription_updates(&[
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(1),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: false },
|
||||
bump_stamp: Some(1),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(1),
|
||||
},
|
||||
]);
|
||||
|
||||
self.upsert_thread_subscriptions(initial_subscriptions.clone()).await?;
|
||||
|
||||
for (room_id, thread_id, expected_sub) in &initial_subscriptions {
|
||||
let stored_subscription = self.load_thread_subscription(room_id, thread_id).await?;
|
||||
assert_eq!(stored_subscription, Some(*expected_sub));
|
||||
}
|
||||
|
||||
let update_subscriptions = build_subscription_updates(&[
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: true },
|
||||
bump_stamp: Some(2),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Unsubscribed,
|
||||
bump_stamp: Some(2),
|
||||
},
|
||||
StoredThreadSubscription {
|
||||
status: ThreadSubscriptionStatus::Subscribed { automatic: false },
|
||||
bump_stamp: Some(2),
|
||||
},
|
||||
]);
|
||||
|
||||
self.upsert_thread_subscriptions(update_subscriptions.clone()).await?;
|
||||
|
||||
for (room_id, thread_id, expected_sub) in &update_subscriptions {
|
||||
let stored_subscription = self.load_thread_subscription(room_id, thread_id).await?;
|
||||
assert_eq!(stored_subscription, Some(*expected_sub));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2192,9 +2332,9 @@ macro_rules! statestore_integration_tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_thread_subscriptions_bumpstamps() -> TestResult {
|
||||
async fn test_thread_subscriptions_bulk_upsert() -> TestResult {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_thread_subscriptions_bumpstamps().await
|
||||
store.test_thread_subscriptions_bulk_upsert().await
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2220,6 +2360,10 @@ fn first_receipt_event_id() -> &'static EventId {
|
||||
event_id!("$example")
|
||||
}
|
||||
|
||||
fn topic_event_id() -> &'static EventId {
|
||||
event_id!("$topic_event")
|
||||
}
|
||||
|
||||
fn power_level_event() -> Raw<AnySyncStateEvent> {
|
||||
let content = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user