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

|
||||
|
||||
## AI policy
|
||||
|
||||
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
|
||||
|
||||
### Terminology
|
||||
|
||||
This does not necessarily reflect the official or commonly used terminology.
|
||||
|
||||
Software and services that heavily rely on large language model technology to
|
||||
generate their outcomes are referred to as _Artificial Intelligence_ (AI).
|
||||
Examples of products that fit this definition: GitHub Copilot, ChatGPT, Claude
|
||||
Sonnet, DeepSeek, Llama and Gemini.
|
||||
|
||||
There is a distinction between _general_ and _narrow_ AI, all the aforementioned
|
||||
examples fall under general AI as they were not trained to execute a specific
|
||||
well-defined task. Narrow AI is trained to be used for specific well-defined
|
||||
tasks where the problem space is known in advance.
|
||||
|
||||
_Vibe coding_ is the practice where AI creates a code change (feature, bugfix,
|
||||
tests, refactor) with a human that describes what needs to be implemented.
|
||||
|
||||
_AI agents_ are AIs that are configured to perform interactions or make changes
|
||||
with little to no human supervision.
|
||||
|
||||
### Agreement
|
||||
|
||||
1. If content was made with the help of AI, you **must** convey that this is
|
||||
the case. This includes content that you authored but was motivated by a
|
||||
suggestion of AI.
|
||||
2. If at any point you used AI's work in your contribution you should make
|
||||
an effort to **verify** that you can submit this under the license of the
|
||||
repository.
|
||||
3. The **accountability** of using AI in a contribution lies with the person
|
||||
that makes that contribution.
|
||||
4. All communication, that includes: commit messages, pull request messages,
|
||||
documentation, code comments and issues (and comments on issues/pull
|
||||
requests), that is intended to be read by people to understand your thoughts
|
||||
and work **must not** have been generated with AI. We exclude machine
|
||||
translation and tooling that helps with grammar and spelling check.
|
||||
5. Using general AI for review is **forbidden**. If the change contains changes
|
||||
to the user experience it has to be approved by a human reviewer.
|
||||
6. It is **not allowed** to use AI in an autonomous-looking way to contribute to
|
||||
the Matrix Rust SDK. This also applies when someone engages in _vibe coding_
|
||||
or uses so-called _agent mode_.
|
||||
|
||||
[Forgejo]: https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md
|
||||
|
||||
Generated
+727
-619
File diff suppressed because it is too large
Load Diff
+117
-84
@@ -13,58 +13,65 @@ members = [
|
||||
exclude = ["testing/data"]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.88"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.99"
|
||||
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.
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.3"
|
||||
byteorder = "1.5.0"
|
||||
cfg-if = "1.0.3"
|
||||
clap = "4.5.46"
|
||||
chrono = "0.4.41"
|
||||
dirs = "6.0.0"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.7.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.9.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
|
||||
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 = "5.0.0"
|
||||
indexmap = "2.11.0"
|
||||
insta = { version = "1.43.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.77"
|
||||
mime = "0.3.17"
|
||||
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 = { 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"
|
||||
once_cell = { version = "1.21.3", default-features = false }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
pin-project-lite = { version = "0.2.16", default-features = false }
|
||||
proc-macro2 = { version = "1.0.106", default-features = false }
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.23", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
ruma = { version = "0.13.0", features = [
|
||||
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 = { version = "1.3.0", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c15801c80b683b67dc", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-arbitrary-length-ids",
|
||||
@@ -72,6 +79,7 @@ ruma = { version = "0.13.0", features = [
|
||||
"compat-encrypted-stickers",
|
||||
"compat-lax-room-create-deser",
|
||||
"compat-lax-room-topic-deser",
|
||||
"unstable-msc3230",
|
||||
"unstable-msc3401",
|
||||
"unstable-msc3488",
|
||||
"unstable-msc3489",
|
||||
@@ -79,54 +87,56 @@ ruma = { version = "0.13.0", features = [
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4143",
|
||||
"unstable-msc4171",
|
||||
"unstable-msc4222",
|
||||
"unstable-msc4278",
|
||||
"unstable-msc4286",
|
||||
"unstable-msc4306",
|
||||
"unstable-msc4308"
|
||||
"unstable-msc4308",
|
||||
"unstable-msc4310",
|
||||
] }
|
||||
sentry = { version = "0.42.0", default-features = false }
|
||||
sentry-tracing = "0.42.0"
|
||||
serde = { version = "1.0.219", features = ["rc"] }
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.143"
|
||||
sha2 = "0.10.9"
|
||||
similar-asserts = "1.7.0"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.21.0"
|
||||
thiserror = "2.0.16"
|
||||
tokio = { version = "1.47.1", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
sentry = { version = "0.46.0", default-features = false }
|
||||
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 = { 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.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.7"
|
||||
uuid = "1.18.0"
|
||||
vergen-gitcl = "1.0.8"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.50"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.5"
|
||||
zeroize = "1.8.1"
|
||||
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.14.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.14.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.14.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.14.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.16.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.16.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.16.0" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.14.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.14.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.14.0" }
|
||||
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.14.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.14.0", default-features = false }
|
||||
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.14.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.16.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.16.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.16.0" }
|
||||
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.16.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.16.0", default-features = false }
|
||||
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.16.0" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
@@ -164,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.
|
||||
@@ -184,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.
|
||||
|
||||
@@ -14,18 +14,21 @@ release = false
|
||||
codspeed = []
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
|
||||
assert_matches.workspace = true
|
||||
criterion = { version = "4.2.1", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
|
||||
futures-util.workspace = true
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-ui.workspace = true
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
wiremock.workspace = true
|
||||
|
||||
[[bench]]
|
||||
@@ -51,3 +54,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "event_cache"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_list"
|
||||
harness = false
|
||||
|
||||
@@ -317,6 +317,7 @@ fn find_event_relations(c: &mut Criterion) {
|
||||
let (target, relations) = room_event_cache
|
||||
.find_event_with_relations(target_event_id, filter)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
|
||||
assert_eq!(relations.len(), num_related_events as usize);
|
||||
|
||||
@@ -181,15 +181,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.as_clean()
|
||||
.unwrap()
|
||||
.clear_all_linked_chunks()
|
||||
.await
|
||||
.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");
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
use assert_matches::assert_matches;
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{stream::StreamExt, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::{
|
||||
RoomListService, eyeball_im::VectorDiff, room_list_service::filters::new_filter_non_left,
|
||||
};
|
||||
use rand::{distributions::Uniform, prelude::Distribution};
|
||||
use ruma::{EventId, RoomId, owned_user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a room list.
|
||||
pub fn create(c: &mut Criterion) {
|
||||
const NUMBER_OF_ROOMS: usize = 1000;
|
||||
const NUMBER_OF_EVENTS_PER_ROOM: usize = 1000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
|
||||
let (server, client) = runtime.block_on(async {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
(server, client)
|
||||
});
|
||||
|
||||
let sender_id = owned_user_id!("@mnt_io:matrix.org");
|
||||
let mut rand = rand::thread_rng();
|
||||
let server_ts_range = Uniform::from(100..1000);
|
||||
|
||||
for room_nth in 0..NUMBER_OF_ROOMS {
|
||||
let room_id = RoomId::parse(format!("!r{room_nth}")).unwrap();
|
||||
let first_server_ts = server_ts_range.sample(&mut rand);
|
||||
let event_factory = EventFactory::new().room(&room_id).server_ts(first_server_ts);
|
||||
|
||||
let events = (0..NUMBER_OF_EVENTS_PER_ROOM)
|
||||
.map(|event_nth| {
|
||||
let event_id = EventId::parse(format!("$ev{room_nth}_{event_nth}")).unwrap();
|
||||
|
||||
event_factory.text_msg("a").sender(&sender_id).event_id(&event_id).into_raw_sync()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let _room = runtime.block_on(async {
|
||||
server
|
||||
.sync_room(&client, JoinedRoomBuilder::new(&room_id).add_timeline_bulk(events))
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
let mut group = c.benchmark_group("RoomList");
|
||||
group.throughput(Throughput::Elements(NUMBER_OF_ROOMS.try_into().unwrap()));
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new(
|
||||
"Create",
|
||||
format!("{NUMBER_OF_ROOMS} rooms × {NUMBER_OF_EVENTS_PER_ROOM} events"),
|
||||
),
|
||||
|bencher| {
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
let room_list_service = RoomListService::new(client.clone())
|
||||
.await
|
||||
.expect("build the room list service");
|
||||
let room_list = room_list_service.all_rooms().await.expect("fetch `all_rooms`");
|
||||
let (entries_stream, entries_controller) =
|
||||
room_list.entries_with_dynamic_adapters(20);
|
||||
|
||||
// Setting the filter will trigger the entries stream computation.
|
||||
entries_controller.set_filter(Box::new(new_filter_non_left()));
|
||||
|
||||
pin_mut!(entries_stream);
|
||||
let update = entries_stream.next().await.expect("receiving the reset update");
|
||||
assert_eq!(update.len(), 1);
|
||||
assert_matches!(&update[0], VectorDiff::Reset { values } => {
|
||||
assert_eq!(values.len(), 20);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room_list;
|
||||
config = Criterion::default();
|
||||
targets = create
|
||||
}
|
||||
criterion_main!(room_list);
|
||||
@@ -1,7 +1,7 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
|
||||
use matrix_sdk_ui::timeline::TimelineBuilder;
|
||||
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineReadReceiptTracking};
|
||||
use ruma::{
|
||||
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
|
||||
owned_user_id,
|
||||
@@ -103,7 +103,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = TimelineBuilder::new(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
@@ -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
|
||||
@@ -869,9 +872,18 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `room_id` - The unique id of the room where the event was sent to.
|
||||
///
|
||||
/// * `handle_verification_events` - if the supplied event is a verification
|
||||
/// event, use it to update the verification state. **Note**: it is
|
||||
/// recommended to avoid setting this flag to true and use the explicit
|
||||
/// [`OlmMachine::receive_verification_event`] method instead:
|
||||
/// verification events sometimes need preparation before we can handle
|
||||
/// them: see the documentation for
|
||||
/// [`OlmMachine::receive_verification_event`].
|
||||
///
|
||||
/// * `strict_shields` - If `true`, messages will be decorated with strict
|
||||
/// warnings (use `false` to match legacy behaviour where unsafe keys have
|
||||
/// lower severity warnings and unverified identities are not decorated).
|
||||
///
|
||||
/// * `decryption_settings` - The setting for decrypting messages.
|
||||
pub fn decrypt_room_event(
|
||||
&self,
|
||||
@@ -1100,6 +1112,14 @@ impl OlmMachine {
|
||||
///
|
||||
/// This method can be used to pass verification events that are happening
|
||||
/// in rooms to the `OlmMachine`. The event should be in the decrypted form.
|
||||
///
|
||||
/// **Note**: If the supplied event is an `m.room.message` event with
|
||||
/// `msgtype: m.key.verification.request`, then the device information for
|
||||
/// the sending user must be up-to-date before calling this method
|
||||
/// (otherwise, the request will be ignored). It is hard to guarantee this
|
||||
/// is the case, but you can maximize your chances by explicitly making a
|
||||
/// request to /keys/query for the user's device info, and processing the
|
||||
/// response with [`OlmMachine::mark_request_as_sent`].
|
||||
pub fn receive_verification_event(
|
||||
&self,
|
||||
event: String,
|
||||
@@ -1392,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
|
||||
quote.workspace = true
|
||||
syn = { workspace = true, features = ["full", "extra-traits"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -6,12 +6,199 @@ 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 `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))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**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
|
||||
|
||||
- `TimelineConfiguration::track_read_receipts`'s type is now an enum to allow tracking to be enabled for all events
|
||||
(like before) or only for message-like events (which prevents read receipts from being placed on state events).
|
||||
([#5900](https://github.com/matrix-org/matrix-rust-sdk/pull/5900))
|
||||
- `Client::reset_server_info()` has been split into `reset_supported_versions()`
|
||||
and `reset_well_known()`.
|
||||
([#5910](https://github.com/matrix-org/matrix-rust-sdk/pull/5910))
|
||||
- Add `HumanQrLoginError::NotFound` for non-existing / expired rendezvous sessions
|
||||
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
|
||||
- Add `HumanQrGrantLoginError::NotFound` for non-existing / expired rendezvous sessions
|
||||
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
|
||||
- The `LatestEventValue::Local` type gains 2 new fields: `sender` and `profile`.
|
||||
([#5885](https://github.com/matrix-org/matrix-rust-sdk/pull/5885))
|
||||
- The `Encryption::user_identity()` method has received a new argument. The
|
||||
`fallback_to_server` argument controls if we should attempt to fetch the user
|
||||
identity from the homeserver if it wasn't found in the local storage.
|
||||
([#5870](https://github.com/matrix-org/matrix-rust-sdk/pull/5870))
|
||||
- Expose the power level required to modify `m.space.child` on
|
||||
`room::power_levels::RoomPowerLevelsValues`.
|
||||
- Rename `Client::login_with_qr_code` to `Client::new_login_with_qr_code_handler`.
|
||||
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
|
||||
- Add the `sqlite` feature, along with the `indexeddb` feature, to enable either
|
||||
the SQLite or IndexedDB store. The `session_paths`, `session_passphrase`,
|
||||
`session_pool_max_size`, `session_cache_size` and `session_journal_size_limit`
|
||||
methods on `ClientBuilder` have been removed. New methods are added:
|
||||
`ClientBuilder::in_memory_store` if one wants non-persistent stores,
|
||||
`ClientBuilder::sqlite_store` to configure and to use SQLite stores (if
|
||||
the `sqlite` feature is enabled), and `ClientBuilder::indexeddb_store` to
|
||||
configure and to use IndexedDB stores (if the `indexeddb` feature is enabled).
|
||||
([#5811](https://github.com/matrix-org/matrix-rust-sdk/pull/5811))
|
||||
|
||||
The code:
|
||||
|
||||
```rust
|
||||
client_builder
|
||||
.session_paths("data_path", "cache_path")
|
||||
.passphrase("foobar")
|
||||
```
|
||||
|
||||
now becomes:
|
||||
|
||||
```rust
|
||||
client_builder
|
||||
.sqlite_store(
|
||||
SqliteSessionStoreBuilder::new("data_path", "cache_path")
|
||||
.passphrase("foobar")
|
||||
)
|
||||
```
|
||||
|
||||
- UniFFI was upgraded to `v0.30.0` ([#5808](https://github.com/matrix-org/matrix-rust-sdk/pull/5808)).
|
||||
- The `waveform` parameter in `Timeline::send_voice_message` format changed to a list of `f32`
|
||||
between 0 and 1.
|
||||
([#5732](https://github.com/matrix-org/matrix-rust-sdk/pull/5732))
|
||||
- The `normalized_power_level` field has been removed from the `RoomMember`
|
||||
struct.
|
||||
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635))
|
||||
- Remove the deprecated `CallNotify` event (`org.matrix.msc4075.call.notify`) in favor of the new
|
||||
`RtcNotification` event (`org.matrix.msc4075.rtc.notification`).
|
||||
([#5668](https://github.com/matrix-org/matrix-rust-sdk/pull/5668))
|
||||
- Add `QrLoginProgress::SyncingSecrets` to indicate that secrets are being synced between the two
|
||||
devices.
|
||||
([#5760](https://github.com/matrix-org/matrix-rust-sdk/pull/5760))
|
||||
- Add `Room::subscribe_to_send_queue_updates` to observe room send queue updates.
|
||||
([#5761](https://github.com/matrix-org/matrix-rust-sdk/pull/5761))
|
||||
- `Client::login_with_qr_code` now returns a handler that allows performing the flow with either the
|
||||
current device scanning or generating the QR code. Additionally, new errors `HumanQrLoginError::CheckCodeAlreadySent`
|
||||
and `HumanQrLoginError::CheckCodeCannotBeSent` were added.
|
||||
([#5786](https://github.com/matrix-org/matrix-rust-sdk/pull/5786))
|
||||
- `ComposerDraft` now includes attachments alongside the text message.
|
||||
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
|
||||
- Add `Client::subscribe_to_send_queue_updates` to observe global send queue updates.
|
||||
([#5784](https://github.com/matrix-org/matrix-rust-sdk/pull/5784))
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Client::get_store_sizes()` so to query the size of the existing stores, if available. ([#5911](https://github.com/matrix-org/matrix-rust-sdk/pull/5911))
|
||||
- Expose `is_space` in `NotificationRoomInfo`, allowing clients to determine if the room that triggered the notification is a space.
|
||||
- Add push actions to `NotificationItem` and replace `SyncNotification` with `NotificationItem`.
|
||||
([#5835](https://github.com/matrix-org/matrix-rust-sdk/pull/5835))
|
||||
- Add `Client::new_grant_login_with_qr_code_handler` for granting login to a new device by way of
|
||||
a QR code.
|
||||
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
|
||||
- Add `Client::register_notification_handler` for observing notifications generated from sync responses.
|
||||
([#5831](https://github.com/matrix-org/matrix-rust-sdk/pull/5831))
|
||||
- Add `Room::mark_as_fully_read_unchecked` so clients can mark a room as read without needing a `Timeline` instance. Note this method is not recommended as it can potentially cause incorrect read receipts, but it can needed in certain cases.
|
||||
- Add `Timeline::latest_event_id` to be able to fetch the event id of the latest event of the timeline.
|
||||
- Add `Room::load_or_fetch_event` so we can get a `TimelineEvent` given its event id ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
|
||||
- Add `TimelineEvent::thread_root_event_id` to expose the thread root event id for this type too ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
|
||||
- Add `NotificationSettings::get_raw_push_rules` so clients can fetch the raw JSON content of the push rules of the current user and include it in bug reports ([#5706](https://github.com/matrix-org/matrix-rust-sdk/pull/5706)).
|
||||
- Add new API to decline calls ([MSC4310](https://github.com/matrix-org/matrix-spec-proposals/pull/4310)): `Room::decline_call` and `Room::subscribe_to_call_decline_events`
|
||||
([#5614](https://github.com/matrix-org/matrix-rust-sdk/pull/5614))
|
||||
- Expose `m.federate` in `OtherState::RoomCreate` and `history_visibility` in `OtherState::RoomHistoryVisibility`, allowing clients to know whether a room federates and how its history is shared in the appropriate timeline events.
|
||||
- Expose `join_rule` in `OtherState::RoomJoinRules`, allowing clients to know the join rules of a room from the appropriate timeline events.
|
||||
|
||||
### Changes
|
||||
|
||||
- `Timeline::latest_event_id` now uses its `ui::Timeline::latest_event_id` counterpart, instead of getting the latest event from the timeline and then its id.([#5864](https://github.com/matrix-org/matrix-rust-sdk/pull/5864))
|
||||
- Build Android ARM64 bindings using better default RUSTFLAGS (the same used for iOS ARM64). This should improve performance. [(#5854)](https://github.com/matrix-org/matrix-rust-sdk/pull/5854)
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features:
|
||||
|
||||
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
|
||||
rooms based on their low priority status. These filters allow clients to show only low priority rooms
|
||||
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
|
||||
rooms based on their low priority status. These filters allow clients to show only low priority rooms
|
||||
or exclude low priority rooms from the room list.
|
||||
([#5508](https://github.com/matrix-org/matrix-rust-sdk/pull/5508))
|
||||
- Add `room_version` and `privileged_creators_role` to `RoomInfo` ([#5449](https://github.com/matrix-org/matrix-rust-sdk/pull/5449)).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
@@ -24,8 +24,13 @@ crate-type = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite", "unstable-msc4274"]
|
||||
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
|
||||
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"]
|
||||
# Use SQLite for the session storage.
|
||||
sqlite = ["matrix-sdk/sqlite"]
|
||||
# Use an embedded version of SQLite.
|
||||
bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
|
||||
# Use IndexedDB for the session storage.
|
||||
indexeddb = ["matrix-sdk/indexeddb"]
|
||||
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
|
||||
# Required when targeting a Javascript environment, like Wasm in a browser.
|
||||
js = ["matrix-sdk-ui/js"]
|
||||
@@ -36,34 +41,61 @@ rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
|
||||
# Enable sentry error monitoring, not compatible with Wasm platforms.
|
||||
sentry = ["dep:sentry", "dep:sentry-tracing"]
|
||||
|
||||
experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-emojis"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
extension-trait = "1.0.1"
|
||||
extension-trait = "1.0.2"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
language-tags = "0.3.2"
|
||||
log-panics = { version = "2", features = ["with-backtrace"] }
|
||||
log-panics = { version = "2.1.0", default-features = false, features = ["with-backtrace"] }
|
||||
matrix-sdk = { workspace = true, features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
"federation-api",
|
||||
] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-common.workspace = true
|
||||
matrix-sdk-ffi-macros.workspace = true
|
||||
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
|
||||
mime = "0.3.16"
|
||||
mime = { version = "0.3.17", default-features = false }
|
||||
once_cell.workspace = true
|
||||
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278", "unstable-hydra"] }
|
||||
ruma = { workspace = true, features = [
|
||||
"html",
|
||||
"unstable-msc3488",
|
||||
"compat-unset-avatar",
|
||||
"unstable-msc3245-v1-compat",
|
||||
"unstable-msc4278",
|
||||
"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",
|
||||
@@ -75,14 +107,15 @@ 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
|
||||
uniffi = { workspace = true, features = ["wasm-unstable-single-threaded"] }
|
||||
futures-executor.workspace = true
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
async-compat.workspace = true
|
||||
@@ -90,7 +123,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
uniffi = { workspace = true, features = ["tokio"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
paranoid-android = "0.2.1"
|
||||
paranoid-android = { version = "0.2.2", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
similar-asserts.workspace = true
|
||||
|
||||
@@ -3,31 +3,33 @@
|
||||
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
|
||||
|
||||
## Features
|
||||
|
||||
Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
### Platform specific
|
||||
### Platform specific
|
||||
|
||||
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
|
||||
### Functionality
|
||||
|
||||
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
|
||||
- `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one.
|
||||
- `sqlite`: Use SQLite for the session storage.
|
||||
- `bundled-sqlite`: Use an embedded version of SQLite instead of the system provided one.
|
||||
- `indexeddb`: Use IndexedDB for the session storage.
|
||||
|
||||
### Unstable specs
|
||||
|
||||
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
|
||||
|
||||
## Platforms
|
||||
|
||||
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
|
||||
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
|
||||
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
- Javascript/Wasm: `"unstable-msc4274,native-tls"`
|
||||
- JavaScript/Wasm: `"indexeddb,unstable-msc4274,native-tls"`
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
|
||||
|
||||
### Swift/iOS async
|
||||
|
||||
TBD
|
||||
|
||||
@@ -43,6 +43,23 @@ fn setup_x86_64_android_workaround() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a workaround for watchOS simulator builds to manually link against the
|
||||
/// CoreFoundation framework in order to avoid linker errors. Otherwise, errors
|
||||
/// like the following may occur:
|
||||
///
|
||||
/// = note: Undefined symbols for architecture arm64:
|
||||
/// "_CFArrayCreate", referenced from:
|
||||
/// "_CFDataCreate", referenced from:
|
||||
/// "_CFRelease", referenced from:
|
||||
/// etc.
|
||||
fn setup_watchos_simulator_workaround() {
|
||||
let target = env::var("TARGET").expect("TARGET not set");
|
||||
if target.ends_with("watchos-sim") {
|
||||
println!("cargo:rustc-link-arg=-framework");
|
||||
println!("cargo:rustc-link-arg=CoreFoundation");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
@@ -58,6 +75,7 @@ fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
setup_watchos_simulator_workaround();
|
||||
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
|
||||
|
||||
let git_config = GitclBuilder::default().sha(true).build()?;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
namespace matrix_sdk_ffi {};
|
||||
|
||||
[Remote]
|
||||
dictionary Mentions {
|
||||
sequence<string> user_ids;
|
||||
boolean room;
|
||||
};
|
||||
|
||||
[Remote]
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
@@ -10,11 +10,14 @@ use anyhow::{anyhow, Context as _};
|
||||
use futures_util::pin_mut;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use matrix_sdk::STATE_STORE_DATABASE_NAME;
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
event_cache::EventCacheError,
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
executor::AbortOnDrop,
|
||||
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
|
||||
ruma::{
|
||||
api::client::{
|
||||
@@ -39,8 +42,8 @@ use matrix_sdk::{
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
STATE_STORE_DATABASE_NAME,
|
||||
task_monitor::BackgroundTaskFailureReason,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
@@ -58,6 +61,7 @@ use ruma::{
|
||||
alias::get_alias,
|
||||
error::ErrorKind,
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
events::{
|
||||
@@ -73,18 +77,18 @@ use ruma::{
|
||||
join_rules::{
|
||||
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
|
||||
},
|
||||
message::OriginalSyncRoomMessageEvent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
message::{OriginalSyncRoomMessageEvent, Relation},
|
||||
},
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
},
|
||||
tag::TagEventContent,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
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};
|
||||
@@ -101,10 +105,13 @@ use crate::{
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClient,
|
||||
notification::{
|
||||
NotificationClient, NotificationEvent, NotificationItem, NotificationRoomInfo,
|
||||
NotificationSenderInfo,
|
||||
},
|
||||
notification_settings::NotificationSettings,
|
||||
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
|
||||
room::{RoomHistoryVisibility, RoomInfoListener},
|
||||
qr_code::{GrantLoginWithQrCodeHandler, LoginWithQrCodeHandler},
|
||||
room::{RoomHistoryVisibility, RoomInfoListener, RoomSendQueueUpdate},
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{
|
||||
@@ -182,7 +189,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)]
|
||||
@@ -196,6 +215,13 @@ pub trait ProgressWatcher: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn transmission_progress(&self, progress: TransmissionProgress);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) update reporter of the send queue.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueRoomUpdateListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called every time the send queue emits an update for a given room.
|
||||
fn on_update(&self, room_id: String, update: RoomSendQueueUpdate);
|
||||
}
|
||||
|
||||
/// A listener to the global (client-wide) error reporter of the send queue.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueRoomErrorListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
@@ -218,6 +244,16 @@ pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_change(&self, event: RoomAccountDataEvent, room_id: String);
|
||||
}
|
||||
|
||||
/// A listener for notifications generated from sync responses.
|
||||
///
|
||||
/// This is called during sync for each event that triggers a notification
|
||||
/// based on the user's push rules.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SyncNotificationListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called when a notifying event is received during sync.
|
||||
fn on_notification(&self, notification: NotificationItem, room_id: String);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, uniffi::Record)]
|
||||
pub struct TransmissionProgress {
|
||||
pub current: u64,
|
||||
@@ -233,16 +269,30 @@ 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>>,
|
||||
|
||||
session_verification_controller:
|
||||
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
|
||||
|
||||
/// The path to the directory where the state store and the crypto store are
|
||||
/// located, if the `Client` instance has been built with a SQLite store
|
||||
/// backend.
|
||||
/// located, if the `Client` instance has been built with a store (either
|
||||
/// SQLite or IndexedDB).
|
||||
#[cfg_attr(not(feature = "sqlite"), allow(unused))]
|
||||
store_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -286,7 +336,7 @@ impl Client {
|
||||
|
||||
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,
|
||||
@@ -332,6 +382,17 @@ impl Client {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Perform database optimizations if any are available, i.e. vacuuming in
|
||||
/// SQLite.
|
||||
pub async fn optimize_stores(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.optimize_stores().await?)
|
||||
}
|
||||
|
||||
/// Returns the sizes of the existing stores, if known.
|
||||
pub async fn get_store_sizes(&self) -> Result<StoreSizes, ClientError> {
|
||||
Ok(self.inner.get_store_sizes().await?.into())
|
||||
}
|
||||
|
||||
/// Information about login options for the client's homeserver.
|
||||
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
|
||||
let oauth = self.inner.oauth();
|
||||
@@ -544,43 +605,24 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Log in using the provided [`QrCodeData`]. The `Client` must be built
|
||||
/// by providing [`QrCodeData::server_name`] as the server name for this
|
||||
/// login to succeed.
|
||||
/// Create a handler for requesting an existing device to grant login to
|
||||
/// this device by way of a QR code.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such
|
||||
/// this method requires OAuth 2.0 support as well as sliding sync support.
|
||||
/// # Arguments
|
||||
///
|
||||
/// The usage of the progress_listener is required to transfer the
|
||||
/// [`CheckCode`] to the existing client.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn login_with_qr_code(
|
||||
/// * `oidc_configuration` - The data to restore or register the client with
|
||||
/// the server.
|
||||
pub fn new_login_with_qr_code_handler(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
oidc_configuration: OidcConfiguration,
|
||||
) -> LoginWithQrCodeHandler {
|
||||
LoginWithQrCodeHandler::new(self.inner.oauth(), oidc_configuration)
|
||||
}
|
||||
|
||||
let oauth = self.inner.oauth();
|
||||
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data));
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
/// Create a handler for granting login from this device to a new device by
|
||||
/// way of a QR code.
|
||||
pub fn new_grant_login_with_qr_code_handler(self: Arc<Self>) -> GrantLoginWithQrCodeHandler {
|
||||
GrantLoginWithQrCodeHandler::new(self.inner.oauth())
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
@@ -634,6 +676,49 @@ impl Client {
|
||||
self.inner.send_queue().enable_upload_progress(enable);
|
||||
}
|
||||
|
||||
/// Subscribe to the global send queue update reporter, at the
|
||||
/// client-wide level.
|
||||
///
|
||||
/// The given listener will be immediately called with
|
||||
/// `RoomSendQueueUpdate::NewLocalEvent` for each local echo existing in
|
||||
/// the queue.
|
||||
pub async fn subscribe_to_send_queue_updates(
|
||||
&self,
|
||||
listener: Box<dyn SendQueueRoomUpdateListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let q = self.inner.send_queue();
|
||||
let local_echoes = q.local_echoes().await?;
|
||||
let mut subscriber = q.subscribe();
|
||||
|
||||
for (room_id, local_echoes) in local_echoes {
|
||||
for local_echo in local_echoes {
|
||||
listener.on_update(
|
||||
room_id.clone().into(),
|
||||
RoomSendQueueUpdate::NewLocalEvent {
|
||||
transaction_id: local_echo.transaction_id.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(update) => {
|
||||
let room_id = update.room_id.to_string();
|
||||
match update.update.try_into() {
|
||||
Ok(update) => listener.on_update(room_id, update),
|
||||
Err(err) => error!("error when converting send queue update: {err}"),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("error when listening to the send queue update reporter: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Subscribe to the global enablement status of the send queue, at the
|
||||
/// client-wide level.
|
||||
///
|
||||
@@ -789,6 +874,158 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a handler for notifications generated from sync responses.
|
||||
///
|
||||
/// The handler will be called during sync for each event that triggers
|
||||
/// a notification based on the user's push rules.
|
||||
///
|
||||
/// The handler receives:
|
||||
/// - The notification with push actions and event data
|
||||
/// - The room ID where the notification occurred
|
||||
///
|
||||
/// This is useful for implementing custom notification logic, such as
|
||||
/// displaying local notifications or updating notification badges.
|
||||
pub async fn register_notification_handler(&self, listener: Box<dyn SyncNotificationListener>) {
|
||||
let listener = Arc::new(listener);
|
||||
self.inner
|
||||
.register_notification_handler(move |notification, room, _client| {
|
||||
let listener = listener.clone();
|
||||
let room_id = room.room_id().to_string();
|
||||
|
||||
async move {
|
||||
// Extract information about the actions
|
||||
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
|
||||
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
|
||||
|
||||
// Convert SDK actions to FFI type
|
||||
let actions: Vec<crate::notification_settings::Action> = notification
|
||||
.actions
|
||||
.into_iter()
|
||||
.filter_map(|action| action.try_into().ok())
|
||||
.collect();
|
||||
|
||||
// Convert SDK event to FFI type
|
||||
let (sender, event, thread_id, raw_event) = match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let thread_id = match &deserialized {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
match event.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(
|
||||
content,
|
||||
)) => match content.relates_to {
|
||||
Some(Relation::Thread(thread)) => {
|
||||
Some(thread.event_id.to_string())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let event = NotificationEvent::Timeline {
|
||||
event: Arc::new(crate::event::TimelineEvent(Box::new(
|
||||
deserialized,
|
||||
))),
|
||||
};
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to deserialize timeline event: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let event =
|
||||
NotificationEvent::Invite { sender: sender.to_string() };
|
||||
let thread_id = None;
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to deserialize stripped state event: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Compile sender info
|
||||
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
|
||||
let sender_info = if let Some(sender) = sender.as_ref() {
|
||||
NotificationSenderInfo {
|
||||
display_name: sender.display_name().map(|name| name.to_owned()),
|
||||
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
|
||||
is_name_ambiguous: sender.name_ambiguous(),
|
||||
}
|
||||
} else {
|
||||
NotificationSenderInfo {
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
is_name_ambiguous: false,
|
||||
}
|
||||
};
|
||||
|
||||
// Compile room info
|
||||
let display_name = match room.display_name().await {
|
||||
Ok(name) => name.to_string(),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to calculate the room's display name: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let is_direct = match room.is_direct().await {
|
||||
Ok(is_direct) => is_direct,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to determine if room is direct or not: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let room_info = NotificationRoomInfo {
|
||||
display_name,
|
||||
avatar_url: room.avatar_url().map(Into::into),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
topic: room.topic(),
|
||||
join_rule: room
|
||||
.join_rule()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
is_encrypted: Some(room.encryption_state().is_encrypted()),
|
||||
is_direct,
|
||||
is_space: room.is_space(),
|
||||
};
|
||||
|
||||
listener.on_notification(
|
||||
NotificationItem {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
has_mention: Some(has_mention),
|
||||
thread_id,
|
||||
actions: Some(actions),
|
||||
},
|
||||
room_id,
|
||||
);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Allows generic GET requests to be made through the SDK's internal HTTP
|
||||
/// client. This is useful when the caller's native HTTP client wouldn't
|
||||
/// have the same configuration (such as certificates, proxies, etc.) This
|
||||
@@ -811,15 +1048,23 @@ impl Client {
|
||||
|
||||
/// Empty the server version and unstable features cache.
|
||||
///
|
||||
/// Since the SDK caches server info (versions, unstable features,
|
||||
/// well-known etc), it's possible to have a stale entry in the cache.
|
||||
/// This functions makes it possible to force reset it.
|
||||
pub async fn reset_server_info(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_server_info().await?)
|
||||
/// Since the SDK caches the supported versions, it's possible to have a
|
||||
/// stale entry in the cache. This functions makes it possible to force
|
||||
/// reset it.
|
||||
pub async fn reset_supported_versions(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_supported_versions().await?)
|
||||
}
|
||||
|
||||
/// Empty the well-known cache.
|
||||
///
|
||||
/// Since the SDK caches the well-known, it's possible to have a stale
|
||||
/// entry in the cache. This functions makes it possible to force reset
|
||||
/// it.
|
||||
pub async fn reset_well_known(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_well_known().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Retrieves a media file from the media source
|
||||
@@ -833,22 +1078,60 @@ impl Client {
|
||||
use_cache: bool,
|
||||
temp_dir: Option<String>,
|
||||
) -> Result<Arc<MediaFileHandle>, ClientError> {
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let source = (*media_source).clone();
|
||||
let mime_type: mime::Mime = mime_type.parse()?;
|
||||
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
let handle = self
|
||||
.inner
|
||||
.media()
|
||||
.get_media_file(
|
||||
&MediaRequestParameters {
|
||||
source: source.media_source,
|
||||
format: MediaFormat::File,
|
||||
},
|
||||
filename,
|
||||
&mime_type,
|
||||
use_cache,
|
||||
temp_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
Ok(Arc::new(MediaFileHandle::new(handle)))
|
||||
}
|
||||
|
||||
/// MediaFileHandle uses SdkMediaFileHandle which requires an
|
||||
/// intermediate TempFile which is not available on wasm
|
||||
/// platforms due to lack of an accessible file system.
|
||||
#[cfg(target_family = "wasm")]
|
||||
Err(ClientError::Generic {
|
||||
msg: "get_media_file is not supported on wasm platforms".to_owned(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
self.inner
|
||||
.account()
|
||||
.set_display_name(Some(name.as_str()))
|
||||
.await
|
||||
.context("Unable to set display name")?;
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
self.inner.account().set_display_name(Some(name.as_str())).await.map_err(|e| {
|
||||
ClientError::Generic {
|
||||
msg: "Unable to set display name".to_owned(),
|
||||
details: Some(e.to_string()),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,14 +1159,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 {
|
||||
@@ -899,13 +1182,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>,
|
||||
@@ -984,15 +1283,6 @@ impl Client {
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.account()
|
||||
.set_display_name(Some(name.as_str()))
|
||||
.await
|
||||
.context("Unable to set display name")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner.account().upload_avatar(&mime, data).await?;
|
||||
@@ -1259,8 +1549,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))
|
||||
}
|
||||
|
||||
@@ -1510,8 +1800,8 @@ impl Client {
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, EventCacheError> {
|
||||
let store = self.inner.event_cache_store().lock().await?;
|
||||
let closure = async || -> Result<_, Error> {
|
||||
let store = self.inner.media_store().lock().await?;
|
||||
Ok(store.set_media_retention_policy(policy).await?)
|
||||
};
|
||||
|
||||
@@ -1559,13 +1849,13 @@ impl Client {
|
||||
|
||||
// Clean up the media cache according to the current media retention policy.
|
||||
self.inner
|
||||
.event_cache_store()
|
||||
.media_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?
|
||||
.clean_up_media_cache()
|
||||
.map_err(Error::from)?
|
||||
.clean()
|
||||
.await
|
||||
.map_err(EventCacheError::from)?;
|
||||
.map_err(Error::from)?;
|
||||
|
||||
// Clear all the room chunks. It's important to *not* call
|
||||
// `EventCacheStore::clear_all_linked_chunks` here, because there might be live
|
||||
@@ -1574,6 +1864,7 @@ impl Client {
|
||||
self.inner.event_cache().clear_all_rooms().await?;
|
||||
|
||||
// Delete the state store file, if it exists.
|
||||
#[cfg(feature = "sqlite")]
|
||||
if let Some(store_path) = &self.store_path {
|
||||
debug!("Removing the state store: {}", store_path.display());
|
||||
|
||||
@@ -1623,6 +1914,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
|
||||
@@ -1748,6 +2045,42 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
mod recent_emoji {
|
||||
use crate::{client::Client, error::ClientError};
|
||||
|
||||
/// Represents an emoji recently used for reactions.
|
||||
#[derive(Debug, uniffi::Record)]
|
||||
pub struct RecentEmoji {
|
||||
/// The actual emoji text representation.
|
||||
pub emoji: String,
|
||||
/// The number of times this emoji has been used for reactions.
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Adds a recently used emoji to the list and uploads the updated
|
||||
/// `io.element.recent_emoji` content to the global account data.
|
||||
pub async fn add_recent_emoji(&self, emoji: String) -> Result<(), ClientError> {
|
||||
Ok(self.inner.account().add_recent_emoji(&emoji).await?)
|
||||
}
|
||||
|
||||
/// Gets the list of recently used emojis from the
|
||||
/// `io.element.recent_emoji` global account data.
|
||||
pub async fn get_recent_emojis(&self) -> Result<Vec<RecentEmoji>, ClientError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.account()
|
||||
.get_recent_emojis(false)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(emoji, count)| RecentEmoji { emoji, count: count.into() })
|
||||
.collect::<Vec<RecentEmoji>>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait MediaPreviewConfigListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_change(&self, media_preview_config: Option<MediaPreviewConfig>);
|
||||
@@ -1841,8 +2174,9 @@ 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);
|
||||
@@ -1941,34 +2275,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()
|
||||
@@ -1980,7 +2297,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels.events = value
|
||||
.events
|
||||
.iter()
|
||||
@@ -1989,7 +2305,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
|
||||
(event_type, (*power_level).into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
power_levels
|
||||
}
|
||||
}
|
||||
@@ -2016,6 +2331,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 {
|
||||
@@ -2048,30 +2365,36 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
if value.is_encrypted {
|
||||
let content =
|
||||
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
|
||||
}
|
||||
|
||||
if let Some(url) = value.avatar {
|
||||
let mut content = RoomAvatarEventContent::new();
|
||||
content.url = Some(url.into());
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
|
||||
}
|
||||
|
||||
if let Some(join_rule_override) = value.join_rule_override {
|
||||
let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
|
||||
}
|
||||
|
||||
if let Some(history_visibility_override) = value.history_visibility_override {
|
||||
let content =
|
||||
RoomHistoryVisibilityEventContent::new(history_visibility_override.try_into()?);
|
||||
initial_state.push(InitialStateEvent::new(content).to_raw_any());
|
||||
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
|
||||
}
|
||||
|
||||
request.initial_state = initial_state;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -2307,25 +2630,25 @@ fn gen_transaction_id() -> String {
|
||||
|
||||
/// A file handle that takes ownership of a media file on disk. When the handle
|
||||
/// is dropped, the file will be removed from the disk.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct MediaFileHandle {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
inner: std::sync::RwLock<Option<SdkMediaFileHandle>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
impl MediaFileHandle {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn new(handle: SdkMediaFileHandle) -> Self {
|
||||
Self { inner: std::sync::RwLock::new(Some(handle)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaFileHandle {
|
||||
/// Get the media file's path.
|
||||
pub fn path(&self) -> Result<String, ClientError> {
|
||||
Ok(self
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
return Ok(self
|
||||
.inner
|
||||
.read()
|
||||
.unwrap()
|
||||
@@ -2334,24 +2657,37 @@ impl MediaFileHandle {
|
||||
.path()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned())
|
||||
.to_owned());
|
||||
#[cfg(target_family = "wasm")]
|
||||
Err(ClientError::Generic {
|
||||
msg: "MediaFileHandle.path() is not supported on WASM targets".to_string(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn persist(&self, path: String) -> Result<bool, ClientError> {
|
||||
let mut guard = self.inner.write().unwrap();
|
||||
Ok(
|
||||
match guard
|
||||
.take()
|
||||
.context("MediaFileHandle was already persisted")?
|
||||
.persist(path.as_ref())
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
*guard = Some(e.file);
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
let mut guard = self.inner.write().unwrap();
|
||||
Ok(
|
||||
match guard
|
||||
.take()
|
||||
.context("MediaFileHandle was already persisted")?
|
||||
.persist(path.as_ref())
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
*guard = Some(e.file);
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
Err(ClientError::Generic {
|
||||
msg: "MediaFileHandle.persist() is not supported on WASM targets".to_string(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2566,3 +2902,91 @@ impl TryFrom<RumaAllowRule> for AllowRule {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the disk size of the different stores, if known. It won't be
|
||||
/// available for in-memory stores.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct StoreSizes {
|
||||
/// The size of the CryptoStore.
|
||||
crypto_store: Option<u64>,
|
||||
/// The size of the StateStore.
|
||||
state_store: Option<u64>,
|
||||
/// The size of the EventCacheStore.
|
||||
event_cache_store: Option<u64>,
|
||||
/// The size of the MediaStore.
|
||||
media_store: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::StoreSizes> for StoreSizes {
|
||||
fn from(value: matrix_sdk::StoreSizes) -> Self {
|
||||
Self {
|
||||
crypto_store: value.crypto_store.map(|v| v as u64),
|
||||
state_store: value.state_store.map(|v| v as u64),
|
||||
event_cache_store: value.event_cache_store.map(|v| v as u64),
|
||||
media_store: value.media_store.map(|v| v as u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,9 +1,11 @@
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
// Allow UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
crypto::{CollectStrategy, DecryptionSettings, TrustRequirement},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
ruma::{ServerName, UserId},
|
||||
@@ -12,14 +14,21 @@ use matrix_sdk::{
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, SqliteStoreConfig, ThreadingSupport,
|
||||
RumaApiError, ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
use zeroize::Zeroizing;
|
||||
use tracing::debug;
|
||||
|
||||
use super::client::Client;
|
||||
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
|
||||
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
|
||||
use crate::store;
|
||||
use crate::{
|
||||
client::ClientSessionDelegate,
|
||||
error::ClientError,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
store::{StoreBuilder, StoreBuilderOutcome},
|
||||
};
|
||||
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
pub type CertificateBytes = Vec<u8>;
|
||||
@@ -100,11 +109,7 @@ impl From<ClientError> for ClientBuildError {
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ClientBuilder {
|
||||
session_paths: Option<SessionPaths>,
|
||||
session_passphrase: Zeroizing<Option<String>>,
|
||||
session_pool_max_size: Option<usize>,
|
||||
session_cache_size: Option<u32>,
|
||||
session_journal_size_limit: Option<u32>,
|
||||
store: Option<StoreBuilder>,
|
||||
system_is_memory_constrained: bool,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
@@ -143,23 +148,24 @@ impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
session_paths: None,
|
||||
session_passphrase: Zeroizing::new(None),
|
||||
session_pool_max_size: None,
|
||||
session_cache_size: None,
|
||||
session_journal_size_limit: None,
|
||||
store: None,
|
||||
system_is_memory_constrained: false,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
user_agent: None,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
proxy: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
cross_process_store_locks_holder_name: None,
|
||||
enable_oidc_refresh_lock: false,
|
||||
session_delegate: None,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
additional_root_certificates: Default::default(),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
disable_built_in_root_certificates: false,
|
||||
encryption_settings: EncryptionSettings {
|
||||
auto_enable_cross_signing: false,
|
||||
@@ -201,80 +207,13 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Sets the paths that the client will use to store its data and caches.
|
||||
/// Both paths **must** be unique per session as the SDK stores aren't
|
||||
/// capable of handling multiple users, however it is valid to use the
|
||||
/// same path for both stores on a single session.
|
||||
///
|
||||
/// Leaving this unset tells the client to use an in-memory data store.
|
||||
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_paths = Some(SessionPaths { data_path, cache_path });
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the passphrase for the stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
pub fn session_passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_passphrase = Zeroizing::new(passphrase);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the pool max size for the SQLite stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store exposes an async pool of connections. This method controls
|
||||
/// the size of the pool. The larger the pool is, the more memory is
|
||||
/// consumed, but also the more the app is reactive because it doesn't need
|
||||
/// to wait on a pool to be available to run queries.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
|
||||
pub fn session_pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_pool_max_size = pool_max_size
|
||||
.map(|size| size.try_into().expect("`pool_max_size` is too large to fit in `usize`"));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the cache size for the SQLite stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store exposes a SQLite connection. This method controls the cache
|
||||
/// size, in **bytes (!)**.
|
||||
///
|
||||
/// The cache represents data SQLite holds in memory at once per open
|
||||
/// database file. The default cache implementation does not allocate the
|
||||
/// full amount of cache memory all at once. Cache memory is allocated
|
||||
/// in smaller chunks on an as-needed basis.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::cache_size`] to learn more.
|
||||
pub fn session_cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_cache_size = cache_size;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the size limit for the SQLite WAL files of stores given to
|
||||
/// [`ClientBuilder::session_paths`].
|
||||
///
|
||||
/// Each store uses the WAL journal mode. This method controls the size
|
||||
/// limit of the WAL files, in **bytes (!)**.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
|
||||
pub fn session_journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.session_journal_size_limit = limit;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Tell the client that the system is memory constrained, like in a push
|
||||
/// notification process for example.
|
||||
///
|
||||
/// So far, at the time of writing (2025-04-07), it changes the defaults of
|
||||
/// [`SqliteStoreConfig`], so one might not need to call
|
||||
/// [`ClientBuilder::session_cache_size`] and siblings for example. Please
|
||||
/// check [`SqliteStoreConfig::with_low_memory_config`].
|
||||
/// `matrix_sdk::SqliteStoreConfig` (if the `sqlite` feature is enabled).
|
||||
/// Please check
|
||||
/// `matrix_sdk::SqliteStoreConfig::with_low_memory_config`.
|
||||
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.system_is_memory_constrained = true;
|
||||
@@ -404,6 +343,13 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Use in-memory session storage.
|
||||
pub fn in_memory_store(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::InMemory);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder();
|
||||
@@ -413,48 +359,26 @@ impl ClientBuilder {
|
||||
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
|
||||
}
|
||||
|
||||
let store_path = if let Some(session_paths) = &builder.session_paths {
|
||||
// This is the path where both the state store and the crypto store will live.
|
||||
let data_path = Path::new(&session_paths.data_path);
|
||||
// This is the path where the event cache store will live.
|
||||
let cache_path = Path::new(&session_paths.cache_path);
|
||||
let store_path = if let Some(store) = &builder.store {
|
||||
match store.build()? {
|
||||
#[cfg(feature = "sqlite")]
|
||||
StoreBuilderOutcome::Sqlite { config, cache_path, store_path: data_path } => {
|
||||
inner_builder = inner_builder
|
||||
.sqlite_store_with_config_and_cache_path(config, Some(cache_path));
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
event_cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data (state and crypto) and cache stores.",
|
||||
);
|
||||
Some(data_path)
|
||||
}
|
||||
#[cfg(feature = "indexeddb")]
|
||||
StoreBuilderOutcome::IndexedDb { name, passphrase } => {
|
||||
inner_builder = inner_builder.indexeddb_store(&name, passphrase.as_deref());
|
||||
|
||||
fs::create_dir_all(data_path)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
None
|
||||
}
|
||||
|
||||
let mut sqlite_store_config = if builder.system_is_memory_constrained {
|
||||
SqliteStoreConfig::with_low_memory_config(data_path)
|
||||
} else {
|
||||
SqliteStoreConfig::new(data_path)
|
||||
};
|
||||
|
||||
sqlite_store_config =
|
||||
sqlite_store_config.passphrase(builder.session_passphrase.as_deref());
|
||||
|
||||
if let Some(size) = builder.session_pool_max_size {
|
||||
sqlite_store_config = sqlite_store_config.pool_max_size(size);
|
||||
StoreBuilderOutcome::InMemory => None,
|
||||
}
|
||||
|
||||
if let Some(size) = builder.session_cache_size {
|
||||
sqlite_store_config = sqlite_store_config.cache_size(size);
|
||||
}
|
||||
|
||||
if let Some(limit) = builder.session_journal_size_limit {
|
||||
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
|
||||
}
|
||||
|
||||
inner_builder = inner_builder
|
||||
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
|
||||
|
||||
Some(data_path.to_owned())
|
||||
} else {
|
||||
debug!("Not using a store path.");
|
||||
debug!("Not using a session store");
|
||||
None
|
||||
};
|
||||
|
||||
@@ -574,21 +498,6 @@ impl ClientBuilder {
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
// Disable retries for this request to prevent it from being retried
|
||||
// indefinitely
|
||||
let config = sdk_client.request_config().disable_retry();
|
||||
|
||||
// Log server version information at info level.
|
||||
if let Ok(server_info) = sdk_client.server_vendor_info(Some(config)).await {
|
||||
tracing::info!(
|
||||
server_name = %server_info.server_name,
|
||||
version = %server_info.version,
|
||||
"Connected to Matrix server"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!("Could not retrieve server version information");
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(
|
||||
sdk_client,
|
||||
@@ -601,18 +510,62 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
/// Use SQLite as the session storage.
|
||||
pub fn sqlite_store(self: Arc<Self>, config: Arc<store::SqliteStoreBuilder>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::Sqlite(unwrap_or_clone_arc(config)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Sets the paths that the client will use to store its data and caches
|
||||
/// with SQLite.
|
||||
///
|
||||
/// Both paths **must** be unique per session as the SDK
|
||||
/// stores aren't capable of handling multiple users, however it is
|
||||
/// valid to use the same path for both stores on a single session.
|
||||
#[deprecated = "Use `ClientBuilder::session_store_with_sqlite` instead"]
|
||||
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store =
|
||||
Some(StoreBuilder::Sqlite(store::SqliteStoreBuilder::raw_new(data_path, cache_path)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
/// Use IndexedDB as the session storage.
|
||||
pub fn indexeddb_store(
|
||||
self: Arc<Self>,
|
||||
config: Arc<store::IndexedDbStoreBuilder>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.store = Some(StoreBuilder::IndexedDb(unwrap_or_clone_arc(config)));
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl ClientBuilder {
|
||||
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.proxy = Some(url);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.proxy = Some(url);
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_ssl_verification = true;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.disable_ssl_verification = true;
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -621,7 +574,11 @@ impl ClientBuilder {
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.additional_root_certificates = certificates;
|
||||
}
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
@@ -631,29 +588,25 @@ impl ClientBuilder {
|
||||
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
|
||||
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.disable_built_in_root_certificates = true;
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.user_agent = Some(user_agent);
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
{
|
||||
builder.user_agent = Some(user_agent);
|
||||
}
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// The store paths the client will use when built.
|
||||
#[derive(Clone)]
|
||||
struct SessionPaths {
|
||||
/// The path that the client will use to store its data.
|
||||
data_path: String,
|
||||
/// The path that the client will use to store its caches. This path can be
|
||||
/// the same as the data path if you prefer to keep everything in one place.
|
||||
cache_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
/// The config to use for HTTP requests by default in this client.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RequestConfig {
|
||||
/// Max number of retries.
|
||||
retry_limit: Option<u64>,
|
||||
|
||||
@@ -79,9 +79,14 @@ pub enum RecoveryError {
|
||||
#[error(transparent)]
|
||||
Client { source: crate::ClientError },
|
||||
|
||||
/// Error in the secret storage subsystem.
|
||||
/// Error in the secret storage subsystem, except for when importing a
|
||||
/// secret.
|
||||
#[error("Error in the secret-storage subsystem: {error_message}")]
|
||||
SecretStorage { error_message: String },
|
||||
|
||||
/// Error when importing a secret from secret storage.
|
||||
#[error("Error importing a secret: {error_message}")]
|
||||
Import { error_message: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
|
||||
@@ -89,6 +94,9 @@ impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
|
||||
match value {
|
||||
recovery::RecoveryError::BackupExistsOnServer => Self::BackupExistsOnServer,
|
||||
recovery::RecoveryError::Sdk(e) => Self::Client { source: ClientError::from(e) },
|
||||
recovery::RecoveryError::SecretStorage(
|
||||
matrix_sdk::encryption::secret_storage::SecretStorageError::ImportError { .. },
|
||||
) => Self::Import { error_message: value.to_string() },
|
||||
recovery::RecoveryError::SecretStorage(e) => {
|
||||
Self::SecretStorage { error_message: e.to_string() }
|
||||
}
|
||||
@@ -287,6 +295,15 @@ impl Encryption {
|
||||
Ok(self.inner.recovery().is_last_device().await?)
|
||||
}
|
||||
|
||||
/// Does the user have other devices that the current device can verify
|
||||
/// against?
|
||||
///
|
||||
/// The device must be signed by the user's cross-signing key, must have an
|
||||
/// identity, and must not be a dehydrated device.
|
||||
pub async fn has_devices_to_verify_against(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.has_devices_to_verify_against().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
&self,
|
||||
progress_listener: Option<Box<dyn BackupSteadyStateListener>>,
|
||||
@@ -417,11 +434,13 @@ impl Encryption {
|
||||
/// This method always tries to fetch the identity from the store, which we
|
||||
/// only have if the user is tracked, meaning that we are both members
|
||||
/// of the same encrypted room. If no user is found locally, a request will
|
||||
/// be made to the homeserver.
|
||||
/// be made to the homeserver unless `fallback_to_server` is set to `false`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user that the identity belongs to.
|
||||
/// * `fallback_to_server` - Should we request the user identity from the
|
||||
/// homeserver if one isn't found locally.
|
||||
///
|
||||
/// Returns a `UserIdentity` if one is found. Returns an error if there
|
||||
/// was an issue with the crypto store or with the request to the
|
||||
@@ -431,6 +450,7 @@ impl Encryption {
|
||||
pub async fn user_identity(
|
||||
&self,
|
||||
user_id: String,
|
||||
fallback_to_server: bool,
|
||||
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
|
||||
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
|
||||
Ok(Some(identity)) => {
|
||||
@@ -446,8 +466,12 @@ impl Encryption {
|
||||
|
||||
info!("Requesting identity from the server.");
|
||||
|
||||
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
|
||||
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
|
||||
if fallback_to_server {
|
||||
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
|
||||
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ use matrix_sdk::{
|
||||
encryption::{identities::RequestVerificationError, CryptoStoreError},
|
||||
event_cache::EventCacheError,
|
||||
reqwest,
|
||||
room::edit::EditError,
|
||||
room::{calls::CallError, edit::EditError},
|
||||
send_queue::RoomSendQueueError,
|
||||
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
|
||||
use ruma::{
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter},
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use tracing::warn;
|
||||
@@ -64,7 +64,9 @@ impl From<matrix_sdk::Error> for ClientError {
|
||||
match e {
|
||||
matrix_sdk::Error::Http(http_error) => {
|
||||
if let Some(api_error) = http_error.as_client_api_error() {
|
||||
if let ErrorBody::Standard { kind, message } = &api_error.body {
|
||||
if let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
|
||||
&api_error.body
|
||||
{
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
@@ -187,6 +189,12 @@ impl From<EditError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallError> for ClientError {
|
||||
fn from(e: CallError) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomSendQueueError> for ClientError {
|
||||
fn from(e: RoomSendQueueError) -> Self {
|
||||
Self::from_err(e)
|
||||
@@ -211,6 +219,12 @@ impl From<RequestVerificationError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<spaces::Error> for ClientError {
|
||||
fn from(e: spaces::Error) -> Self {
|
||||
Self::from_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
encrypted,
|
||||
message::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
TimelineEventType as RumaTimelineEventType,
|
||||
},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, NotifyType},
|
||||
ruma::{MessageType, RtcNotificationType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
@@ -40,16 +40,30 @@ impl TimelineEvent {
|
||||
self.0.origin_server_ts().into()
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
let event_type = match self.0.deref() {
|
||||
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
|
||||
/// thread.
|
||||
pub fn thread_root_event_id(&self) -> Option<String> {
|
||||
match &*self.0 {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
match event.original_content().and_then(|content| content.relation()) {
|
||||
Some(encrypted::Relation::Thread(thread)) => Some(thread.event_id.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
AnySyncTimelineEvent::State(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,16 +73,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 },
|
||||
}
|
||||
@@ -153,7 +367,11 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
pub enum MessageLikeEventContent {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
CallNotify { notify_type: NotifyType },
|
||||
RtcNotification {
|
||||
notification_type: RtcNotificationType,
|
||||
/// The timestamp at which this notification is considered invalid.
|
||||
expiration_ts: Timestamp,
|
||||
},
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
@@ -163,11 +381,21 @@ pub enum MessageLikeEventContent {
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationDone,
|
||||
Poll { question: String },
|
||||
ReactionContent { related_event_id: String },
|
||||
Poll {
|
||||
question: String,
|
||||
},
|
||||
ReactionContent {
|
||||
related_event_id: String,
|
||||
},
|
||||
RoomEncrypted,
|
||||
RoomMessage { message_type: MessageType, in_reply_to_event_id: Option<String> },
|
||||
RoomRedaction { redacted_event_id: Option<String>, reason: Option<String> },
|
||||
RoomMessage {
|
||||
message_type: MessageType,
|
||||
in_reply_to_event_id: Option<String>,
|
||||
},
|
||||
RoomRedaction {
|
||||
redacted_event_id: Option<String>,
|
||||
reason: Option<String>,
|
||||
},
|
||||
Sticker,
|
||||
}
|
||||
|
||||
@@ -178,10 +406,13 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
let content = match value {
|
||||
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
|
||||
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
|
||||
AnySyncMessageLikeEvent::CallNotify(content) => {
|
||||
let original_content = get_message_like_event_original_content(content)?;
|
||||
MessageLikeEventContent::CallNotify {
|
||||
notify_type: original_content.notify_type.into(),
|
||||
AnySyncMessageLikeEvent::RtcNotification(event) => {
|
||||
let origin_server_ts = event.origin_server_ts();
|
||||
let original_content = get_message_like_event_original_content(event)?;
|
||||
let expiration_ts = original_content.expiration_ts(origin_server_ts, None).into();
|
||||
MessageLikeEventContent::RtcNotification {
|
||||
notification_type: original_content.notification_type.into(),
|
||||
expiration_ts,
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
|
||||
@@ -270,9 +501,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,
|
||||
@@ -283,8 +516,10 @@ pub enum StateEventType {
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomImagePack,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomLanguage,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
@@ -294,12 +529,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,
|
||||
@@ -310,7 +548,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,
|
||||
@@ -321,17 +561,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,
|
||||
CallNegotiate,
|
||||
CallNotify,
|
||||
CallReject,
|
||||
CallSdpStreamMetadataChanged,
|
||||
CallSelectAnswer,
|
||||
Emote,
|
||||
Encrypted,
|
||||
File,
|
||||
Image,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationDone,
|
||||
@@ -339,6 +590,8 @@ pub enum MessageLikeEventType {
|
||||
KeyVerificationMac,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
Location,
|
||||
Message,
|
||||
PollEnd,
|
||||
PollResponse,
|
||||
PollStart,
|
||||
@@ -346,20 +599,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::CallNotify => Self::CallNotify,
|
||||
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,
|
||||
@@ -367,17 +639,22 @@ 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,
|
||||
MessageLikeEventType::Other(msgtype) => Self::from(msgtype),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::crypto::IdentityState;
|
||||
use matrix_sdk_base::crypto::IdentityState;
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct IdentityStatusChange {
|
||||
|
||||
@@ -26,6 +26,7 @@ mod ruma;
|
||||
mod runtime;
|
||||
mod session_verification;
|
||||
mod spaces;
|
||||
mod store;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
|
||||
@@ -3,6 +3,7 @@ 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};
|
||||
|
||||
@@ -36,12 +37,16 @@ pub struct NotificationRoomInfo {
|
||||
pub joined_members_count: u64,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_space: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
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,
|
||||
|
||||
@@ -51,6 +56,9 @@ pub struct NotificationItem {
|
||||
pub is_noisy: Option<bool>,
|
||||
pub has_mention: Option<bool>,
|
||||
pub thread_id: Option<String>,
|
||||
|
||||
/// The push actions for this notification (notify, sound, highlight, etc.).
|
||||
pub actions: Option<Vec<crate::notification_settings::Action>>,
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
@@ -63,8 +71,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,
|
||||
@@ -79,10 +94,14 @@ impl NotificationItem {
|
||||
joined_members_count: item.joined_members_count,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_space: item.is_space,
|
||||
},
|
||||
is_noisy: item.is_noisy,
|
||||
has_mention: item.has_mention,
|
||||
thread_id: item.thread_id.map(|t| t.to_string()),
|
||||
actions: item
|
||||
.actions
|
||||
.map(|a| a.into_iter().filter_map(|action| action.try_into().ok()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use matrix_sdk::{
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
events::push_rules::PushRulesEventContent,
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
|
||||
@@ -20,7 +21,7 @@ use ruma::{
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
|
||||
use crate::error::NotificationSettingsError;
|
||||
use crate::error::{ClientError, NotificationSettingsError};
|
||||
|
||||
#[derive(Clone, Default, uniffi::Enum)]
|
||||
pub enum ComparisonOperator {
|
||||
@@ -167,6 +168,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
#[allow(deprecated)]
|
||||
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
SdkPushCondition::RoomMemberCount { is } => {
|
||||
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
|
||||
@@ -189,6 +191,7 @@ impl From<PushCondition> for SdkPushCondition {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
#[allow(deprecated)]
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
|
||||
is: RoomMemberCountIs {
|
||||
@@ -770,4 +773,11 @@ impl NotificationSettings {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the raw push rules in JSON format.
|
||||
pub async fn get_raw_push_rules(&self) -> Result<Option<String>, ClientError> {
|
||||
let raw_push_rules =
|
||||
self.sdk_client.account().account_data::<PushRulesEventContent>().await?;
|
||||
Ok(raw_push_rules.map(|raw| serde_json::to_string(&raw)).transpose()?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing::warn;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing_core::Level;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
field::RecordFields,
|
||||
@@ -21,6 +23,8 @@ use tracing_subscriber::{
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use crate::tracing::BRIDGE_SPAN_NAME;
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
@@ -274,7 +278,8 @@ enum LogTarget {
|
||||
MatrixSdkBaseResponseProcessors,
|
||||
|
||||
// SDK common modules.
|
||||
MatrixSdkCommonStoreLocks,
|
||||
MatrixSdkCommonCrossProcessLock,
|
||||
MatrixSdkCommonDeserializedResponses,
|
||||
|
||||
// SDK modules.
|
||||
MatrixSdk,
|
||||
@@ -284,6 +289,7 @@ enum LogTarget {
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkLatestEvents,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkSendQueue,
|
||||
MatrixSdkSlidingSync,
|
||||
@@ -302,7 +308,10 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
|
||||
LogTarget::MatrixSdkBaseResponseProcessors => "matrix_sdk_base::response_processors",
|
||||
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock => "matrix_sdk_common::cross_process_lock",
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses => {
|
||||
"matrix_sdk_common::deserialized_responses"
|
||||
}
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
|
||||
@@ -311,6 +320,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",
|
||||
@@ -333,20 +343,22 @@ 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::MatrixSdkCommonStoreLocks, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkCommonCrossProcessLock, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkCommonDeserializedResponses, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseResponseProcessors, LogLevel::Debug),
|
||||
];
|
||||
|
||||
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
LogTarget::MatrixSdkCommonStoreLocks, // Too verbose
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock, // Too verbose
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
|
||||
];
|
||||
|
||||
/// A log pack can be used to set the trace log level for a group of multiple
|
||||
@@ -363,6 +375,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 {
|
||||
@@ -374,15 +388,26 @@ impl TraceLogPacks {
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
LogTarget::MatrixSdkBaseEventCache,
|
||||
LogTarget::MatrixSdkEventCacheStore,
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
|
||||
TraceLogPacks::Timeline => {
|
||||
&[LogTarget::MatrixSdkUiTimeline, LogTarget::MatrixSdkCommonDeserializedResponses]
|
||||
}
|
||||
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
|
||||
TraceLogPacks::SyncProfiling => &[
|
||||
LogTarget::MatrixSdkSlidingSync,
|
||||
LogTarget::MatrixSdkBaseSlidingSync,
|
||||
LogTarget::MatrixSdkBaseResponseProcessors,
|
||||
LogTarget::MatrixSdkCrypto,
|
||||
LogTarget::MatrixSdkCommonCrossProcessLock,
|
||||
LogTarget::MatrixSdkCommonDeserializedResponses,
|
||||
],
|
||||
TraceLogPacks::LatestEvents => &[
|
||||
LogTarget::MatrixSdkLatestEvents,
|
||||
LogTarget::MatrixSdkSendQueue,
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -454,7 +479,14 @@ impl TracingConfiguration {
|
||||
let sentry_guard = sentry::init((
|
||||
sentry_dsn,
|
||||
sentry::ClientOptions {
|
||||
traces_sample_rate: 0.0,
|
||||
traces_sampler: Some(Arc::new(|ctx| {
|
||||
// Make sure bridge spans are always uploaded
|
||||
if ctx.name() == BRIDGE_SPAN_NAME {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})),
|
||||
attach_stacktrace: true,
|
||||
release: Some(env!("VERGEN_GIT_SHA").into()),
|
||||
..sentry::ClientOptions::default()
|
||||
@@ -488,7 +520,10 @@ impl TracingConfiguration {
|
||||
|
||||
move |metadata| {
|
||||
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
sentry_tracing::default_span_filter(metadata)
|
||||
matches!(
|
||||
metadata.level(),
|
||||
&Level::ERROR | &Level::WARN | &Level::INFO | &Level::DEBUG
|
||||
)
|
||||
} else {
|
||||
// Ignore, if sentry is globally disabled.
|
||||
false
|
||||
@@ -721,9 +756,11 @@ 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::store_locks=warn,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
@@ -765,9 +802,11 @@ 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::store_locks=warn,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=trace,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=trace,
|
||||
matrix_sdk_base::response_processors=trace,
|
||||
@@ -810,9 +849,11 @@ 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::store_locks=warn,
|
||||
matrix_sdk_common::cross_process_lock=warn,
|
||||
matrix_sdk_common::deserialized_responses=trace,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
matrix_sdk_ui::notification_client=info,
|
||||
matrix_sdk_base::response_processors=debug,
|
||||
|
||||
@@ -1,11 +1,222 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
qrcode::{
|
||||
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
|
||||
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
|
||||
},
|
||||
OAuth,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use tracing::error;
|
||||
use matrix_sdk_base::crypto::types::qr_login;
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
/// Handler for logging in with a QR code.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LoginWithQrCodeHandler {
|
||||
oauth: OAuth,
|
||||
oidc_configuration: OidcConfiguration,
|
||||
}
|
||||
|
||||
impl LoginWithQrCodeHandler {
|
||||
pub(crate) fn new(oauth: OAuth, oidc_configuration: OidcConfiguration) -> Self {
|
||||
Self { oauth, oidc_configuration }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LoginWithQrCodeHandler {
|
||||
/// This method allows you to log in with a scanned QR code.
|
||||
///
|
||||
/// The existing device needs to display the QR code which this device can
|
||||
/// scan, call this method and handle its progress updates to log in.
|
||||
///
|
||||
/// For the login to succeed, the [`Client`] associated with the
|
||||
/// [`LoginWithQrCodeHandler`] must have been built with
|
||||
/// [`QrCodeData::server_name`] as the server name.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where this device generates the QR code for the
|
||||
/// existing device to scan, use [`LoginWithQrCodeHandler::generate`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// transfer the [`CheckCode`] to the existing device.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn scan(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
progress_listener: Box<dyn QrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = self
|
||||
.oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let login =
|
||||
self.oauth.login_with_qr_code(Some(®istration_data)).scan(&qr_code_data.inner);
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This method allows you to log in by generating a QR code.
|
||||
///
|
||||
/// This device needs to call this method and handle its progress updates to
|
||||
/// generate a QR code which the existing device can scan and grant the
|
||||
/// log in.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where the existing device generates the QR code
|
||||
/// for this device to scan, use [`LoginWithQrCodeHandler::scan`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn generate(
|
||||
self: Arc<Self>,
|
||||
progress_listener: Box<dyn GeneratedQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrLoginError> {
|
||||
let registration_data = self
|
||||
.oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let login = self.oauth.login_with_qr_code(Some(®istration_data)).generate();
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
login.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for granting login in with a QR code.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct GrantLoginWithQrCodeHandler {
|
||||
oauth: OAuth,
|
||||
}
|
||||
|
||||
impl GrantLoginWithQrCodeHandler {
|
||||
pub(crate) fn new(oauth: OAuth) -> Self {
|
||||
Self { oauth }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl GrantLoginWithQrCodeHandler {
|
||||
/// This method allows you to grant login with a scanned QR code.
|
||||
///
|
||||
/// The new device needs to display the QR code which this device can
|
||||
/// scan, call this method and handle its progress updates to grant the
|
||||
/// login.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where this device generates the QR code for the
|
||||
/// existing device to scan, use [`GrantLoginWithQrCodeHandler::generate`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// transfer the [`CheckCode`] to the new device.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn scan(
|
||||
self: Arc<Self>,
|
||||
qr_code_data: &QrCodeData,
|
||||
progress_listener: Box<dyn GrantQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrGrantLoginError> {
|
||||
let grant = self.oauth.grant_login_with_qr_code().scan(&qr_code_data.inner);
|
||||
|
||||
let mut progress = grant.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
grant.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This method allows you to grant login by generating a QR code.
|
||||
///
|
||||
/// This device needs to call this method and handle its progress updates to
|
||||
/// generate a QR code which the new device can scan to log in.
|
||||
///
|
||||
/// This method uses the login mechanism described in [MSC4108]. As such,
|
||||
/// it requires OAuth 2.0 support.
|
||||
///
|
||||
/// For the reverse flow where the existing device generates the QR code
|
||||
/// for this device to scan, use [`GrantLoginWithQrCodeHandler::scan`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `progress_listener` - A progress listener that must also be used to
|
||||
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
|
||||
///
|
||||
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
|
||||
pub async fn generate(
|
||||
self: Arc<Self>,
|
||||
progress_listener: Box<dyn GrantGeneratedQrLoginProgressListener>,
|
||||
) -> Result<(), HumanQrGrantLoginError> {
|
||||
let grant = self.oauth.grant_login_with_qr_code().generate();
|
||||
|
||||
let mut progress = grant.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
}));
|
||||
|
||||
grant.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Data for the QR code login mechanism.
|
||||
///
|
||||
@@ -26,15 +237,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 {
|
||||
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +268,7 @@ pub enum QrCodeDecodeError {
|
||||
#[error("Error decoding QR code: {error:?}")]
|
||||
Crypto {
|
||||
#[from]
|
||||
error: LoginQrCodeDecodeError,
|
||||
error: qrcode::LoginQrCodeDecodeError,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,6 +292,12 @@ pub enum HumanQrLoginError {
|
||||
OidcMetadataInvalid,
|
||||
#[error("The other device is not signed in and as such can't sign in other devices.")]
|
||||
OtherDeviceNotSignedIn,
|
||||
#[error("The check code was already sent.")]
|
||||
CheckCodeAlreadySent,
|
||||
#[error("The check code could not be sent.")]
|
||||
CheckCodeCannotBeSent,
|
||||
#[error("The rendezvous session was not found and might have expired")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
@@ -100,10 +328,14 @@ 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 => HumanQrLoginError::ConnectionInsecure,
|
||||
| SecureChannelError::InvalidCheckCode
|
||||
| SecureChannelError::CannotReceiveCheckCode => {
|
||||
HumanQrLoginError::ConnectionInsecure
|
||||
}
|
||||
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
|
||||
},
|
||||
|
||||
@@ -112,12 +344,77 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
| QRCodeLoginError::DeviceKeyUpload(_)
|
||||
| QRCodeLoginError::SessionTokens(_)
|
||||
| QRCodeLoginError::UserIdDiscovery(_)
|
||||
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
|
||||
| QRCodeLoginError::SecretImport(_)
|
||||
| QRCodeLoginError::ServerReset(_) => HumanQrLoginError::Unknown,
|
||||
|
||||
QRCodeLoginError::NotFound => HumanQrLoginError::NotFound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of the QR-code login.
|
||||
impl From<CheckCodeSenderError> for HumanQrLoginError {
|
||||
fn from(value: CheckCodeSenderError) -> Self {
|
||||
match value {
|
||||
CheckCodeSenderError::AlreadySent => HumanQrLoginError::CheckCodeAlreadySent,
|
||||
CheckCodeSenderError::CannotSend => HumanQrLoginError::CheckCodeCannotBeSent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum HumanQrGrantLoginError {
|
||||
/// The requested device ID is already in use.
|
||||
#[error("The requested device ID is already in use.")]
|
||||
DeviceIDAlreadyInUse,
|
||||
|
||||
/// The check code was incorrect.
|
||||
#[error("The check code was incorrect.")]
|
||||
InvalidCheckCode,
|
||||
|
||||
/// The other client proposed an unsupported protocol.
|
||||
#[error("Unsupported protocol: {0}")]
|
||||
UnsupportedProtocol(String),
|
||||
|
||||
/// Secrets backup not set up properly.
|
||||
#[error("Secrets backup not set up: {0}")]
|
||||
MissingSecretsBackup(String),
|
||||
|
||||
/// The rendezvous session was not found and might have expired.
|
||||
#[error("The rendezvous session was not found and might have expired")]
|
||||
NotFound,
|
||||
|
||||
/// The device could not be created.
|
||||
#[error("The device could not be created.")]
|
||||
UnableToCreateDevice,
|
||||
|
||||
/// An unknown error has happened.
|
||||
#[error("An unknown error has happened.")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
|
||||
fn from(value: qrcode::QRCodeGrantLoginError) -> Self {
|
||||
use qrcode::QRCodeGrantLoginError;
|
||||
|
||||
match value {
|
||||
QRCodeGrantLoginError::DeviceIDAlreadyInUse => Self::DeviceIDAlreadyInUse,
|
||||
QRCodeGrantLoginError::InvalidCheckCode => Self::InvalidCheckCode,
|
||||
QRCodeGrantLoginError::UnableToCreateDevice => Self::UnableToCreateDevice,
|
||||
QRCodeGrantLoginError::UnsupportedProtocol(protocol) => {
|
||||
Self::UnsupportedProtocol(protocol.to_string())
|
||||
}
|
||||
QRCodeGrantLoginError::MissingSecretsBackup(error) => {
|
||||
Self::MissingSecretsBackup(error.map_or("other".to_owned(), |e| e.to_string()))
|
||||
}
|
||||
QRCodeGrantLoginError::NotFound => Self::NotFound,
|
||||
QRCodeGrantLoginError::Unknown(string) => Self::Unknown(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of logging in by scanning a QR code that was
|
||||
/// generated on an existing device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum QrLoginProgress {
|
||||
/// The login process is starting.
|
||||
@@ -136,6 +433,8 @@ pub enum QrLoginProgress {
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
@@ -145,13 +444,13 @@ pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: QrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress) -> Self {
|
||||
impl From<qrcode::LoginProgress<QrProgress>> for QrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress<QrProgress>) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel { check_code } => {
|
||||
LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
@@ -160,7 +459,185 @@ impl From<qrcode::LoginProgress> for QrLoginProgress {
|
||||
}
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of logging in by generating a QR code and
|
||||
/// having an existing device scan it.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GeneratedQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We have established the secure channel and now need to display the
|
||||
/// QR code so that the existing device can scan it.
|
||||
QrReady { qr_code: Arc<QrCodeData> },
|
||||
/// The existing device has scanned the QR code and is displaying the
|
||||
/// checkcode. We now need to ask the user to enter the checkcode so that
|
||||
/// we can verify that the channel is indeed secure.
|
||||
QrScanned { check_code_sender: Arc<CheckCodeSender> },
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GeneratedQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::LoginProgress<GeneratedQrProgress>> for GeneratedQrLoginProgress {
|
||||
fn from(value: qrcode::LoginProgress<GeneratedQrProgress>) -> Self {
|
||||
use qrcode::LoginProgress;
|
||||
|
||||
match value {
|
||||
LoginProgress::Starting => Self::Starting,
|
||||
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
|
||||
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
|
||||
}
|
||||
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(inner)) => {
|
||||
Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) }
|
||||
}
|
||||
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
|
||||
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
LoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of granting login in by scanning a QR code that
|
||||
/// was generated on a new device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GrantQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We established a secure channel with the other device.
|
||||
EstablishingSecureChannel {
|
||||
/// The check code that the device should display so the other device
|
||||
/// can confirm that the channel is secure as well.
|
||||
check_code: u8,
|
||||
/// The string representation of the check code, will be guaranteed to
|
||||
/// be 2 characters long, preserving the leading zero if the
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// The secure channel has been confirmed using the [`CheckCode`] and this
|
||||
/// device is waiting for the authorization to complete.
|
||||
WaitingForAuth {
|
||||
/// A URI to open in a (secure) system browser to verify the new login.
|
||||
verification_uri: String,
|
||||
},
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GrantQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GrantQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::GrantLoginProgress<QrProgress>> for GrantQrLoginProgress {
|
||||
fn from(value: qrcode::GrantLoginProgress<QrProgress>) -> Self {
|
||||
use qrcode::GrantLoginProgress;
|
||||
|
||||
match value {
|
||||
GrantLoginProgress::Starting => Self::Starting,
|
||||
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
|
||||
let check_code = check_code.to_digit();
|
||||
|
||||
Self::EstablishingSecureChannel {
|
||||
check_code,
|
||||
check_code_string: format!("{check_code:02}"),
|
||||
}
|
||||
}
|
||||
GrantLoginProgress::WaitingForAuth { verification_uri } => {
|
||||
Self::WaitingForAuth { verification_uri: verification_uri.into() }
|
||||
}
|
||||
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
GrantLoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the progress of granting login by generating a QR code to
|
||||
/// be scanned on the new device.
|
||||
#[derive(Debug, Default, Clone, uniffi::Enum)]
|
||||
pub enum GrantGeneratedQrLoginProgress {
|
||||
/// The login process is starting.
|
||||
#[default]
|
||||
Starting,
|
||||
/// We have established the secure channel and now need to display the
|
||||
/// QR code so that the existing device can scan it.
|
||||
QrReady { qr_code: Arc<QrCodeData> },
|
||||
/// The existing device has scanned the QR code and is displaying the
|
||||
/// checkcode. We now need to ask the user to enter the checkcode so that
|
||||
/// we can verify that the channel is indeed secure.
|
||||
QrScanned { check_code_sender: Arc<CheckCodeSender> },
|
||||
/// The secure channel has been confirmed using the [`CheckCode`] and this
|
||||
/// device is waiting for the authorization to complete.
|
||||
WaitingForAuth {
|
||||
/// A URI to open in a (secure) system browser to verify the new login.
|
||||
verification_uri: String,
|
||||
},
|
||||
/// We are syncing secrets.
|
||||
SyncingSecrets,
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait GrantGeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn on_update(&self, state: GrantGeneratedQrLoginProgress);
|
||||
}
|
||||
|
||||
impl From<qrcode::GrantLoginProgress<GeneratedQrProgress>> for GrantGeneratedQrLoginProgress {
|
||||
fn from(value: qrcode::GrantLoginProgress<GeneratedQrProgress>) -> Self {
|
||||
use qrcode::GrantLoginProgress;
|
||||
|
||||
match value {
|
||||
GrantLoginProgress::Starting => Self::Starting,
|
||||
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
|
||||
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
|
||||
}
|
||||
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(
|
||||
inner,
|
||||
)) => Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) },
|
||||
GrantLoginProgress::WaitingForAuth { verification_uri } => {
|
||||
Self::WaitingForAuth { verification_uri: verification_uri.into() }
|
||||
}
|
||||
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
|
||||
GrantLoginProgress::Done => Self::Done,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, uniffi::Object)]
|
||||
/// Used to pass back the [`CheckCode`] entered by the user to verify that the
|
||||
/// secure channel is indeed secure.
|
||||
pub struct CheckCodeSender {
|
||||
inner: SdkCheckCodeSender,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl CheckCodeSender {
|
||||
/// Send the [`CheckCode`].
|
||||
///
|
||||
/// Calling this method more than once will result in an error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `check_code` - The check code in digits representation.
|
||||
pub async fn send(&self, code: u8) -> Result<(), HumanQrLoginError> {
|
||||
self.inner.send(code).await.map_err(HumanQrLoginError::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges,
|
||||
ListThreadsOptions as SdkListThreadsOptions, Room as SdkRoom, RoomMemberRole,
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
|
||||
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
|
||||
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
SuccessorRoom as SdkSuccessorRoom,
|
||||
};
|
||||
@@ -19,14 +22,15 @@ use matrix_sdk_ui::{
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads,
|
||||
assign,
|
||||
events::{
|
||||
call::notify,
|
||||
receipt::ReceiptThread,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
MediaSource,
|
||||
MediaSource as RumaMediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
@@ -39,16 +43,19 @@ use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
|
||||
event::TimelineEvent,
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{ImageInfo, LocationContent},
|
||||
ruma::{
|
||||
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
|
||||
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
|
||||
},
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
@@ -227,11 +234,8 @@ impl Room {
|
||||
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
if configuration.track_read_receipts {
|
||||
builder = builder.track_read_marker_and_receipts();
|
||||
}
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into())
|
||||
.track_read_marker_and_receipts(configuration.track_read_receipts);
|
||||
|
||||
match configuration.filter {
|
||||
TimelineFilter::All => {
|
||||
@@ -255,10 +259,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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -300,12 +304,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> {
|
||||
@@ -345,6 +345,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:
|
||||
@@ -670,6 +678,25 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a room as fully read, by attaching a read receipt to the provided
|
||||
/// `event_id`.
|
||||
///
|
||||
/// **Warning:** using this method is **NOT** recommended, as providing the
|
||||
/// latest event id can cause incorrect read receipts. This method won't
|
||||
/// check if sending the read receipt is necessary or valid. It should
|
||||
/// *only* be used when some constraint prevents you from instantiating a
|
||||
/// [`Timeline`]. For any other case use [`Timeline::mark_as_read`]
|
||||
/// instead.
|
||||
pub async fn mark_as_fully_read_unchecked(&self, event_id: String) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
|
||||
self.inner
|
||||
.send_single_receipt(ReceiptType::FullyRead.into(), ReceiptThread::Unthreaded, event_id)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
|
||||
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
|
||||
Ok(Arc::new(RoomPowerLevels::new(power_levels, self.inner.own_user_id().to_owned())))
|
||||
@@ -735,6 +762,37 @@ impl Room {
|
||||
self.inner.send_queue().set_enabled(enable);
|
||||
}
|
||||
|
||||
/// Subscribe to all send queue updates in this room.
|
||||
///
|
||||
/// The given listener will be immediately called with
|
||||
/// `RoomSendQueueUpdate::NewLocalEvent` for each local echo existing in
|
||||
/// the queue.
|
||||
pub async fn subscribe_to_send_queue_updates(
|
||||
&self,
|
||||
listener: Box<dyn SendQueueListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let q = self.inner.send_queue();
|
||||
let (local_echoes, mut subscriber) = q.subscribe().await?;
|
||||
|
||||
for local_echo in local_echoes {
|
||||
listener.on_update(RoomSendQueueUpdate::NewLocalEvent {
|
||||
transaction_id: local_echo.transaction_id.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
loop {
|
||||
match subscriber.recv().await {
|
||||
Ok(update) => match update.try_into() {
|
||||
Ok(update) => listener.on_update(update),
|
||||
Err(err) => error!("error when converting send queue update: {err}"),
|
||||
},
|
||||
Err(err) => error!("error when listening for send queue updates: {err}"),
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Store the given `ComposerDraft` in the state store using the current
|
||||
/// room id, as identifier.
|
||||
pub async fn save_composer_draft(
|
||||
@@ -1010,6 +1068,44 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Declines a call (and stop ringing).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `rtc_notification_event_id` - the event id of the m.rtc.notification
|
||||
/// event.
|
||||
pub async fn decline_call(&self, rtc_notification_event_id: String) -> Result<(), ClientError> {
|
||||
let parsed_id = EventId::parse(rtc_notification_event_id.as_str())?;
|
||||
|
||||
let content = self.inner.make_decline_call_event(&parsed_id).await?;
|
||||
|
||||
self.inner.send_queue().send(content.into()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to call decline for a currently ringing call, using a
|
||||
/// `listener` to be notified when someone declines.
|
||||
///
|
||||
/// Will error if `rtc_notification_event_id` is not a valid event id.
|
||||
/// Use the [`TaskHandle`] to cancel the subscription.
|
||||
pub fn subscribe_to_call_decline_events(
|
||||
self: Arc<Self>,
|
||||
rtc_notification_event_id: String,
|
||||
listener: Box<dyn CallDeclineListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let parsed_id = EventId::parse(rtc_notification_event_id.as_str())?;
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let (_event_handler_drop_guard, mut subscriber) =
|
||||
self.inner.subscribe_to_call_decline_events(&parsed_id);
|
||||
|
||||
while let Ok(user_id) = subscriber.recv().await {
|
||||
listener.call(user_id.to_string());
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Subscribes to live location shares in this room, using a `listener` to
|
||||
/// be notified of the changes.
|
||||
///
|
||||
@@ -1141,6 +1237,57 @@ impl Room {
|
||||
.await?
|
||||
.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(
|
||||
&self,
|
||||
event_id: String,
|
||||
) -> Result<TimelineEvent, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let timeline_event = self.inner.load_or_fetch_event(&event_id, None).await?;
|
||||
Ok(timeline_event
|
||||
.kind
|
||||
.into_raw()
|
||||
.deserialize()?
|
||||
.into_full_event(self.inner.room_id().to_owned())
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread subscription (MSC4306).
|
||||
@@ -1151,12 +1298,72 @@ 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 {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
/// A listener for receiving call decline events in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait CallDeclineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, decliner_user_id: String);
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
|
||||
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
|
||||
Self {
|
||||
@@ -1321,8 +1528,8 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
|
||||
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
|
||||
match &media_source.as_ref().media_source {
|
||||
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
RumaMediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
RumaMediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -1340,18 +1547,6 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RtcApplicationType {
|
||||
Call,
|
||||
}
|
||||
impl From<RtcApplicationType> for notify::ApplicationType {
|
||||
fn from(value: RtcApplicationType) -> Self {
|
||||
match value {
|
||||
RtcApplicationType::Call => notify::ApplicationType::Call,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current draft of the composer for the room.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ComposerDraft {
|
||||
@@ -1362,21 +1557,257 @@ pub struct ComposerDraft {
|
||||
pub html_text: Option<String>,
|
||||
/// The type of draft.
|
||||
pub draft_type: ComposerDraftType,
|
||||
/// Attachments associated with this draft.
|
||||
pub attachments: Vec<DraftAttachment>,
|
||||
}
|
||||
|
||||
impl From<SdkComposerDraft> for ComposerDraft {
|
||||
fn from(value: SdkComposerDraft) -> Self {
|
||||
let SdkComposerDraft { plain_text, html_text, draft_type } = value;
|
||||
Self { plain_text, html_text, draft_type: draft_type.into() }
|
||||
let SdkComposerDraft { plain_text, html_text, draft_type, attachments } = value;
|
||||
Self {
|
||||
plain_text,
|
||||
html_text,
|
||||
draft_type: draft_type.into(),
|
||||
attachments: attachments.into_iter().map(|a| a.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ComposerDraft> for SdkComposerDraft {
|
||||
type Error = ruma::IdParseError;
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: ComposerDraft) -> std::result::Result<Self, Self::Error> {
|
||||
let ComposerDraft { plain_text, html_text, draft_type } = value;
|
||||
Ok(Self { plain_text, html_text, draft_type: draft_type.try_into()? })
|
||||
let ComposerDraft { plain_text, html_text, draft_type, attachments } = value;
|
||||
Ok(Self {
|
||||
plain_text,
|
||||
html_text,
|
||||
draft_type: draft_type.try_into()?,
|
||||
attachments: attachments
|
||||
.into_iter()
|
||||
.map(|a| a.try_into())
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An attachment stored with a composer draft.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DraftAttachment {
|
||||
Audio { audio_info: AudioInfo, source: UploadSource },
|
||||
File { file_info: FileInfo, source: UploadSource },
|
||||
Image { image_info: ImageInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
|
||||
Video { video_info: VideoInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
|
||||
}
|
||||
|
||||
impl From<SdkDraftAttachment> for DraftAttachment {
|
||||
fn from(value: SdkDraftAttachment) -> Self {
|
||||
match value.content {
|
||||
DraftAttachmentContent::Image {
|
||||
data,
|
||||
mimetype,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
blurhash,
|
||||
thumbnail,
|
||||
} => {
|
||||
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
|
||||
bytes: t.data.clone(),
|
||||
filename: t.filename.clone(),
|
||||
});
|
||||
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
|
||||
width: t.width,
|
||||
height: t.height,
|
||||
mimetype: t.mimetype,
|
||||
size: t.size,
|
||||
});
|
||||
DraftAttachment::Image {
|
||||
image_info: ImageInfo {
|
||||
height,
|
||||
width,
|
||||
mimetype,
|
||||
size,
|
||||
thumbnail_info,
|
||||
thumbnail_source: None,
|
||||
blurhash,
|
||||
is_animated: None,
|
||||
},
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
thumbnail_source,
|
||||
}
|
||||
}
|
||||
DraftAttachmentContent::Video {
|
||||
data,
|
||||
mimetype,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
duration,
|
||||
blurhash,
|
||||
thumbnail,
|
||||
} => {
|
||||
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
|
||||
bytes: t.data.clone(),
|
||||
filename: t.filename.clone(),
|
||||
});
|
||||
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
|
||||
width: t.width,
|
||||
height: t.height,
|
||||
mimetype: t.mimetype,
|
||||
size: t.size,
|
||||
});
|
||||
DraftAttachment::Video {
|
||||
video_info: VideoInfo {
|
||||
duration,
|
||||
height,
|
||||
width,
|
||||
mimetype,
|
||||
size,
|
||||
thumbnail_info,
|
||||
thumbnail_source: None,
|
||||
blurhash,
|
||||
},
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
thumbnail_source,
|
||||
}
|
||||
}
|
||||
DraftAttachmentContent::Audio { data, mimetype, size, duration } => {
|
||||
DraftAttachment::Audio {
|
||||
audio_info: AudioInfo { duration, size, mimetype },
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
}
|
||||
}
|
||||
DraftAttachmentContent::File { data, mimetype, size } => DraftAttachment::File {
|
||||
file_info: FileInfo {
|
||||
mimetype,
|
||||
size,
|
||||
thumbnail_info: None,
|
||||
thumbnail_source: None,
|
||||
},
|
||||
source: UploadSource::Data { bytes: data, filename: value.filename },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the bytes and filename from an `UploadSource`, reading the file
|
||||
/// contents if needed.
|
||||
fn read_upload_source(source: UploadSource) -> Result<(Vec<u8>, String), ClientError> {
|
||||
match source {
|
||||
UploadSource::Data { bytes, filename } => Ok((bytes, filename)),
|
||||
UploadSource::File { filename } => {
|
||||
let path: PathBuf = filename.into();
|
||||
let filename = path
|
||||
.file_name()
|
||||
.ok_or(ClientError::Generic {
|
||||
msg: "Invalid attachment path".to_owned(),
|
||||
details: None,
|
||||
})?
|
||||
.to_str()
|
||||
.ok_or(ClientError::Generic {
|
||||
msg: "Invalid attachment path".to_owned(),
|
||||
details: None,
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
let bytes = fs::read(&path).map_err(|_| ClientError::Generic {
|
||||
msg: "Could not load file".to_owned(),
|
||||
details: None,
|
||||
})?;
|
||||
|
||||
Ok((bytes, filename))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
DraftAttachment::Image { image_info, source, thumbnail_source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
let thumbnail = match (image_info.thumbnail_info, thumbnail_source) {
|
||||
(Some(info), Some(source)) => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Image {
|
||||
data,
|
||||
mimetype: image_info.mimetype,
|
||||
size: image_info.size,
|
||||
width: image_info.width,
|
||||
height: image_info.height,
|
||||
blurhash: image_info.blurhash,
|
||||
thumbnail,
|
||||
},
|
||||
})
|
||||
}
|
||||
DraftAttachment::Video { video_info, source, thumbnail_source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
let thumbnail = match (video_info.thumbnail_info, thumbnail_source) {
|
||||
(Some(info), Some(source)) => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Video {
|
||||
data,
|
||||
mimetype: video_info.mimetype,
|
||||
size: video_info.size,
|
||||
width: video_info.width,
|
||||
height: video_info.height,
|
||||
duration: video_info.duration,
|
||||
blurhash: video_info.blurhash,
|
||||
thumbnail,
|
||||
},
|
||||
})
|
||||
}
|
||||
DraftAttachment::Audio { audio_info, source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Audio {
|
||||
data,
|
||||
mimetype: audio_info.mimetype,
|
||||
size: audio_info.size,
|
||||
duration: audio_info.duration,
|
||||
},
|
||||
})
|
||||
}
|
||||
DraftAttachment::File { file_info, source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::File {
|
||||
data,
|
||||
mimetype: file_info.mimetype,
|
||||
size: file_info.size,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1521,3 +1952,126 @@ impl From<SdkPredecessorRoom> for PredecessorRoom {
|
||||
Self { room_id: value.room_id.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener to send queue updates in a specific room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait SendQueueListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
/// Called every time the send queue dispatches an update for the given
|
||||
/// room.
|
||||
fn on_update(&self, update: RoomSendQueueUpdate);
|
||||
}
|
||||
|
||||
/// An update to a room send queue.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomSendQueueUpdate {
|
||||
/// A new local event is being sent.
|
||||
NewLocalEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// A local event that hadn't been sent to the server yet has been cancelled
|
||||
/// before sending.
|
||||
CancelledLocalEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// A local event's content has been replaced with something else.
|
||||
ReplacedLocalEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// An error happened when an event was being sent.
|
||||
///
|
||||
/// The event has not been removed from the queue. All the send queues
|
||||
/// will be disabled after this happens, and must be manually re-enabled.
|
||||
SendError {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
/// Error received while sending the event.
|
||||
error: QueueWedgeError,
|
||||
/// Whether the error is considered recoverable or not.
|
||||
///
|
||||
/// An error that's recoverable will disable the room's send queue,
|
||||
/// while an unrecoverable error will be parked, until the user
|
||||
/// decides to cancel sending it.
|
||||
is_recoverable: bool,
|
||||
},
|
||||
|
||||
/// The event has been unwedged and sending is now being retried.
|
||||
RetryEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
},
|
||||
|
||||
/// The event has been sent to the server, and the query returned
|
||||
/// successfully.
|
||||
SentEvent {
|
||||
/// Transaction id used to identify this event.
|
||||
transaction_id: String,
|
||||
/// Received event id from the send response.
|
||||
event_id: String,
|
||||
},
|
||||
|
||||
/// A media upload (consisting of a file and possibly a thumbnail) has made
|
||||
/// progress.
|
||||
MediaUpload {
|
||||
/// The media event this uploaded media relates to.
|
||||
related_to: String,
|
||||
|
||||
/// The final media source for the file if it has finished uploading.
|
||||
file: Option<Arc<MediaSource>>,
|
||||
|
||||
/// The index of the media within the transaction. A file and its
|
||||
/// thumbnail share the same index. Will always be 0 for non-gallery
|
||||
/// media uploads.
|
||||
index: u64,
|
||||
|
||||
/// The combined upload progress across the file and, if existing, its
|
||||
/// thumbnail. For gallery uploads, the progress is reported per indexed
|
||||
/// gallery item.
|
||||
progress: AbstractProgress,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkRoomSendQueueUpdate> for RoomSendQueueUpdate {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: SdkRoomSendQueueUpdate) -> std::result::Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkRoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
|
||||
Self::CancelledLocalEvent { transaction_id: transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::MediaUpload { related_to, file, index, progress } => {
|
||||
Self::MediaUpload {
|
||||
related_to: related_to.into(),
|
||||
file: file.map(|source| source.try_into().map(Arc::new)).transpose()?,
|
||||
index,
|
||||
progress: progress.into(),
|
||||
}
|
||||
}
|
||||
SdkRoomSendQueueUpdate::NewLocalEvent(local_echo) => {
|
||||
Self::NewLocalEvent { transaction_id: local_echo.transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::ReplacedLocalEvent { transaction_id, .. } => {
|
||||
Self::ReplacedLocalEvent { transaction_id: transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::RetryEvent { transaction_id } => {
|
||||
Self::RetryEvent { transaction_id: transaction_id.into() }
|
||||
}
|
||||
SdkRoomSendQueueUpdate::SendError { transaction_id, error, is_recoverable } => {
|
||||
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&*error).into();
|
||||
Self::SendError {
|
||||
transaction_id: transaction_id.into(),
|
||||
error: as_queue_wedge_error.into(),
|
||||
is_recoverable,
|
||||
}
|
||||
}
|
||||
SdkRoomSendQueueUpdate::SentEvent { transaction_id, event_id } => {
|
||||
Self::SentEvent { transaction_id: transaction_id.into(), event_id: event_id.into() }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,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> {
|
||||
@@ -206,6 +210,8 @@ pub struct RoomPowerLevelsValues {
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
/// The level required to change the space's children.
|
||||
pub space_child: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
|
||||
@@ -228,6 +234,7 @@ impl From<RumaPowerLevels> for RoomPowerLevelsValues {
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
space_child: state_event_level_for(&value, &TimelineEventType::SpaceChild),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
|
||||
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_invite,
|
||||
new_filter_joined, new_filter_low_priority, new_filter_non_left, new_filter_none,
|
||||
new_filter_normalized_match_room_name, new_filter_not, new_filter_space, new_filter_unread,
|
||||
BoxedFilterFn, RoomCategory,
|
||||
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_identifiers,
|
||||
new_filter_invite, new_filter_joined, new_filter_low_priority, new_filter_non_left,
|
||||
new_filter_none, new_filter_normalized_match_room_name, new_filter_not, new_filter_space,
|
||||
new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
@@ -168,6 +168,15 @@ impl RoomList {
|
||||
self: Arc<Self>,
|
||||
page_size: u32,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
self.entries_with_dynamic_adapters_with(page_size, false, listener)
|
||||
}
|
||||
|
||||
fn entries_with_dynamic_adapters_with(
|
||||
self: Arc<Self>,
|
||||
page_size: u32,
|
||||
enable_latest_event_sorter: bool,
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
|
||||
let this = self;
|
||||
|
||||
@@ -216,7 +225,10 @@ impl RoomList {
|
||||
// borrowing `this`, which is going to live long enough since it will live as
|
||||
// long as `entries_stream` and `dynamic_entries_controller`.
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
this.inner.entries_with_dynamic_adapters_with(
|
||||
page_size.try_into().unwrap(),
|
||||
enable_latest_event_sorter,
|
||||
);
|
||||
|
||||
// FFI dance to make those values consumable by foreign language, nothing fancy
|
||||
// here, that's the real code for this method.
|
||||
@@ -231,7 +243,12 @@ impl RoomList {
|
||||
listener.on_update(
|
||||
diffs
|
||||
.into_iter()
|
||||
.map(|room| RoomListEntriesUpdate::from(utd_hook.clone(), room))
|
||||
.map(|diff| {
|
||||
RoomListEntriesUpdate::from(
|
||||
utd_hook.clone(),
|
||||
diff.map(|room| room.into_inner()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
@@ -455,7 +472,9 @@ impl RoomListDynamicEntriesController {
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Identifiers { identifiers: Vec<String> },
|
||||
NonSpace,
|
||||
Space,
|
||||
NonLeft,
|
||||
// Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
|
||||
// support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
|
||||
@@ -464,6 +483,7 @@ pub enum RoomListEntriesDynamicFilterKind {
|
||||
Favourite,
|
||||
LowPriority,
|
||||
NonLowPriority,
|
||||
NonFavorite,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
None,
|
||||
@@ -498,13 +518,18 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
|
||||
Kind::Any { filters } => Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
|
||||
)),
|
||||
Kind::NonLeft => Box::new(new_filter_non_left()),
|
||||
Kind::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()),
|
||||
Kind::Joined => Box::new(new_filter_joined()),
|
||||
Kind::Unread => Box::new(new_filter_unread()),
|
||||
Kind::Favourite => Box::new(new_filter_favourite()),
|
||||
Kind::LowPriority => Box::new(new_filter_low_priority()),
|
||||
Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
|
||||
Kind::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()),
|
||||
|
||||
@@ -93,7 +93,6 @@ pub struct RoomMember {
|
||||
pub membership: MembershipState,
|
||||
pub is_name_ambiguous: bool,
|
||||
pub power_level: PowerLevel,
|
||||
pub normalized_power_level: PowerLevel,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
@@ -110,7 +109,6 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
membership: m.membership().clone().try_into()?,
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level().try_into()?,
|
||||
normalized_power_level: m.normalized_power_level().try_into()?,
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
|
||||
@@ -23,7 +23,6 @@ use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVid
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
call::notify::NotifyType as RumaNotifyType,
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
@@ -57,6 +56,7 @@ use ruma::{
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
rtc::notification::NotificationType as RumaNotificationType,
|
||||
secret_storage::{
|
||||
default_key::SecretStorageDefaultKeyEventContent,
|
||||
key::{
|
||||
@@ -487,25 +487,25 @@ impl TryFrom<RumaMessageType> for MessageType {
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum NotifyType {
|
||||
pub enum RtcNotificationType {
|
||||
Ring,
|
||||
Notify,
|
||||
Notification,
|
||||
}
|
||||
|
||||
impl From<RumaNotifyType> for NotifyType {
|
||||
fn from(val: RumaNotifyType) -> Self {
|
||||
impl From<RumaNotificationType> for RtcNotificationType {
|
||||
fn from(val: RumaNotificationType) -> Self {
|
||||
match val {
|
||||
RumaNotifyType::Ring => Self::Ring,
|
||||
_ => Self::Notify,
|
||||
RumaNotificationType::Ring => Self::Ring,
|
||||
_ => Self::Notification,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotifyType> for RumaNotifyType {
|
||||
fn from(value: NotifyType) -> Self {
|
||||
impl From<RtcNotificationType> for RumaNotificationType {
|
||||
fn from(value: RtcNotificationType) -> Self {
|
||||
match value {
|
||||
NotifyType::Ring => RumaNotifyType::Ring,
|
||||
NotifyType::Notify => RumaNotifyType::Notify,
|
||||
RtcNotificationType::Ring => RumaNotificationType::Ring,
|
||||
RtcNotificationType::Notification => RumaNotificationType::Notification,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -736,7 +736,7 @@ impl TryFrom<&AudioInfo> for BaseAudioInfo {
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size) })
|
||||
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size), waveform: None })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@ use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
room_list::SpaceRoomListPaginationState, SpaceRoom as UISpaceRoom,
|
||||
SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
|
||||
SpaceService as UISpaceService,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
@@ -54,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(),
|
||||
@@ -77,22 +79,114 @@ 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::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()
|
||||
}
|
||||
|
||||
/// Returns a `SpaceRoomList` for the given space ID.
|
||||
#[allow(clippy::unused_async)]
|
||||
// This method doesn't need to be async but if its not the FFI layer panics
|
||||
// with "there is no no reactor running, must be called from the context
|
||||
// of a Tokio 1.x runtime" error because the underlying constructor spawns
|
||||
// an async task.
|
||||
pub async fn space_room_list(
|
||||
&self,
|
||||
space_id: String,
|
||||
) -> Result<Arc<SpaceRoomList>, ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id))))
|
||||
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id).await)))
|
||||
}
|
||||
|
||||
/// Returns all known direct-parents of a given space room ID.
|
||||
pub async fn joined_parents_of_child(
|
||||
&self,
|
||||
child_id: String,
|
||||
) -> Result<Vec<SpaceRoom>, ClientError> {
|
||||
let child_id = RoomId::parse(child_id)?;
|
||||
|
||||
let parents = self.inner.joined_parents_of_child(&child_id).await;
|
||||
|
||||
Ok(parents.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
space_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
let child_id = RoomId::parse(child_id)?;
|
||||
|
||||
self.inner.add_child_to_space(child_id, space_id).await.map_err(ClientError::from)
|
||||
}
|
||||
|
||||
pub async fn remove_child_from_space(
|
||||
&self,
|
||||
child_id: String,
|
||||
space_id: String,
|
||||
) -> Result<(), ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
let child_id = RoomId::parse(child_id)?;
|
||||
|
||||
self.inner.remove_child_from_space(child_id, space_id).await.map_err(ClientError::from)
|
||||
}
|
||||
|
||||
/// Start a space leave process returning a [`LeaveSpaceHandle`] from which
|
||||
/// rooms can be retrieved in reversed BFS order starting from the requested
|
||||
/// `space_id` graph node. If the room is unknown then an error will be
|
||||
/// returned.
|
||||
///
|
||||
/// Once the rooms to be left are chosen the handle can be used to leave
|
||||
/// them.
|
||||
pub async fn leave_space(
|
||||
&self,
|
||||
space_id: String,
|
||||
) -> Result<Arc<LeaveSpaceHandle>, ClientError> {
|
||||
let space_id = RoomId::parse(space_id)?;
|
||||
|
||||
let handle = self.inner.leave_space(&space_id).await.map_err(ClientError::from)?;
|
||||
|
||||
Ok(Arc::new(handle.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The `SpaceRoomList`represents a paginated list of direct rooms
|
||||
/// 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
|
||||
@@ -115,6 +209,27 @@ impl SpaceRoomList {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SpaceRoomList {
|
||||
/// Returns the space of the room list if known.
|
||||
pub fn space(&self) -> Option<SpaceRoom> {
|
||||
self.inner.space().map(Into::into)
|
||||
}
|
||||
|
||||
/// Subscribe to space updates.
|
||||
pub fn subscribe_to_space_updates(
|
||||
&self,
|
||||
listener: Box<dyn SpaceRoomListSpaceListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let space_updates = self.inner.subscribe_to_space_updates();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(space_updates);
|
||||
|
||||
while let Some(space) = space_updates.next().await {
|
||||
listener.on_update(space.map(Into::into));
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Returns if the room list is currently paginating or not.
|
||||
pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
|
||||
self.inner.pagination_state()
|
||||
@@ -164,6 +279,23 @@ 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)]
|
||||
pub trait SpaceRoomListSpaceListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
fn on_update(&self, space: Option<SpaceRoom>);
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -181,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)]
|
||||
@@ -189,8 +326,11 @@ pub struct SpaceRoom {
|
||||
pub room_id: String,
|
||||
/// The canonical alias of the room, if any.
|
||||
pub canonical_alias: Option<String>,
|
||||
/// The name of the room, if any.
|
||||
pub name: Option<String>,
|
||||
/// The room's name from the room state event if received from sync, or one
|
||||
/// that's been computed otherwise.
|
||||
pub display_name: String,
|
||||
/// Room name as defined by the room state event only.
|
||||
pub raw_name: Option<String>,
|
||||
/// The topic of the room, if any.
|
||||
pub topic: Option<String>,
|
||||
/// The URL for the room's avatar, if one is set.
|
||||
@@ -206,12 +346,19 @@ pub struct SpaceRoom {
|
||||
/// Whether guest users may join the room and participate in it.
|
||||
pub guest_can_join: bool,
|
||||
|
||||
/// Whether this room is a direct room.
|
||||
///
|
||||
/// Only set if the room is known to the client otherwise we
|
||||
/// assume DMs shouldn't be exposed publicly in spaces.
|
||||
pub is_direct: Option<bool>,
|
||||
/// The number of children room this has, if a space.
|
||||
pub children_count: u64,
|
||||
/// Whether this room is joined, left etc.
|
||||
pub state: Option<Membership>,
|
||||
/// A list of room members considered to be heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
/// The via parameters of the room.
|
||||
pub via: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<UISpaceRoom> for SpaceRoom {
|
||||
@@ -219,7 +366,8 @@ impl From<UISpaceRoom> for SpaceRoom {
|
||||
Self {
|
||||
room_id: room.room_id.into(),
|
||||
canonical_alias: room.canonical_alias.map(|alias| alias.into()),
|
||||
name: room.name,
|
||||
display_name: room.display_name,
|
||||
raw_name: room.name,
|
||||
topic: room.topic,
|
||||
avatar_url: room.avatar_url.map(|url| url.into()),
|
||||
room_type: room.room_type.into(),
|
||||
@@ -227,9 +375,11 @@ impl From<UISpaceRoom> for SpaceRoom {
|
||||
join_rule: room.join_rule.map(Into::into),
|
||||
world_readable: room.world_readable,
|
||||
guest_can_join: room.guest_can_join,
|
||||
is_direct: room.is_direct,
|
||||
children_count: room.children_count,
|
||||
state: room.state.map(Into::into),
|
||||
heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
|
||||
via: room.via.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,3 +424,127 @@ 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.
|
||||
///
|
||||
/// Once the upstream client decides what rooms should actually be left, the
|
||||
/// handle provides a method to execute that too.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LeaveSpaceHandle {
|
||||
inner: UILeaveSpaceHandle,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LeaveSpaceHandle {
|
||||
/// A list of rooms to be left which next to normal [`SpaceRoom`] data also
|
||||
/// include leave specific information.
|
||||
pub fn rooms(&self) -> Vec<LeaveSpaceRoom> {
|
||||
let rooms = self.inner.rooms();
|
||||
rooms.iter().map(|room| room.clone().into()).collect()
|
||||
}
|
||||
|
||||
/// Bulk leave the given rooms. Stops when encountering an error.
|
||||
pub async fn leave(&self, room_ids: Vec<String>) -> Result<(), ClientError> {
|
||||
let room_ids = room_ids.iter().map(RoomId::parse).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.inner
|
||||
.leave(|room| room_ids.contains(&room.space_room.room_id))
|
||||
.await
|
||||
.map_err(ClientError::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UILeaveSpaceHandle> for LeaveSpaceHandle {
|
||||
fn from(handle: UILeaveSpaceHandle) -> Self {
|
||||
LeaveSpaceHandle { inner: handle }
|
||||
}
|
||||
}
|
||||
|
||||
/// Space leaving specific room that groups normal [`SpaceRoom`] details with
|
||||
/// information about the leaving user's role.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LeaveSpaceRoom {
|
||||
/// The underlying [`SpaceRoom`]
|
||||
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.
|
||||
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_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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#[cfg(feature = "sqlite")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use matrix_sdk::SqliteStoreConfig;
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::client_builder::ClientBuilder;
|
||||
|
||||
/// The outcome of building a [`StoreBuilder`], with data that can be passed
|
||||
/// directly to a [`ClientBuilder`].
|
||||
pub enum StoreBuilderOutcome {
|
||||
/// An SQLite store configuration successfully built.
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite { config: SqliteStoreConfig, cache_path: PathBuf, store_path: PathBuf },
|
||||
|
||||
/// An IndexedDB store configuration successfully built.
|
||||
#[cfg(feature = "indexeddb")]
|
||||
IndexedDb { name: String, passphrase: Option<String> },
|
||||
|
||||
/// An in-memory store configuration successfully built.
|
||||
InMemory,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
mod sqlite {
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
|
||||
use matrix_sdk::SqliteStoreConfig;
|
||||
use tracing::debug;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::StoreBuilderOutcome;
|
||||
use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc};
|
||||
|
||||
/// The store paths the client will use when built.
|
||||
#[derive(Clone)]
|
||||
struct StorePaths {
|
||||
/// The path that the client will use to store its data.
|
||||
data_path: String,
|
||||
|
||||
/// The path that the client will use to store its caches. This path can
|
||||
/// be the same as the data path if you prefer to keep
|
||||
/// everything in one place.
|
||||
cache_path: String,
|
||||
}
|
||||
|
||||
/// A builder for configuring a Sqlite session store.
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct SqliteStoreBuilder {
|
||||
paths: StorePaths,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
pool_max_size: Option<usize>,
|
||||
cache_size: Option<u32>,
|
||||
journal_size_limit: Option<u32>,
|
||||
system_is_memory_constrained: bool,
|
||||
}
|
||||
|
||||
impl SqliteStoreBuilder {
|
||||
pub(crate) fn raw_new(data_path: String, cache_path: String) -> Self {
|
||||
Self {
|
||||
paths: StorePaths { data_path, cache_path },
|
||||
passphrase: Zeroizing::new(None),
|
||||
pool_max_size: None,
|
||||
cache_size: None,
|
||||
journal_size_limit: None,
|
||||
system_is_memory_constrained: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SqliteStoreBuilder {
|
||||
/// Construct a [`SqliteStoreBuilder`] and set the paths that the client
|
||||
/// will use to store its data and caches.
|
||||
///
|
||||
/// Both paths **must** be unique per session as the SDK stores aren't
|
||||
/// capable of handling multiple users, however it is valid to use the
|
||||
/// same path for both stores on a single session.
|
||||
#[uniffi::constructor]
|
||||
pub fn new(data_path: String, cache_path: String) -> Arc<Self> {
|
||||
Arc::new(Self::raw_new(data_path, cache_path))
|
||||
}
|
||||
|
||||
/// Set the passphrase for the stores.
|
||||
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.passphrase = Zeroizing::new(passphrase);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the pool max size for the stores.
|
||||
///
|
||||
/// Each store exposes an async pool of connections. This method
|
||||
/// controls the size of the pool. The larger the pool is, the more
|
||||
/// memory is consumed, but also the more the app is reactive because it
|
||||
/// doesn't need to wait on a pool to be available to run queries.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
|
||||
pub fn pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.pool_max_size = pool_max_size.map(|size| {
|
||||
size.try_into().expect("`pool_max_size` is too large to fit in `usize`")
|
||||
});
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the cache size for the stores.
|
||||
///
|
||||
/// Each store exposes a SQLite connection. This method controls the
|
||||
/// cache size, in **bytes (!)**.
|
||||
///
|
||||
/// The cache represents data SQLite holds in memory at once per open
|
||||
/// database file. The default cache implementation does not allocate
|
||||
/// the full amount of cache memory all at once. Cache memory is
|
||||
/// allocated in smaller chunks on an as-needed basis.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::cache_size`] to learn more.
|
||||
pub fn cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.cache_size = cache_size;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set the size limit for the SQLite WAL files of stores.
|
||||
///
|
||||
/// Each store uses the WAL journal mode. This method controls the size
|
||||
/// limit of the WAL files, in **bytes (!)**.
|
||||
///
|
||||
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
|
||||
pub fn journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.journal_size_limit = limit;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Tell the client that the system is memory constrained, like in a
|
||||
/// push notification process for example.
|
||||
///
|
||||
/// So far, at the time of writing (2025-04-07), it changes
|
||||
/// the defaults of [`SqliteStoreConfig`]. Please check
|
||||
/// [`SqliteStoreConfig::with_low_memory_config`].
|
||||
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.system_is_memory_constrained = true;
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl SqliteStoreBuilder {
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
|
||||
let data_path = Path::new(&self.paths.data_path);
|
||||
let cache_path = Path::new(&self.paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
cache_path = %cache_path.to_string_lossy(),
|
||||
"Creating directories for data and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(data_path)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
|
||||
let mut sqlite_store_config = if self.system_is_memory_constrained {
|
||||
SqliteStoreConfig::with_low_memory_config(data_path)
|
||||
} else {
|
||||
SqliteStoreConfig::new(data_path)
|
||||
};
|
||||
|
||||
sqlite_store_config = sqlite_store_config.passphrase(self.passphrase.as_deref());
|
||||
|
||||
if let Some(size) = self.pool_max_size {
|
||||
sqlite_store_config = sqlite_store_config.pool_max_size(size);
|
||||
}
|
||||
|
||||
if let Some(size) = self.cache_size {
|
||||
sqlite_store_config = sqlite_store_config.cache_size(size);
|
||||
}
|
||||
|
||||
if let Some(limit) = self.journal_size_limit {
|
||||
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
|
||||
}
|
||||
|
||||
Ok(StoreBuilderOutcome::Sqlite {
|
||||
config: sqlite_store_config,
|
||||
store_path: data_path.to_owned(),
|
||||
cache_path: cache_path.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
mod indexeddb {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::StoreBuilderOutcome;
|
||||
use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct IndexedDbStoreBuilder {
|
||||
name: String,
|
||||
passphrase: Option<String>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl IndexedDbStoreBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new(name: String) -> Arc<Self> {
|
||||
Arc::new(Self { name, passphrase: None })
|
||||
}
|
||||
|
||||
/// Set the passphrase for the stores.
|
||||
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.passphrase = passphrase;
|
||||
Arc::new(builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexedDbStoreBuilder {
|
||||
pub fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
|
||||
Ok(StoreBuilderOutcome::IndexedDb {
|
||||
name: self.name.clone(),
|
||||
passphrase: self.passphrase.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
pub use indexeddb::*;
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub use sqlite::*;
|
||||
|
||||
use crate::client_builder::ClientBuildError;
|
||||
|
||||
/// Represent the kind of store the client will configure.
|
||||
#[derive(Clone)]
|
||||
pub enum StoreBuilder {
|
||||
/// Represents the builder for the SQLite store.
|
||||
#[cfg(feature = "sqlite")]
|
||||
Sqlite(SqliteStoreBuilder),
|
||||
|
||||
/// Represents the builder for the IndexedDB store.
|
||||
#[cfg(feature = "indexeddb")]
|
||||
IndexedDb(IndexedDbStoreBuilder),
|
||||
|
||||
/// Represents the builder for in-memory store.
|
||||
InMemory,
|
||||
}
|
||||
|
||||
impl StoreBuilder {
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
|
||||
match self {
|
||||
#[cfg(feature = "sqlite")]
|
||||
Self::Sqlite(config) => config.build(),
|
||||
|
||||
#[cfg(feature = "indexeddb")]
|
||||
Self::IndexedDb(config) => config.build(),
|
||||
|
||||
Self::InMemory => Ok(StoreBuilderOutcome::InMemory),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
MatrixSyncServiceState::Idle => Self::Idle,
|
||||
MatrixSyncServiceState::Running => Self::Running,
|
||||
MatrixSyncServiceState::Terminated => Self::Terminated,
|
||||
MatrixSyncServiceState::Error => Self::Error,
|
||||
MatrixSyncServiceState::Error(_error) => Self::Error,
|
||||
MatrixSyncServiceState::Offline => Self::Offline,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
|
||||
use matrix_sdk_ui::timeline::{
|
||||
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
|
||||
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
|
||||
};
|
||||
use ruma::{
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
EventId,
|
||||
@@ -12,31 +15,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)
|
||||
}
|
||||
@@ -61,6 +83,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 {
|
||||
@@ -73,17 +120,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 {
|
||||
@@ -94,18 +138,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| {
|
||||
@@ -117,9 +157,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,8 +189,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.
|
||||
@@ -173,11 +211,11 @@ pub struct TimelineConfiguration {
|
||||
pub date_divider_mode: DateDividerMode,
|
||||
|
||||
/// Should the read receipts and read markers be tracked for the timeline
|
||||
/// items in this instance?
|
||||
/// items in this instance and on which event types?
|
||||
///
|
||||
/// As this has a non negligible performance impact, make sure to enable it
|
||||
/// only when you need it.
|
||||
pub track_read_receipts: bool,
|
||||
pub track_read_receipts: TimelineReadReceiptTracking,
|
||||
|
||||
/// Whether this timeline instance should report UTDs through the client's
|
||||
/// delegate.
|
||||
|
||||
@@ -16,9 +16,14 @@ use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
|
||||
use ruma::events::FullStateEventContent;
|
||||
use ruma::events::{
|
||||
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, FullStateEventContent,
|
||||
};
|
||||
|
||||
use crate::{timeline::msg_like::MsgLikeContent, utils::Timestamp};
|
||||
use crate::{
|
||||
client::JoinRule, event::TimelineEventType, timeline::msg_like::MsgLikeContent,
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
@@ -35,7 +40,7 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::CallNotify => TimelineItemContent::CallNotify,
|
||||
Content::RtcNotification => TimelineItemContent::RtcNotification,
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
@@ -95,6 +100,51 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum HistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member' state changes to
|
||||
/// something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they joined the room onwards.
|
||||
/// Events stop being accessible when the member' state changes to
|
||||
/// something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when the member
|
||||
/// was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be shared by
|
||||
/// any participating homeserver with anyone, regardless of whether they
|
||||
/// have ever joined the room.
|
||||
WorldReadable,
|
||||
|
||||
/// A custom history visibility, up for interpretation by the consumer.
|
||||
Custom {
|
||||
/// The string representation for this custom history visibility.
|
||||
repr: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: RumaHistoryVisibility) -> Self {
|
||||
match value {
|
||||
RumaHistoryVisibility::Invited => Self::Invited,
|
||||
RumaHistoryVisibility::Joined => Self::Joined,
|
||||
RumaHistoryVisibility::Shared => Self::Shared,
|
||||
RumaHistoryVisibility::WorldReadable => Self::WorldReadable,
|
||||
_ => Self::Custom { repr: value.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
// A note about this `allow(clippy::large_enum_variant)`.
|
||||
// In order to reduce the size of `TimelineItemContent`, we would need to
|
||||
@@ -109,7 +159,7 @@ pub enum TimelineItemContent {
|
||||
content: MsgLikeContent,
|
||||
},
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
RtcNotification,
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
@@ -195,29 +245,69 @@ 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,
|
||||
RoomCreate {
|
||||
federate: Option<bool>,
|
||||
},
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
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 {
|
||||
@@ -240,11 +330,39 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Self::RoomAvatar { url }
|
||||
}
|
||||
Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
|
||||
Content::RoomCreate(_) => Self::RoomCreate,
|
||||
Content::RoomCreate(c) => {
|
||||
let federate = match c {
|
||||
FullContent::Original { content, .. } => Some(content.federate),
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomCreate { federate }
|
||||
}
|
||||
Content::RoomEncryption(_) => Self::RoomEncryption,
|
||||
Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
|
||||
Content::RoomHistoryVisibility(_) => Self::RoomHistoryVisibility,
|
||||
Content::RoomJoinRules(_) => Self::RoomJoinRules,
|
||||
Content::RoomHistoryVisibility(c) => {
|
||||
let history_visibility = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
Some(content.history_visibility.clone().into())
|
||||
}
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomHistoryVisibility { history_visibility }
|
||||
}
|
||||
Content::RoomJoinRules(c) => {
|
||||
let join_rule = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
match content.join_rule.clone().try_into() {
|
||||
Ok(jr) => Some(jr),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to convert join rule: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
FullContent::Redacted(_) => None,
|
||||
};
|
||||
Self::RoomJoinRules { join_rule }
|
||||
}
|
||||
Content::RoomName(c) => {
|
||||
let name = match c {
|
||||
FullContent::Original { content, .. } => Some(content.name.clone()),
|
||||
@@ -255,17 +373,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,7 +21,6 @@ use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
event_cache::RoomPaginationStatus,
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
};
|
||||
@@ -31,12 +30,15 @@ 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};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
poll::{
|
||||
@@ -49,6 +51,7 @@ use ruma::{
|
||||
},
|
||||
room::message::{
|
||||
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
|
||||
TextMessageEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
@@ -111,16 +114,16 @@ impl Timeline {
|
||||
.transpose()
|
||||
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
let caption = params.caption.map(|caption| {
|
||||
let formatted =
|
||||
formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
|
||||
assign!(TextMessageEventContent::plain(caption), { formatted })
|
||||
});
|
||||
|
||||
let attachment_config = AttachmentConfig {
|
||||
info: Some(attachment_info),
|
||||
thumbnail,
|
||||
caption: params.caption,
|
||||
formatted_caption,
|
||||
caption,
|
||||
mentions: params.mentions.map(Into::into),
|
||||
in_reply_to: in_reply_to_event_id,
|
||||
..Default::default()
|
||||
@@ -352,17 +355,31 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark the room as read by trying to attach an *unthreaded* read receipt
|
||||
/// to the latest room event.
|
||||
/// Mark the timeline as read by attempting to send a read receipt on the
|
||||
/// latest visible event.
|
||||
///
|
||||
/// This works even if the latest event belongs to a thread, as a threaded
|
||||
/// reply also belongs to the unthreaded timeline. No threaded receipt
|
||||
/// will be sent here (see also #3123).
|
||||
/// The latest visible event is determined from the timeline's focus kind
|
||||
/// and whether or not it hides threaded events. If no latest event can
|
||||
/// be determined and the timeline is live, the room's unread marker is
|
||||
/// unset instead.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `receipt_type` - The type of receipt to send. When using
|
||||
/// [`ReceiptType::FullyRead`], an unthreaded receipt will be sent. This
|
||||
/// works even if the latest event belongs to a thread, as a threaded
|
||||
/// reply also belongs to the unthreaded timeline. Otherwise the receipt
|
||||
/// thread will be determined based on the timeline's focus kind.
|
||||
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
|
||||
self.inner.mark_as_read(receipt_type.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the latest [`EventId`] in the timeline.
|
||||
pub async fn latest_event_id(&self) -> Option<String> {
|
||||
self.inner.latest_event_id().await.as_deref().map(ToString::to_string)
|
||||
}
|
||||
|
||||
/// Queues an event in the room's send queue so it's processed for
|
||||
/// sending later.
|
||||
///
|
||||
@@ -422,14 +439,12 @@ impl Timeline {
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
waveform: Vec<f32>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Voice {
|
||||
audio_info: BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
waveform: Some(waveform),
|
||||
};
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
|
||||
let mut info =
|
||||
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
info.waveform = Some(waveform);
|
||||
self.send_attachment(params, AttachmentInfo::Voice(info), audio_info.mimetype, None)
|
||||
}
|
||||
|
||||
pub fn send_file(
|
||||
@@ -605,13 +620,14 @@ impl Timeline {
|
||||
///
|
||||
/// Ensures that only one reaction is sent at a time to avoid race
|
||||
/// conditions and spamming the homeserver with requests.
|
||||
///
|
||||
/// Returns `true` if the reaction was added, `false` if it was removed.
|
||||
pub async fn toggle_reaction(
|
||||
&self,
|
||||
item_id: EventOrTransactionId,
|
||||
key: String,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.toggle_reaction(&item_id.try_into()?, &key).await?;
|
||||
Ok(())
|
||||
) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.toggle_reaction(&item_id.try_into()?, &key).await?)
|
||||
}
|
||||
|
||||
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
|
||||
@@ -707,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`
|
||||
@@ -717,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(
|
||||
@@ -964,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,
|
||||
}
|
||||
@@ -977,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,
|
||||
}
|
||||
}
|
||||
@@ -995,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,
|
||||
@@ -1018,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(),
|
||||
@@ -1073,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,
|
||||
@@ -1261,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.
|
||||
@@ -1286,7 +1317,7 @@ impl LazyTimelineItemProvider {
|
||||
}
|
||||
|
||||
/// Mimic the [`UiLatestEventValue`] type.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum LatestEventValue {
|
||||
None,
|
||||
Remote {
|
||||
@@ -1296,10 +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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1316,8 +1354,21 @@ impl From<UiLatestEventValue> for LatestEventValue {
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
UiLatestEventValue::Local { timestamp, content, is_sending } => {
|
||||
Self::Local { timestamp: timestamp.into(), content: content.into(), is_sending }
|
||||
UiLatestEventValue::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(),
|
||||
state,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1336,7 +1387,7 @@ mod galleries {
|
||||
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use ruma::EventId;
|
||||
use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
@@ -1475,15 +1526,18 @@ mod galleries {
|
||||
let mime_str = self.mimetype().as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
let caption = self.caption().as_ref().map(|caption| {
|
||||
let formatted = formatted_body_from(
|
||||
Some(caption),
|
||||
self.formatted_caption().clone().map(Into::into),
|
||||
);
|
||||
assign!(TextMessageEventContent::plain(caption), { formatted })
|
||||
});
|
||||
Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
|
||||
source: self.source().clone().into(),
|
||||
content_type: mime_type,
|
||||
attachment_info: self.attachment_info()?,
|
||||
caption: self.caption().clone(),
|
||||
formatted_caption: self
|
||||
.formatted_caption()
|
||||
.clone()
|
||||
.map(ruma::events::room::message::FormattedBody::from),
|
||||
caption,
|
||||
thumbnail: self.thumbnail()?,
|
||||
})
|
||||
}
|
||||
@@ -1542,10 +1596,11 @@ mod galleries {
|
||||
params: GalleryUploadParameters,
|
||||
item_infos: Vec<GalleryItemInfo>,
|
||||
) -> Result<Arc<SendGalleryJoinHandle>, RoomError> {
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
let caption = params.caption.map(|caption| {
|
||||
let formatted =
|
||||
formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
|
||||
assign!(TextMessageEventContent::plain(caption), { formatted })
|
||||
});
|
||||
|
||||
let in_reply_to = params
|
||||
.in_reply_to
|
||||
@@ -1555,8 +1610,7 @@ mod galleries {
|
||||
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
|
||||
let mut gallery_config = GalleryConfig::new()
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.caption(caption)
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.in_reply_to(in_reply_to);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
|
||||
use super::{
|
||||
@@ -23,6 +23,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
event::MessageLikeEventType,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
timeline::content::ReactionSenderData,
|
||||
utils::Timestamp,
|
||||
@@ -50,6 +51,9 @@ pub enum MsgLikeKind {
|
||||
|
||||
/// An `m.room.encrypted` event that could not be decrypted.
|
||||
UnableToDecrypt { msg: EncryptedMessage },
|
||||
|
||||
/// A custom message like event.
|
||||
Other { event_type: MessageLikeEventType },
|
||||
}
|
||||
|
||||
/// A special kind of [`super::TimelineItemContent`] that groups together
|
||||
@@ -182,6 +186,15 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
Kind::Other(other) => Self {
|
||||
kind: MsgLikeKind::Other {
|
||||
event_type: MessageLikeEventType::Other(other.event_type().to_string()),
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
thread_summary,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -242,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]
|
||||
@@ -260,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,10 +1,12 @@
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::borrow::ToOwned;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use tracing::{callsite::DefaultCallsite, field::FieldSet, Callsite};
|
||||
use tracing::{callsite::DefaultCallsite, debug, error, field::FieldSet, Callsite};
|
||||
use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
|
||||
/// Log an event.
|
||||
@@ -96,6 +98,8 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Span(tracing::Span);
|
||||
|
||||
pub(crate) const BRIDGE_SPAN_NAME: &str = "<sdk_bridge_span>";
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Span {
|
||||
/// Create a span originating at the given callsite (file, line and column).
|
||||
@@ -129,18 +133,41 @@ impl Span {
|
||||
level: LogLevel,
|
||||
target: String,
|
||||
name: String,
|
||||
bridge_trace_id: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
|
||||
Mutex::new(BTreeMap::new());
|
||||
|
||||
let loc = MetadataId { file, line, level, target, name: Some(name) };
|
||||
let callsite = get_or_init_metadata(&CALLSITES, loc, &[], MetadataKind::SPAN);
|
||||
|
||||
// If sentry isn't enabled, ignore bridge_trace_id's contents
|
||||
let bridge_trace_id = if cfg!(feature = "sentry") { bridge_trace_id } else { None };
|
||||
|
||||
let callsite = if cfg!(feature = "sentry") {
|
||||
get_or_init_metadata(&CALLSITES, loc, &["sentry", "sentry.trace"], MetadataKind::SPAN)
|
||||
} else {
|
||||
get_or_init_metadata(&CALLSITES, loc, &[], MetadataKind::SPAN)
|
||||
};
|
||||
|
||||
let metadata = callsite.metadata();
|
||||
|
||||
let span = if span_or_event_enabled(callsite) {
|
||||
// This function is hidden from docs, but we have to use it (see above).
|
||||
let values = metadata.fields().value_set(&[]);
|
||||
tracing::Span::new(metadata, &values)
|
||||
let fields = metadata.fields();
|
||||
|
||||
if let Some(parent_trace_id) = bridge_trace_id {
|
||||
debug!("Adding fields | sentry:true, sentry.trace={parent_trace_id}");
|
||||
let sentry_field = fields.field("sentry").unwrap();
|
||||
let sentry_trace_field = fields.field("sentry.trace").unwrap();
|
||||
#[allow(trivial_casts)] // The compiler is lying, it can't infer this cast
|
||||
let values = [
|
||||
(&sentry_field, Some(&true as &dyn tracing::Value)),
|
||||
(&sentry_trace_field, Some(&parent_trace_id as &dyn tracing::Value)),
|
||||
];
|
||||
tracing::Span::new(metadata, &fields.value_set(&values))
|
||||
} else {
|
||||
tracing::Span::new(metadata, &fields.value_set(&[]))
|
||||
}
|
||||
} else {
|
||||
tracing::Span::none()
|
||||
};
|
||||
@@ -164,6 +191,27 @@ impl Span {
|
||||
fn is_none(&self) -> bool {
|
||||
self.0.is_none()
|
||||
}
|
||||
|
||||
/// Creates a [`Span`] that acts as a bridge between the client spans and
|
||||
/// the SDK ones, allowing them to be joined in Sentry. This function
|
||||
/// will only return a valid span if the `sentry` feature is enabled,
|
||||
/// otherwise it will return a noop span.
|
||||
#[uniffi::constructor]
|
||||
pub fn new_bridge_span(target: String, parent_trace_id: Option<String>) -> Arc<Self> {
|
||||
if cfg!(feature = "sentry") {
|
||||
Self::new(
|
||||
"Bridge".to_owned(),
|
||||
None,
|
||||
LogLevel::Info,
|
||||
target,
|
||||
BRIDGE_SPAN_NAME.to_owned(),
|
||||
parent_trace_id,
|
||||
)
|
||||
} else {
|
||||
error!("Sentry is not enabled!");
|
||||
Arc::new(Self(tracing::Span::none()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, uniffi::Enum)]
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo,
|
||||
|
||||
@@ -125,9 +125,10 @@ pub async fn generate_webview_url(
|
||||
/// call widget.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn new_virtual_element_call_widget(
|
||||
props: matrix_sdk::widget::VirtualElementCallWidgetOptions,
|
||||
props: matrix_sdk::widget::VirtualElementCallWidgetProperties,
|
||||
config: matrix_sdk::widget::VirtualElementCallWidgetConfig,
|
||||
) -> Result<WidgetSettings, ParseError> {
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props)
|
||||
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props, config)
|
||||
.map(|w| w.into())?)
|
||||
}
|
||||
|
||||
@@ -175,6 +176,10 @@ pub fn get_element_call_required_permissions(
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::RoomRedaction.to_string(),
|
||||
},
|
||||
// This allows declining an incoming call and detect if someone declines a call.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::RtcDecline.to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
WidgetCapabilities {
|
||||
@@ -199,10 +204,12 @@ pub fn get_element_call_required_permissions(
|
||||
send: vec![
|
||||
// To notify other users that a call has started.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
|
||||
event_type: MessageLikeEventType::RtcNotification.to_string(),
|
||||
},
|
||||
// Also for call notifications, except this is the deprecated fallback type which
|
||||
// Element Call still sends.
|
||||
// Deprecated for now, kept for backward compatibility as widgets will send both
|
||||
// CallNotify and RtcNotification.
|
||||
WidgetEventFilter::MessageLikeWithType {
|
||||
event_type: MessageLikeEventType::CallNotify.to_string(),
|
||||
},
|
||||
@@ -528,5 +535,9 @@ mod tests {
|
||||
);
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
|
||||
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
|
||||
|
||||
// RTC decline
|
||||
cap_assert("org.matrix.msc2762.receive.event:org.matrix.msc4310.rtc.decline");
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.msc4310.rtc.decline");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[bindings.kotlin]
|
||||
package_name = "org.matrix.rustcomponents.sdk"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
android_cleaner = true
|
||||
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,6 +6,82 @@ 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**] 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)) (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
|
||||
|
||||
- [**breaking**] `ServerInfo` has been renamed to `SupportedVersionsResponse`,
|
||||
and its `well_known` field has been removed. It is also wrapped in a
|
||||
`TtlStoreValue` that handles the expiration of the data, rather than calling
|
||||
`maybe_decode()`. Its constructor has been removed since all its fields are
|
||||
now public.
|
||||
([#5910](https://github.com/matrix-org/matrix-rust-sdk/pull/5910))
|
||||
- `StateStoreData(Key/Value)::ServerInfo` has been split into the
|
||||
`SupportedVersions` and `WellKnown` variants.
|
||||
- [**breaking**] Upgrade Ruma to version 0.14.0.
|
||||
([#5882](https://github.com/matrix-org/matrix-rust-sdk/pull/5882))
|
||||
- `Client::sync_lock` has been renamed `Client::state_store_lock`.
|
||||
([#5707](https://github.com/matrix-org/matrix-rust-sdk/pull/5707))
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] The `EventCacheStore::get_room_events()` method has received
|
||||
two new arguments. This allows users to load only events of a certain event
|
||||
type and events that were encrypted using a certain room key identified by its
|
||||
session ID.
|
||||
([#5817](https://github.com/matrix-org/matrix-rust-sdk/pull/5817))
|
||||
- `ComposerDraft` can now store attachments alongside text messages.
|
||||
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
|
||||
|
||||
## [0.14.1] - 2025-09-10
|
||||
|
||||
### Security Fixes
|
||||
|
||||
- Fix a panic in the `RoomMember::normalized_power_level` method.
|
||||
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635)) (Low, [CVE-2025-59047](https://www.cve.org/CVERecord?id=CVE-2025-59047), [GHSA-qhj8-q5r6-8q6j](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-qhj8-q5r6-8q6j)).
|
||||
|
||||
## [0.14.0] - 2025-09-04
|
||||
|
||||
### Features
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version.workspace = true
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -36,7 +36,7 @@ experimental-send-custom-to-device = [
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
|
||||
experimental-encrypted-state-events = [
|
||||
"e2e-encryption",
|
||||
"ruma/unstable-msc3414",
|
||||
"ruma/unstable-msc4362",
|
||||
"matrix-sdk-crypto?/experimental-encrypted-state-events"
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
@@ -65,13 +62,15 @@ testing = [
|
||||
# Add support for inline media galleries via msgtypes
|
||||
unstable-msc4274 = []
|
||||
|
||||
experimental-element-recent-emojis = []
|
||||
|
||||
[dependencies]
|
||||
as_variant.workspace = true
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait.workspace = true
|
||||
bitflags = { workspace = true, features = ["serde"] }
|
||||
decancer = "3.3.3"
|
||||
decancer = { version = "3.3.3", default-features = false }
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
@@ -82,9 +81,8 @@ matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption.workspace = true
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell.workspace = true
|
||||
regex = "1.11.2"
|
||||
regex.workspace = true
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc4186",
|
||||
@@ -107,6 +105,7 @@ futures-executor.workspace = true
|
||||
http.workspace = true
|
||||
matrix-sdk-test.workspace = true
|
||||
matrix-sdk-test-utils.workspace = true
|
||||
proptest.workspace = true
|
||||
similar-asserts.workspace = true
|
||||
stream_assert.workspace = true
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ use crate::{
|
||||
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
|
||||
deserialized_responses::DisplayName,
|
||||
error::{Error, Result},
|
||||
event_cache::store::EventCacheStoreLock,
|
||||
event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
|
||||
media::store::MediaStoreLock,
|
||||
response_processors::{self as processors, Context},
|
||||
room::{
|
||||
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
|
||||
@@ -92,6 +93,9 @@ pub struct BaseClient {
|
||||
/// The store used by the event cache.
|
||||
event_cache_store: EventCacheStoreLock,
|
||||
|
||||
/// The store used by the media cache.
|
||||
media_store: MediaStoreLock,
|
||||
|
||||
/// The store used for encryption.
|
||||
///
|
||||
/// This field is only meant to be used for `OlmMachine` initialization.
|
||||
@@ -108,10 +112,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")]
|
||||
@@ -175,27 +175,15 @@ 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,
|
||||
media_store: config.media_store,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
crypto_store: config.crypto_store,
|
||||
#[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")]
|
||||
@@ -223,6 +211,7 @@ impl BaseClient {
|
||||
let copy = Self {
|
||||
state_store: BaseStateStore::new(config.state_store),
|
||||
event_cache_store: config.event_cache_store,
|
||||
media_store: config.media_store,
|
||||
// We copy the crypto store as well as the `OlmMachine` for two reasons:
|
||||
// 1. The `self.crypto_store` is the same as the one used inside the `OlmMachine`.
|
||||
// 2. We need to ensure that the parent and child use the same data and caches inside
|
||||
@@ -232,16 +221,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)
|
||||
}
|
||||
@@ -290,11 +276,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.
|
||||
@@ -307,6 +289,11 @@ impl BaseClient {
|
||||
&self.event_cache_store
|
||||
}
|
||||
|
||||
/// Get a reference to the media store.
|
||||
pub fn media_store(&self) -> &MediaStoreLock {
|
||||
&self.media_store
|
||||
}
|
||||
|
||||
/// Check whether the client has been activated.
|
||||
///
|
||||
/// See [`BaseClient::activate`] to know what it means.
|
||||
@@ -355,13 +342,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);
|
||||
|
||||
@@ -407,14 +388,10 @@ 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 _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_knocked();
|
||||
@@ -472,16 +449,12 @@ 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.
|
||||
if room.state() != RoomState::Joined {
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
let previous_state = room.state();
|
||||
@@ -525,14 +498,10 @@ 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 _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_as_left();
|
||||
@@ -547,9 +516,12 @@ impl BaseClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get access to the store's sync lock.
|
||||
pub fn sync_lock(&self) -> &Mutex<()> {
|
||||
self.state_store.sync_lock()
|
||||
/// Get a lock to the state store, with an exclusive access.
|
||||
///
|
||||
/// It doesn't give an access to the state store itself. It's rather a lock
|
||||
/// to synchronise all accesses to the state store.
|
||||
pub fn state_store_lock(&self) -> &Mutex<()> {
|
||||
self.state_store.lock()
|
||||
}
|
||||
|
||||
/// Receive a response from a sync call.
|
||||
@@ -591,41 +563,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
|
||||
@@ -673,7 +630,6 @@ impl BaseClient {
|
||||
&mut context,
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
@@ -701,7 +657,6 @@ impl BaseClient {
|
||||
&mut context,
|
||||
processors::room::RoomCreationData::new(
|
||||
&room_id,
|
||||
self.room_info_notable_update_sender.clone(),
|
||||
requested_required_states,
|
||||
&mut ambiguity_cache,
|
||||
),
|
||||
@@ -727,8 +682,8 @@ 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,
|
||||
@@ -744,8 +699,8 @@ 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,
|
||||
@@ -772,7 +727,7 @@ impl BaseClient {
|
||||
context.state_changes.ambiguity_maps = ambiguity_cache.cache;
|
||||
|
||||
{
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
@@ -795,7 +750,11 @@ impl BaseClient {
|
||||
.await;
|
||||
|
||||
// Save the new display name updates if any.
|
||||
processors::changes::save_only(context, &self.state_store).await?;
|
||||
{
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
processors::changes::save_only(context, &self.state_store).await?;
|
||||
}
|
||||
|
||||
for (room_id, member_ids) in updated_members_in_room {
|
||||
if let Some(room) = self.get_room(&room_id) {
|
||||
@@ -916,18 +875,21 @@ impl BaseClient {
|
||||
|
||||
context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
|
||||
|
||||
let _sync_lock = self.sync_lock().lock().await;
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_members_synced();
|
||||
context.state_changes.add_room(room_info);
|
||||
{
|
||||
let _state_store_lock = self.state_store_lock().lock().await;
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&self.state_store,
|
||||
&self.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let mut room_info = room.clone_info();
|
||||
room_info.mark_members_synced();
|
||||
context.state_changes.add_room(room_info);
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&self.state_store,
|
||||
&self.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
|
||||
|
||||
@@ -1041,7 +1003,15 @@ impl BaseClient {
|
||||
self.state_store.forget_room(room_id).await?;
|
||||
|
||||
// Remove the room in the event cache store too.
|
||||
self.event_cache_store().lock().await?.remove_room(room_id).await?;
|
||||
match self.event_cache_store().lock().await? {
|
||||
// If the lock is clear, we can do the operation as expected.
|
||||
// If the lock is dirty, we can ignore to refresh the state, we just need to remove a
|
||||
// room. Also, we must not mark the lock as non-dirty because other operations may be
|
||||
// critical and may need to refresh the `EventCache`' state.
|
||||
EventCacheStoreLockState::Clean(guard) | EventCacheStoreLockState::Dirty(guard) => {
|
||||
guard.remove_room(room_id).await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1091,7 +1061,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.
|
||||
|
||||
@@ -20,7 +20,8 @@ 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,
|
||||
@@ -486,16 +487,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.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use matrix_sdk_common::store_locks::LockStoreError;
|
||||
use matrix_sdk_common::cross_process_lock::CrossProcessLockError;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
|
||||
use thiserror::Error;
|
||||
@@ -51,7 +51,7 @@ pub enum Error {
|
||||
|
||||
/// An error happened while attempting to lock the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheLock(#[from] LockStoreError),
|
||||
EventCacheLock(#[from] CrossProcessLockError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,90 +14,53 @@
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
num::NonZeroUsize,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
cross_process_lock::{
|
||||
CrossProcessLockGeneration,
|
||||
memory_store_helper::{Lease, try_take_leased_lock},
|
||||
},
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update, relational::RelationalLinkedChunk,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
EventCacheStore, EventCacheStoreError, Result, compute_filters_string, extract_event_relation,
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaRequestParameters, UniqueKey as _},
|
||||
};
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result, extract_event_relation};
|
||||
use crate::event_cache::{Event, Gap};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
/// 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>>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
leases: HashMap<String, Lease>,
|
||||
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: SystemTime,
|
||||
}
|
||||
|
||||
/// A media content in the `MemoryStore`.
|
||||
#[derive(Debug)]
|
||||
struct MediaContent {
|
||||
/// The URI of the content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The unique key of the content.
|
||||
key: String,
|
||||
|
||||
/// The bytes of the content.
|
||||
data: Vec<u8>,
|
||||
|
||||
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
// Given that the store is empty, we won't need to clean it up right away.
|
||||
let last_media_cleanup_time = SystemTime::now();
|
||||
let media_service = MediaService::new();
|
||||
media_service.restore(None, Some(last_media_cleanup_time));
|
||||
|
||||
Self {
|
||||
inner: Arc::new(StdRwLock::new(MemoryStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
media_retention_policy: None,
|
||||
last_media_cleanup_time,
|
||||
})),
|
||||
media_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +82,7 @@ impl EventCacheStore for MemoryStore {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
@@ -131,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(())
|
||||
}
|
||||
@@ -238,14 +204,13 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.filter_map(|(event, pos)| {
|
||||
// Must have a relation.
|
||||
let (related_to, rel_type) = extract_event_relation(event.raw())?;
|
||||
let rel_type = RelationType::from(rel_type.as_str());
|
||||
|
||||
// Must relate to the target item.
|
||||
if related_to != event_id {
|
||||
@@ -264,6 +229,28 @@ impl EventCacheStore for MemoryStore {
|
||||
Ok(related_events)
|
||||
}
|
||||
|
||||
async fn get_room_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event: Vec<_> = inner
|
||||
.events
|
||||
.items(room_id)
|
||||
.map(|(event, _pos)| event.clone())
|
||||
.filter(|e| {
|
||||
event_type
|
||||
.is_none_or(|event_type| Some(event_type) == e.kind.event_type().as_deref())
|
||||
})
|
||||
.filter(|e| session_id.is_none_or(|s| Some(s) == e.kind.session_id()))
|
||||
.collect();
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
|
||||
if event.event_id().is_none() {
|
||||
error!(%room_id, "Trying to save an event with no ID");
|
||||
@@ -273,318 +260,26 @@ impl EventCacheStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<()> {
|
||||
self.media_service.add_media_content(self, request, data, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some(media_content) =
|
||||
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
|
||||
{
|
||||
media_content.uri = to.uri().to_owned();
|
||||
media_content.key = to.unique_key();
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
self.media_service.get_media_content(self, request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) =
|
||||
inner.media.iter().position(|media_content| media_content.key == expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
inner.media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.media_service.get_media_content_for_uri(self, uri).await
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let positions = inner
|
||||
.media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
inner.media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_media_retention_policy(self, policy).await
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.media_service.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.media_service.clean_up_media_cache(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStoreMedia for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
|
||||
Ok(self.inner.read().unwrap().media_retention_policy)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().media_retention_policy = Some(policy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
last_access: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push(MediaContent {
|
||||
uri: request.uri().to_owned(),
|
||||
key: request.unique_key(),
|
||||
data,
|
||||
ignore_policy,
|
||||
last_access,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
|
||||
{
|
||||
media_content.ignore_policy = ignore_policy.is_yes();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
expected_uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error> {
|
||||
if !policy.has_limitations() {
|
||||
// We can safely skip all the checks.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First, check media content that exceed the max filesize.
|
||||
if policy.computed_max_file_size().is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
// Then, clean up expired media content.
|
||||
if policy.last_access_expiry.is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy
|
||||
|| !policy.has_content_expired(current_time, content.last_access)
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, if the cache size is too big, remove old items until it fits.
|
||||
if let Some(max_cache_size) = policy.max_cache_size {
|
||||
// Reverse the iterator because in case the cache size is overflowing, we want
|
||||
// to count the number of old items to remove. Items are sorted by last access
|
||||
// and old items are at the start.
|
||||
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
|
||||
(0u64, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
|(mut cache_size, mut items_to_remove), (index, content)| {
|
||||
if content.ignore_policy {
|
||||
// Do not count it.
|
||||
return (cache_size, items_to_remove);
|
||||
}
|
||||
|
||||
let remove_item = if items_to_remove.is_empty() {
|
||||
// We have not reached the max cache size yet.
|
||||
if let Some(sum) = cache_size.checked_add(content.data.len() as u64) {
|
||||
cache_size = sum;
|
||||
// Start removing items if we have exceeded the max cache size.
|
||||
cache_size > max_cache_size
|
||||
} else {
|
||||
// The cache size is overflowing, remove the remaining items, since the
|
||||
// max cache size cannot be bigger than
|
||||
// usize::MAX.
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// We have reached the max cache size already, just remove it.
|
||||
true
|
||||
};
|
||||
|
||||
if remove_item {
|
||||
items_to_remove.push(index);
|
||||
}
|
||||
|
||||
(cache_size, items_to_remove)
|
||||
},
|
||||
);
|
||||
|
||||
// The indexes are already in reverse order so we can just iterate in that order
|
||||
// to remove them starting by the end.
|
||||
for index in items_to_remove {
|
||||
inner.media.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
inner.last_media_cleanup_time = current_time;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(unused_imports)] // There seems to be a false positive when importing the test macros.
|
||||
mod tests {
|
||||
use super::{MemoryStore, Result};
|
||||
use crate::event_cache_store_media_integration_tests;
|
||||
use crate::{event_cache_store_integration_tests, event_cache_store_integration_tests_time};
|
||||
|
||||
async fn get_event_cache_store() -> Result<MemoryStore> {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
event_cache_store_integration_tests_time!();
|
||||
event_cache_store_media_integration_tests!(with_media_size_tests);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! The event cache stores holds events and downloaded media when the cache was
|
||||
//! The event cache stores holds events when the cache was
|
||||
//! activated to save bandwidth at the cost of increased storage space usage.
|
||||
//!
|
||||
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
|
||||
@@ -24,19 +24,15 @@ use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
pub mod media;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
|
||||
use matrix_sdk_common::store_locks::{
|
||||
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
|
||||
use matrix_sdk_common::cross_process_lock::{
|
||||
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
|
||||
MappedCrossProcessLockState, TryLock,
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
use ruma::{
|
||||
OwnedEventId,
|
||||
events::{AnySyncTimelineEvent, relation::RelationType},
|
||||
serde::Raw,
|
||||
};
|
||||
use ruma::{OwnedEventId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
@@ -50,7 +46,7 @@ pub use self::{
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `EventCacheStore`.
|
||||
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
|
||||
cross_process_lock: Arc<CrossProcessLock<LockableEventCacheStore>>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
@@ -69,7 +65,7 @@ impl EventCacheStoreLock {
|
||||
/// Create a new lock around the [`EventCacheStore`].
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessStoreLock::new`].
|
||||
/// [`CrossProcessLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
where
|
||||
S: IntoEventCacheStore,
|
||||
@@ -77,7 +73,7 @@ impl EventCacheStoreLock {
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
|
||||
cross_process_lock: Arc::new(CrossProcessLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
@@ -86,38 +82,62 @@ impl EventCacheStoreLock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire a spin lock (see [`CrossProcessStoreLock::spin_lock`]).
|
||||
pub async fn lock(&self) -> Result<EventCacheStoreLockGuard<'_>, LockStoreError> {
|
||||
let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?;
|
||||
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
|
||||
pub async fn lock(&self) -> Result<EventCacheStoreLockState, CrossProcessLockError> {
|
||||
let lock_state =
|
||||
self.cross_process_lock.spin_lock(None).await??.map(|cross_process_lock_guard| {
|
||||
EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.clone() }
|
||||
});
|
||||
|
||||
Ok(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
|
||||
Ok(lock_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// The equivalent of [`CrossProcessLockState`] but for the [`EventCacheStore`].
|
||||
///
|
||||
/// [`CrossProcessLockState`]: matrix_sdk_common::cross_process_lock::CrossProcessLockState
|
||||
pub type EventCacheStoreLockState = MappedCrossProcessLockState<EventCacheStoreLockGuard>;
|
||||
|
||||
/// An RAII implementation of a “scoped lock” of an [`EventCacheStoreLock`].
|
||||
/// When this structure is dropped (falls out of scope), the lock will be
|
||||
/// unlocked.
|
||||
pub struct EventCacheStoreLockGuard<'a> {
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLockGuard {
|
||||
/// The cross process lock guard.
|
||||
#[allow(unused)]
|
||||
cross_process_lock_guard: CrossProcessStoreLockGuard,
|
||||
cross_process_lock_guard: CrossProcessLockGuard,
|
||||
|
||||
/// A reference to the store.
|
||||
store: &'a DynEventCacheStore,
|
||||
store: Arc<DynEventCacheStore>,
|
||||
}
|
||||
|
||||
impl EventCacheStoreLockGuard {
|
||||
/// Forward to [`CrossProcessLockGuard::clear_dirty`].
|
||||
///
|
||||
/// This is an associated method to avoid colliding with the [`Deref`]
|
||||
/// implementation.
|
||||
pub fn clear_dirty(this: &Self) {
|
||||
this.cross_process_lock_guard.clear_dirty();
|
||||
}
|
||||
|
||||
/// Force to [`CrossProcessLockGuard::is_dirty`].
|
||||
pub fn is_dirty(this: &Self) -> bool {
|
||||
this.cross_process_lock_guard.is_dirty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
|
||||
impl fmt::Debug for EventCacheStoreLockGuard {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
impl Deref for EventCacheStoreLockGuard {
|
||||
type Target = DynEventCacheStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.store
|
||||
self.store.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,15 +197,21 @@ impl EventCacheStoreError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheStoreError> for CrossProcessLockError {
|
||||
fn from(value: EventCacheStoreError) -> Self {
|
||||
Self::TryLock(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `EventCacheStore` specific result type.
|
||||
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
|
||||
|
||||
/// A type that wraps the [`EventCacheStore`] but implements [`BackingStore`] to
|
||||
/// A type that wraps the [`EventCacheStore`] but implements [`TryLock`] to
|
||||
/// make it usable inside the cross process lock.
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
|
||||
|
||||
impl BackingStore for LockableEventCacheStore {
|
||||
impl TryLock for LockableEventCacheStore {
|
||||
type LockError = EventCacheStoreError;
|
||||
|
||||
async fn try_lock(
|
||||
@@ -193,7 +219,7 @@ impl BackingStore for LockableEventCacheStore {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> std::result::Result<bool, Self::LockError> {
|
||||
) -> std::result::Result<Option<CrossProcessLockGeneration>, Self::LockError> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
@@ -226,18 +252,3 @@ pub fn extract_event_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(Owne
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the list of string filters to be applied when looking for an event's
|
||||
/// relations.
|
||||
// TODO: get Ruma fix from https://github.com/ruma/ruma/pull/2052, and get rid of this function
|
||||
// then.
|
||||
pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<String>> {
|
||||
filters.map(|filter| {
|
||||
filter
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if *f == RelationType::Replacement { "m.replace".to_owned() } else { f.to_string() }
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,21 +17,16 @@ use std::{fmt, sync::Arc};
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
AsyncTraitDeps,
|
||||
cross_process_lock::CrossProcessLockGeneration,
|
||||
linked_chunk::{
|
||||
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
|
||||
RawChunk, Update,
|
||||
},
|
||||
};
|
||||
use ruma::{EventId, MxcUri, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
|
||||
|
||||
use super::{
|
||||
EventCacheStoreError,
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
};
|
||||
use super::EventCacheStoreError;
|
||||
use crate::event_cache::{Event, Gap};
|
||||
|
||||
/// A default capacity for linked chunks, when manipulating in conjunction with
|
||||
/// an `EventCacheStore` implementation.
|
||||
@@ -52,7 +47,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error>;
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error>;
|
||||
|
||||
/// An [`Update`] reflects an operation that has happened inside a linked
|
||||
/// chunk. The linked chunk is used by the event cache to store the events
|
||||
@@ -160,6 +155,17 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<(Event, Option<Position>)>, Self::Error>;
|
||||
|
||||
/// Get all events in this room.
|
||||
///
|
||||
/// This method must return events saved either in any linked chunks, *or*
|
||||
/// events saved "out-of-band" with the [`Self::save_event`] method.
|
||||
async fn get_room_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<Vec<Event>, Self::Error>;
|
||||
|
||||
/// Save an event, that might or might not be part of an existing linked
|
||||
/// chunk.
|
||||
///
|
||||
@@ -170,128 +176,16 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
/// without causing an error.
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
/// Perform database optimizations if any are available, i.e. vacuuming in
|
||||
/// SQLite.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
/// **Warning:** this was added to check if SQLite fragmentation was the
|
||||
/// source of performance issues, **DO NOT use in production**.
|
||||
#[doc(hidden)]
|
||||
async fn optimize(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Replaces the given media's content key with another one.
|
||||
///
|
||||
/// This should be used whenever a temporary (local) MXID has been used, and
|
||||
/// it must now be replaced with its actual remote counterpart (after
|
||||
/// uploading some content, or creating an empty MXC URI).
|
||||
///
|
||||
/// ⚠ No check is performed to ensure that the media formats are consistent,
|
||||
/// i.e. it's possible to update with a thumbnail key a media that was
|
||||
/// keyed as a file before. The caller is responsible of ensuring that
|
||||
/// the replacement makes sense, according to their use case.
|
||||
///
|
||||
/// This should not raise an error when the `from` parameter points to an
|
||||
/// unknown media, and it should silently continue in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `from` - The previous `MediaRequest` of the file.
|
||||
///
|
||||
/// * `to` - The new `MediaRequest` of the file.
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove a media file's content from the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// In theory, there could be several files stored using the same URI and a
|
||||
/// different `MediaFormat`. This API is meant to be used with a media file
|
||||
/// that has only been stored with a single format.
|
||||
///
|
||||
/// If there are several media files for a given URI in different formats,
|
||||
/// this API will only return one of them. Which one is left as an
|
||||
/// implementation detail.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
async fn get_media_content_for_uri(&self, uri: &MxcUri)
|
||||
-> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// This should not raise an error when the `uri` parameter points to an
|
||||
/// unknown media, and it should return an Ok result in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
|
||||
/// keep media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the current `MediaRetentionPolicy`.
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
///
|
||||
/// If there is already an ongoing cleanup, this is a noop.
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error>;
|
||||
/// Returns the size of the store in bytes, if known.
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
@@ -314,7 +208,7 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -387,73 +281,26 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_room_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
self.0.get_room_events(room_id, event_type, session_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
|
||||
self.0.save_event(room_id, event).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
self.0.optimize().await.map_err(Into::into)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.replace_media_key(from, to).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.0.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.0.clean_up_media_cache().await.map_err(Into::into)
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
Ok(self.0.get_size().await.map_err(Into::into)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +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, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
message::{MessageType, SyncRoomMessageEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
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)]
|
||||
@@ -33,14 +16,114 @@ 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),
|
||||
}
|
||||
|
||||
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 [`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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the [`LatestEventValue`] represents a local value or not,
|
||||
/// i.e. it is [`LocalIsSending`] or [`LocalCannotBeSent`].
|
||||
///
|
||||
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
|
||||
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
|
||||
pub fn is_local(&self) -> bool {
|
||||
match self {
|
||||
Self::LocalIsSending(_)
|
||||
| Self::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::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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the value for [`LatestEventValue::Remote`].
|
||||
pub type RemoteLatestEventValue = TimelineEvent;
|
||||
|
||||
@@ -55,675 +138,157 @@ pub struct LocalLatestEventValue {
|
||||
pub content: SerializableEventContent,
|
||||
}
|
||||
|
||||
/// 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
|
||||
YesCallNotify(&'a SyncCallNotifyEvent),
|
||||
|
||||
/// 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::CallNotify(notify)) => {
|
||||
PossibleLatestEvent::YesCallNotify(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;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches::assert_matches;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::serde::Raw;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
mod tests_latest_event_value {
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
|
||||
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
call::{
|
||||
SessionDescription,
|
||||
invite::{CallInviteEventContent, SyncCallInviteEvent},
|
||||
notify::{
|
||||
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
|
||||
},
|
||||
},
|
||||
poll::{
|
||||
unstable_response::{
|
||||
SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
|
||||
},
|
||||
unstable_start::{
|
||||
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
|
||||
UnstablePollAnswer, UnstablePollStartContentBlock,
|
||||
},
|
||||
},
|
||||
relation::Replacement,
|
||||
room::{
|
||||
ImageInfo, MediaSource,
|
||||
encrypted::{
|
||||
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
|
||||
SyncRoomEncryptedEvent,
|
||||
},
|
||||
message::{
|
||||
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
|
||||
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
},
|
||||
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
|
||||
},
|
||||
sticker::{StickerEventContent, SyncStickerEvent},
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
|
||||
owned_event_id,
|
||||
serde::Raw,
|
||||
uint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::LatestEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
|
||||
use super::{LatestEventValue, LocalLatestEventValue, RemoteLatestEventValue};
|
||||
use crate::store::SerializableEventContent;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomMessageEventContent::new(MessageType::Image(
|
||||
ImageMessageEventContent::new(
|
||||
"".to_owned(),
|
||||
MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
|
||||
),
|
||||
)),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
fn test_timestamp_with_none() {
|
||||
let value = LatestEventValue::None;
|
||||
|
||||
assert_eq!(m.content.msgtype.msgtype(), "m.image");
|
||||
assert_eq!(value.timestamp(), None);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_polls_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
|
||||
SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
|
||||
"do you like rust?",
|
||||
vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
|
||||
fn test_timestamp_with_remote() {
|
||||
let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
|
||||
Raw::from_json_string(
|
||||
json!({
|
||||
"content": RoomMessageEventContent::text_plain("raclette"),
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ev0",
|
||||
"room_id": "!r0",
|
||||
"origin_server_ts": 42,
|
||||
"sender": "@mnt_io:matrix.org",
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_local_is_sending() {
|
||||
let value = LatestEventValue::LocalIsSending(LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_local_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"),
|
||||
))
|
||||
.into(),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
|
||||
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
|
||||
SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: CallInviteEventContent::new(
|
||||
"call_id".into(),
|
||||
UInt::new(123).unwrap(),
|
||||
SessionDescription::new("".into(), "".into()),
|
||||
VoipVersionId::V1,
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
|
||||
SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: CallNotifyEventContent::new(
|
||||
"call_id".into(),
|
||||
ApplicationType::Call,
|
||||
NotifyType::Ring,
|
||||
Mentions::new(),
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: StickerEventContent::new(
|
||||
"sticker!".to_owned(),
|
||||
ImageInfo::new(),
|
||||
owned_mxc_uri!("mxc://example.com/1"),
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
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 =
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
|
||||
SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: UnstablePollResponseEventContent::new(
|
||||
vec![String::from("option1")],
|
||||
owned_event_id!("$1"),
|
||||
),
|
||||
event_id: owned_event_id!("$2"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
let room_redaction_event = serde_json::from_value(json!({
|
||||
"content": {},
|
||||
"event_id": "$redaction",
|
||||
"sender": "@x:y.za",
|
||||
"origin_server_ts": 223543,
|
||||
"unsigned": { "reason": "foo" }
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent {
|
||||
content: RedactedRoomMessageEventContent::new(),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: RedactedUnsigned::new(room_redaction_event),
|
||||
}),
|
||||
));
|
||||
|
||||
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 = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomEncryptedEventContent::new(
|
||||
EncryptedEventScheme::OlmV1Curve25519AesSha2(
|
||||
OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()),
|
||||
),
|
||||
None,
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event, None),
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
|
||||
content: RoomTopicEventContent::new("".to_owned()),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: StateUnsigned::new(),
|
||||
state_key: EmptyStateKey,
|
||||
}),
|
||||
));
|
||||
|
||||
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 = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: event_content,
|
||||
event_id: owned_event_id!("$2"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
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 = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomMessageEventContent::new(MessageType::VerificationRequest(
|
||||
KeyVerificationRequestEventContent::new(
|
||||
"body".to_owned(),
|
||||
vec![],
|
||||
device_id!("device_id").to_owned(),
|
||||
user_id!("@user_id:example.com").to_owned(),
|
||||
),
|
||||
)),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_let!(
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType =
|
||||
is_suitable_for_latest_event(&event, 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,
|
||||
.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",
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
// 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_timestamp_with_local_cannot_be_sent() {
|
||||
let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
|
||||
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
|
||||
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
|
||||
RoomMessageEventContent::text_plain("raclette"),
|
||||
))
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
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_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
|
||||
}
|
||||
|
||||
// The even older format can also be deserialized.
|
||||
let serialized = json!({
|
||||
"latest_event": event
|
||||
#[test]
|
||||
fn test_event_id_with_none() {
|
||||
let value = LatestEventValue::None;
|
||||
|
||||
assert!(value.event_id().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
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!(value.event_id(), Some(event_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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"),
|
||||
))
|
||||
.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());
|
||||
assert!(value.event_id().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
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(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(value.event_id(), Some(event_id));
|
||||
}
|
||||
|
||||
#[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(),
|
||||
});
|
||||
|
||||
assert!(value.event_id().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(target_family = "wasm", allow(clippy::arc_with_non_send_sync))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
@@ -45,6 +45,9 @@ pub mod sync;
|
||||
mod test_utils;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
pub mod recent_emojis;
|
||||
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
@@ -57,16 +60,15 @@ pub use once_cell;
|
||||
pub use room::{
|
||||
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
|
||||
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomState,
|
||||
RoomStateFilter, SuccessorRoom, apply_redaction,
|
||||
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp,
|
||||
RoomState, RoomStateFilter, SuccessorRoom, apply_redaction,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
StateStoreDataValue, StoreError, ThreadSubscriptionCatchupToken,
|
||||
};
|
||||
pub use utils::{
|
||||
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
|
||||
QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
|
||||
ThreadSubscriptionCatchupToken,
|
||||
};
|
||||
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawSyncStateEventWithKeys};
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test_utils::init_tracing_for_tests!();
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Media store and common types for [media content](https://spec.matrix.org/latest/client-server-api/#content-repository).
|
||||
|
||||
pub mod store;
|
||||
|
||||
use ruma::{
|
||||
MxcUri, UInt,
|
||||
+390
-55
@@ -12,26 +12,28 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Trait and macro of integration tests for `EventCacheStoreMedia`
|
||||
//! Trait and macro of integration tests for `MediaStoreInner`
|
||||
//! implementations.
|
||||
|
||||
use ruma::{
|
||||
events::room::MediaSource,
|
||||
media::Method,
|
||||
mxc_uri, owned_mxc_uri,
|
||||
time::{Duration, SystemTime},
|
||||
uint,
|
||||
};
|
||||
|
||||
use super::{
|
||||
EventCacheStoreMedia, MediaRetentionPolicy, media_service::IgnoreMediaRetentionPolicy,
|
||||
use super::{MediaRetentionPolicy, MediaStoreInner, media_service::IgnoreMediaRetentionPolicy};
|
||||
use crate::media::{
|
||||
MediaFormat, MediaRequestParameters, MediaThumbnailSettings, store::MediaStore,
|
||||
};
|
||||
use crate::media::{MediaFormat, MediaRequestParameters};
|
||||
|
||||
/// [`EventCacheStoreMedia`] integration tests.
|
||||
/// [`MediaStoreInner`] integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `event_cache_store_media_integration_tests!` macro.
|
||||
/// `media_store_inner_integration_tests!` macro.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait EventCacheStoreMediaIntegrationTests {
|
||||
pub trait MediaStoreInnerIntegrationTests {
|
||||
/// Test media retention policy storage.
|
||||
async fn test_store_media_retention_policy(&self);
|
||||
|
||||
@@ -56,9 +58,9 @@ pub trait EventCacheStoreMediaIntegrationTests {
|
||||
async fn test_store_last_media_cleanup_time(&self);
|
||||
}
|
||||
|
||||
impl<Store> EventCacheStoreMediaIntegrationTests for Store
|
||||
impl<Store> MediaStoreInnerIntegrationTests for Store
|
||||
where
|
||||
Store: EventCacheStoreMedia + std::fmt::Debug,
|
||||
Store: MediaStoreInner + std::fmt::Debug,
|
||||
{
|
||||
async fn test_store_media_retention_policy(&self) {
|
||||
let stored = self.media_retention_policy_inner().await.unwrap();
|
||||
@@ -138,7 +140,7 @@ where
|
||||
assert!(stored.is_some());
|
||||
|
||||
// A cleanup doesn't have any effect.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
|
||||
assert!(stored.is_some());
|
||||
@@ -149,7 +151,7 @@ where
|
||||
let policy = MediaRetentionPolicy::empty().with_max_file_size(Some(100));
|
||||
|
||||
// The cleanup removes the average media.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
|
||||
assert!(stored.is_none());
|
||||
@@ -217,7 +219,7 @@ where
|
||||
.with_max_file_size(Some(1000));
|
||||
|
||||
// The cleanup removes the average media.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
|
||||
assert!(stored.is_none());
|
||||
@@ -395,7 +397,7 @@ where
|
||||
|
||||
// Cleanup removes the oldest content first.
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small_1, time).await.unwrap();
|
||||
@@ -481,7 +483,7 @@ where
|
||||
// before.
|
||||
time += Duration::from_secs(1);
|
||||
tracing::info!(?self, "before");
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
tracing::info!(?self, "after");
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small_1, time).await.unwrap();
|
||||
@@ -602,7 +604,7 @@ where
|
||||
assert_eq!(time, SystemTime::UNIX_EPOCH + Duration::from_secs(10));
|
||||
|
||||
// Cleanup has no effect, nothing has expired.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -629,7 +631,7 @@ where
|
||||
time += Duration::from_secs(26);
|
||||
|
||||
// Cleanup removes the two oldest media contents.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -745,7 +747,7 @@ where
|
||||
|
||||
// Because the big and average contents are ignored, cleanup has no effect.
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
|
||||
@@ -763,7 +765,7 @@ where
|
||||
.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
|
||||
@@ -782,7 +784,7 @@ where
|
||||
.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
|
||||
@@ -892,7 +894,7 @@ where
|
||||
time += Duration::from_secs(120);
|
||||
|
||||
// Cleanup removes all the media contents that are not ignored.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -922,7 +924,7 @@ where
|
||||
time += Duration::from_secs(120);
|
||||
|
||||
// Cleanup removes the remaining media contents.
|
||||
self.clean_up_media_cache_inner(policy, time).await.unwrap();
|
||||
self.clean_inner(policy, time).await.unwrap();
|
||||
|
||||
time += Duration::from_secs(1);
|
||||
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
|
||||
@@ -947,21 +949,21 @@ where
|
||||
|
||||
// With an empty policy.
|
||||
let policy = MediaRetentionPolicy::empty();
|
||||
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
|
||||
self.clean_inner(policy, new_time).await.unwrap();
|
||||
|
||||
let stored = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
assert_eq!(stored, initial);
|
||||
|
||||
// With the default policy.
|
||||
let policy = MediaRetentionPolicy::default();
|
||||
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
|
||||
self.clean_inner(policy, new_time).await.unwrap();
|
||||
|
||||
let stored = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
assert_eq!(stored, Some(new_time));
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your [`EventCacheStoreMedia`] implementation to run
|
||||
/// Macro building to allow your [`MediaStoreInner`] implementation to run
|
||||
/// the entire tests suite locally.
|
||||
///
|
||||
/// Can be run with the `with_media_size_tests` argument to include more tests
|
||||
@@ -969,91 +971,424 @@ where
|
||||
/// recommended to run those in encrypted stores because the size of the
|
||||
/// encrypted content may vary compared to what the tests expect.
|
||||
///
|
||||
/// You need to provide an `async fn get_event_cache_store() ->
|
||||
/// event_cache::store::Result<Store>` that provides a fresh event cache store
|
||||
/// that implements `EventCacheStoreMedia` on the same level you invoke the
|
||||
/// You need to provide an `async fn get_media_store() ->
|
||||
/// media::store::Result<Store>` that provides a fresh media store
|
||||
/// that implements `MediaStoreInner` on the same level you invoke the
|
||||
/// macro.
|
||||
///
|
||||
/// ## Usage Example:
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_base::event_cache::store::{
|
||||
/// # EventCacheStore,
|
||||
/// # MemoryStore as MyStore,
|
||||
/// # Result as EventCacheStoreResult,
|
||||
/// # use matrix_sdk_base::media::store::{
|
||||
/// # MediaStore,
|
||||
/// # MemoryMediaStore as MyStore,
|
||||
/// # Result as MediaStoreResult,
|
||||
/// # };
|
||||
///
|
||||
/// #[cfg(test)]
|
||||
/// mod tests {
|
||||
/// use super::{EventCacheStoreResult, MyStore};
|
||||
/// use super::{MediaStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_event_cache_store() -> EventCacheStoreResult<MyStore> {
|
||||
/// async fn get_media_store() -> MediaStoreResult<MyStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
/// event_cache_store_media_integration_tests!();
|
||||
/// media_store_inner_integration_tests!();
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! event_cache_store_media_integration_tests {
|
||||
macro_rules! media_store_inner_integration_tests {
|
||||
(with_media_size_tests) => {
|
||||
mod event_cache_store_media_integration_tests {
|
||||
$crate::event_cache_store_media_integration_tests!(@inner);
|
||||
mod media_store_inner_integration_tests {
|
||||
$crate::media_store_inner_integration_tests!(@inner);
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_max_file_size() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_max_file_size().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_max_file_size().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_max_cache_size() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_max_cache_size().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_max_cache_size().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_ignore_max_size() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_ignore_max_size().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_ignore_max_size().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
() => {
|
||||
mod event_cache_store_media_integration_tests {
|
||||
$crate::event_cache_store_media_integration_tests!(@inner);
|
||||
mod media_store_inner_integration_tests {
|
||||
$crate::media_store_inner_integration_tests!(@inner);
|
||||
}
|
||||
};
|
||||
|
||||
(@inner) => {
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::event_cache::store::media::EventCacheStoreMediaIntegrationTests;
|
||||
use $crate::media::store::MediaStoreInnerIntegrationTests;
|
||||
|
||||
use super::get_event_cache_store;
|
||||
use super::get_media_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_store_media_retention_policy() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_store_media_retention_policy().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_store_media_retention_policy().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_expiry() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_expiry().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_expiry().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_ignore_expiry() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_media_ignore_expiry().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_media_ignore_expiry().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_store_last_media_cleanup_time() {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.test_store_last_media_cleanup_time().await;
|
||||
let media_store_inner = get_media_store().await.unwrap();
|
||||
media_store_inner.test_store_last_media_cleanup_time().await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// [`MediaStore`] integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// `media_store_inner_integration_tests!` macro.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait MediaStoreIntegrationTests {
|
||||
/// Test media content storage.
|
||||
async fn test_media_content(&self);
|
||||
|
||||
/// Test replacing a MXID.
|
||||
async fn test_replace_media_key(&self);
|
||||
}
|
||||
|
||||
impl<Store> MediaStoreIntegrationTests for Store
|
||||
where
|
||||
Store: MediaStore + std::fmt::Debug,
|
||||
{
|
||||
async fn test_media_content(&self) {
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let request_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
let request_thumbnail = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
|
||||
Method::Crop,
|
||||
uint!(100),
|
||||
uint!(100),
|
||||
)),
|
||||
};
|
||||
|
||||
let other_uri = mxc_uri!("mxc://localhost/media-other");
|
||||
let request_other_file = MediaRequestParameters {
|
||||
source: MediaSource::Plain(other_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content: Vec<u8> = "hello".into();
|
||||
let thumbnail_content: Vec<u8> = "world".into();
|
||||
let other_content: Vec<u8> = "foo".into();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"unexpected media found"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"media not found"
|
||||
);
|
||||
|
||||
// Let's add the media.
|
||||
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media failed");
|
||||
|
||||
// Media is present in the cache.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found though added"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found by URI though added"
|
||||
);
|
||||
|
||||
// Let's remove the media.
|
||||
self.remove_media_content(&request_file).await.expect("removing media failed");
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media still there after removing"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
|
||||
"media still found by URI after removing"
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media again failed");
|
||||
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_file).await.unwrap().as_ref(),
|
||||
Some(&content),
|
||||
"media not found after adding again"
|
||||
);
|
||||
|
||||
// Let's add the thumbnail media.
|
||||
self.add_media_content(
|
||||
&request_thumbnail,
|
||||
thumbnail_content.clone(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
|
||||
// Media's thumbnail is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
|
||||
Some(&thumbnail_content),
|
||||
"thumbnail not found"
|
||||
);
|
||||
|
||||
// We get a file with the URI, we don't know which one.
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_some(),
|
||||
"media not found by URI though two where added"
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(
|
||||
&request_other_file,
|
||||
other_content.clone(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
|
||||
// Other file is present.
|
||||
assert_eq!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found"
|
||||
);
|
||||
assert_eq!(
|
||||
self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(),
|
||||
Some(&other_content),
|
||||
"other file not found by URI"
|
||||
);
|
||||
|
||||
// Let's remove media based on URI.
|
||||
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
|
||||
|
||||
assert!(
|
||||
self.get_media_content(&request_file).await.unwrap().is_none(),
|
||||
"media wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
|
||||
"thumbnail wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content(&request_other_file).await.unwrap().is_some(),
|
||||
"other media was removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
|
||||
"media found by URI wasn't removed"
|
||||
);
|
||||
assert!(
|
||||
self.get_media_content_for_uri(other_uri).await.unwrap().is_some(),
|
||||
"other media found by URI was removed"
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_replace_media_key(&self) {
|
||||
let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
|
||||
let req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let content = "hello".as_bytes().to_owned();
|
||||
|
||||
// Media isn't present in the cache.
|
||||
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
|
||||
|
||||
// Add the media.
|
||||
self.add_media_content(&req, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media failed");
|
||||
|
||||
// Sanity-check: media is found after adding it.
|
||||
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
|
||||
|
||||
// Replacing a media request works.
|
||||
let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
|
||||
let new_req = MediaRequestParameters {
|
||||
source: MediaSource::Plain(new_uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
self.replace_media_key(&req, &new_req)
|
||||
.await
|
||||
.expect("replacing the media request key failed");
|
||||
|
||||
// Finding with the previous request doesn't work anymore.
|
||||
assert!(
|
||||
self.get_media_content(&req).await.unwrap().is_none(),
|
||||
"unexpected media found with the old key"
|
||||
);
|
||||
|
||||
// Finding with the new request does work.
|
||||
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your [`MediaStore`] implementation to run
|
||||
/// the entire tests suite locally.
|
||||
///
|
||||
/// You need to provide an `async fn get_media_store() ->
|
||||
/// media::store::Result<Store>` that provides a fresh media store
|
||||
/// that implements `MediaStoreInner` on the same level you invoke the
|
||||
/// macro.
|
||||
///
|
||||
/// ## Usage Example:
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_base::media::store::{
|
||||
/// # MediaStore,
|
||||
/// # MemoryMediaStore as MyStore,
|
||||
/// # Result as MediaStoreResult,
|
||||
/// # };
|
||||
///
|
||||
/// #[cfg(test)]
|
||||
/// mod tests {
|
||||
/// use super::{MediaStoreResult, MyStore};
|
||||
///
|
||||
/// async fn get_media_store() -> MediaStoreResult<MyStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
/// media_store_integration_tests!();
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! media_store_integration_tests {
|
||||
() => {
|
||||
mod media_store_integration_tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use $crate::media::store::integration_tests::MediaStoreIntegrationTests;
|
||||
|
||||
use super::get_media_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_content() {
|
||||
let media_store = get_media_store().await.unwrap();
|
||||
media_store.test_media_content().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_replace_media_key() {
|
||||
let media_store = get_media_store().await.unwrap();
|
||||
media_store.test_replace_media_key().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro generating tests for the media store, related to time (mostly
|
||||
/// for the cross-process lock).
|
||||
#[allow(unused_macros)]
|
||||
#[macro_export]
|
||||
macro_rules! media_store_integration_tests_time {
|
||||
() => {
|
||||
mod media_store_integration_tests_time {
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
|
||||
use gloo_timers::future::sleep;
|
||||
use matrix_sdk_test::async_test;
|
||||
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
|
||||
use tokio::time::sleep;
|
||||
use $crate::media::store::MediaStore;
|
||||
|
||||
use super::get_media_store;
|
||||
|
||||
#[async_test]
|
||||
async fn test_lease_locks() {
|
||||
let store = get_media_store().await.unwrap();
|
||||
|
||||
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert_eq!(acquired0, Some(1)); // first lock generation
|
||||
|
||||
// Should extend the lease automatically (same holder).
|
||||
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert_eq!(acquired2, Some(1)); // same lock generation
|
||||
|
||||
// Should extend the lease automatically (same holder + time is ok).
|
||||
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert_eq!(acquired3, Some(1)); // same lock generation
|
||||
|
||||
// Another attempt at taking the lock should fail, because it's taken.
|
||||
let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired4.is_none()); // not acquired
|
||||
|
||||
// Even if we insist.
|
||||
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired5.is_none()); // not acquired
|
||||
|
||||
// That's a nice test we got here, go take a little nap.
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
|
||||
// Still too early.
|
||||
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert!(acquired55.is_none()); // not acquired
|
||||
|
||||
// Ok you can take another nap then.
|
||||
sleep(Duration::from_millis(250)).await;
|
||||
|
||||
// At some point, we do get the lock.
|
||||
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
|
||||
assert_eq!(acquired6, Some(2)); // new lock generation!
|
||||
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// The other gets it almost immediately too.
|
||||
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
|
||||
assert_eq!(acquired7, Some(3)); // new lock generation!
|
||||
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
|
||||
// But when we take a longer lease…
|
||||
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert_eq!(acquired8, Some(4)); // new lock generation!
|
||||
|
||||
// It blocks the other user.
|
||||
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
|
||||
assert!(acquired9.is_none()); // not acquired
|
||||
|
||||
// We can hold onto our lease.
|
||||
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
|
||||
assert_eq!(acquired10, Some(4)); // same lock generation
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+8
-5
@@ -17,19 +17,22 @@
|
||||
//! indefinitely.
|
||||
//!
|
||||
//! To proceed to a cleanup, first set the [`MediaRetentionPolicy`] to use with
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]. Then call
|
||||
//! [`EventCacheStore::clean_up_media_cache()`].
|
||||
//! [`MediaStore::set_media_retention_policy()`]. Then call
|
||||
//! [`MediaStore::clean()`].
|
||||
//!
|
||||
//! In the future, other settings will allow to run automatic periodic cleanup
|
||||
//! jobs.
|
||||
//!
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]: crate::event_cache::store::EventCacheStore::set_media_retention_policy
|
||||
//! [`EventCacheStore::clean_up_media_cache()`]: crate::event_cache::store::EventCacheStore::clean_up_media_cache
|
||||
//! [`MediaStore::set_media_retention_policy()`]: crate::media::store::MediaStore::set_media_retention_policy
|
||||
//! [`MediaStore::clean()`]: crate::media::store::MediaStore::clean
|
||||
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The retention policy for media content used by the [`EventCacheStore`].
|
||||
#[cfg(doc)]
|
||||
use crate::media::store::MediaStore;
|
||||
|
||||
/// The retention policy for media content used by the [`MediaStore`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
+47
-177
@@ -12,11 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
|
||||
SendOutsideWasm, SyncOutsideWasm,
|
||||
executor::{JoinHandle, spawn},
|
||||
locks::Mutex,
|
||||
};
|
||||
@@ -24,13 +23,13 @@ use ruma::{MxcUri, time::SystemTime};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::error;
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
|
||||
use super::{MediaRetentionPolicy, MediaStoreInner};
|
||||
use crate::media::MediaRequestParameters;
|
||||
|
||||
/// API for implementors of [`EventCacheStore`] to manage their media through
|
||||
/// their implementation of [`EventCacheStoreMedia`].
|
||||
/// API for implementors of [`MediaStore`] to manage their media through
|
||||
/// their implementation of [`MediaStoreInner`].
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
/// [`MediaStore`]: crate::media::store::MediaStore
|
||||
#[derive(Debug)]
|
||||
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
inner: Arc<MediaServiceInner<Time>>,
|
||||
@@ -122,10 +121,10 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn set_media_retention_policy<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
policy: MediaRetentionPolicy,
|
||||
@@ -148,7 +147,7 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
@@ -156,7 +155,7 @@ where
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn add_media_content<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -189,13 +188,13 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn set_ignore_media_retention_policy<Store: EventCacheStoreMedia>(
|
||||
pub async fn set_ignore_media_retention_policy<Store: MediaStoreInner>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -208,10 +207,10 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn get_media_content<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -229,10 +228,10 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia + 'static>(
|
||||
pub async fn get_media_content_for_uri<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
uri: &MxcUri,
|
||||
@@ -251,15 +250,12 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
pub async fn clean_up_media_cache<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
) -> Result<(), Store::Error> {
|
||||
self.clean_up_media_cache_inner(store, self.now()).await
|
||||
/// * `store` - The `MediaStoreInner`.
|
||||
pub async fn clean<Store: MediaStoreInner>(&self, store: &Store) -> Result<(), Store::Error> {
|
||||
self.clean_inner(store, self.now()).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner<Store: EventCacheStoreMedia>(
|
||||
async fn clean_inner<Store: MediaStoreInner>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
@@ -276,7 +272,7 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store.clean_up_media_cache_inner(policy, current_time).await?;
|
||||
store.clean_inner(policy, current_time).await?;
|
||||
|
||||
*self.inner.last_media_cleanup_time.lock() = Some(current_time);
|
||||
|
||||
@@ -290,7 +286,7 @@ where
|
||||
/// * The media retention policy's `cleanup_frequency` is set and enough
|
||||
/// time has passed since the last cleanup.
|
||||
/// * No other cleanup is running,
|
||||
fn maybe_spawn_automatic_media_cache_cleanup<Store: EventCacheStoreMedia + 'static>(
|
||||
fn maybe_spawn_automatic_media_cache_cleanup<Store: MediaStoreInner + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
@@ -320,7 +316,7 @@ where
|
||||
let store = store.clone();
|
||||
|
||||
let handle = spawn(async move {
|
||||
if let Err(error) = this.clean_up_media_cache_inner(&store, current_time).await {
|
||||
if let Err(error) = this.clean_inner(&store, current_time).await {
|
||||
error!("Failed to run automatic media cache cleanup: {error}");
|
||||
}
|
||||
});
|
||||
@@ -349,132 +345,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the media cache of the SDK.
|
||||
///
|
||||
/// The main purposes of this trait are to be able to centralize where we handle
|
||||
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
|
||||
/// simplify the implementation of tests by being able to have complete control
|
||||
/// over the `SystemTime`s provided to the store.
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
|
||||
|
||||
/// The persisted media retention policy in the media cache.
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
|
||||
|
||||
/// Persist the media retention policy in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to persist.
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to set the last access time of the
|
||||
/// media.
|
||||
///
|
||||
/// * `policy` - The media retention policy, to check whether the media is
|
||||
/// too big to be cached.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
|
||||
/// for this media. This setting should be persisted alongside the media
|
||||
/// and taken into account whenever the policy is used.
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
current_time: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// If the media of the given request is not found, this should be a noop.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the given policy.
|
||||
///
|
||||
/// For the integration tests, it is expected that content that does not
|
||||
/// pass the last access expiry and max file size criteria will be
|
||||
/// removed first. After that, the remaining cache size should be
|
||||
/// computed to compare against the max cache size criteria.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The media retention policy to use for the cleanup. The
|
||||
/// `cleanup_frequency` will be ignored.
|
||||
///
|
||||
/// * `current_time` - The current time, to be used to check for expired
|
||||
/// content and to be stored as the time of the last media cache cleanup.
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// The time of the last media cache cleanup.
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
|
||||
}
|
||||
|
||||
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
|
||||
/// content.
|
||||
///
|
||||
@@ -544,18 +414,18 @@ mod tests {
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
|
||||
use crate::{
|
||||
event_cache::store::{EventCacheStoreError, media::MediaRetentionPolicy},
|
||||
media::{MediaFormat, MediaRequestParameters, UniqueKey},
|
||||
use super::{
|
||||
IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStoreInner,
|
||||
TimeProvider,
|
||||
};
|
||||
use crate::media::{MediaFormat, MediaRequestParameters, UniqueKey, store::MediaStoreError};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct MockEventCacheStoreMedia {
|
||||
inner: Arc<Mutex<MockEventCacheStoreMediaInner>>,
|
||||
struct MockMediaStoreInner {
|
||||
inner: Arc<Mutex<MockMediaStoreInnerInner>>,
|
||||
}
|
||||
|
||||
impl MockEventCacheStoreMedia {
|
||||
impl MockMediaStoreInner {
|
||||
/// Whether the store was accessed.
|
||||
fn accessed(&self) -> bool {
|
||||
self.inner.lock().accessed
|
||||
@@ -570,7 +440,7 @@ mod tests {
|
||||
///
|
||||
/// Should be called for every access to the inner store as it also sets
|
||||
/// the `accessed` boolean.
|
||||
fn inner(&self) -> MutexGuard<'_, MockEventCacheStoreMediaInner> {
|
||||
fn inner(&self) -> MutexGuard<'_, MockMediaStoreInnerInner> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.accessed = true;
|
||||
inner
|
||||
@@ -578,7 +448,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockEventCacheStoreMediaInner {
|
||||
struct MockMediaStoreInnerInner {
|
||||
/// Whether this store was accessed.
|
||||
///
|
||||
/// Must be set to `true` for any operation that unlocks the store.
|
||||
@@ -614,26 +484,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockEventCacheStoreMediaError;
|
||||
struct MockMediaStoreInnerError;
|
||||
|
||||
impl fmt::Display for MockEventCacheStoreMediaError {
|
||||
impl fmt::Display for MockMediaStoreInnerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "MockEventCacheStoreMediaError")
|
||||
write!(f, "MockMediaStoreInnerError")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MockEventCacheStoreMediaError {}
|
||||
impl std::error::Error for MockMediaStoreInnerError {}
|
||||
|
||||
impl From<MockEventCacheStoreMediaError> for EventCacheStoreError {
|
||||
fn from(value: MockEventCacheStoreMediaError) -> Self {
|
||||
impl From<MockMediaStoreInnerError> for MediaStoreError {
|
||||
fn from(value: MockMediaStoreInnerError) -> Self {
|
||||
Self::backend(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
|
||||
type Error = MockEventCacheStoreMediaError;
|
||||
impl MediaStoreInner for MockMediaStoreInner {
|
||||
type Error = MockMediaStoreInnerError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
@@ -736,7 +606,7 @@ mod tests {
|
||||
Ok(Some(media_content.content.clone()))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
async fn clean_inner(
|
||||
&self,
|
||||
_policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
@@ -787,7 +657,7 @@ mod tests {
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let store = MockMediaStoreInner::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// By default an empty policy is used.
|
||||
@@ -849,7 +719,7 @@ mod tests {
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
service.clean(&store).await.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
}
|
||||
@@ -877,7 +747,7 @@ mod tests {
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let store = MockMediaStoreInner::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Check that restoring the policy works.
|
||||
@@ -1011,7 +881,7 @@ mod tests {
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
service.clean(&store).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
}
|
||||
@@ -1034,7 +904,7 @@ mod tests {
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let store = MockMediaStoreInner::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Set an empty policy.
|
||||
@@ -0,0 +1,451 @@
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
num::NonZeroUsize,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
cross_process_lock::{
|
||||
CrossProcessLockGeneration,
|
||||
memory_store_helper::{Lease, try_take_leased_lock},
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedMxcUri, time::SystemTime};
|
||||
|
||||
use super::Result;
|
||||
use crate::media::{
|
||||
MediaRequestParameters, UniqueKey as _,
|
||||
store::{
|
||||
IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore,
|
||||
MediaStoreError, MediaStoreInner,
|
||||
},
|
||||
};
|
||||
|
||||
/// In-memory, non-persistent implementation of the `MediaStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryMediaStore {
|
||||
inner: Arc<StdRwLock<MemoryMediaStoreInner>>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryMediaStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, Lease>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: SystemTime,
|
||||
}
|
||||
|
||||
/// A media content in the `MemoryStore`.
|
||||
#[derive(Debug)]
|
||||
struct MediaContent {
|
||||
/// The URI of the content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The unique key of the content.
|
||||
key: String,
|
||||
|
||||
/// The bytes of the content.
|
||||
data: Vec<u8>,
|
||||
|
||||
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
|
||||
|
||||
impl Default for MemoryMediaStore {
|
||||
fn default() -> Self {
|
||||
// Given that the store is empty, we won't need to clean it up right away.
|
||||
let last_media_cleanup_time = SystemTime::now();
|
||||
let media_service = MediaService::new();
|
||||
media_service.restore(None, Some(last_media_cleanup_time));
|
||||
|
||||
Self {
|
||||
inner: Arc::new(StdRwLock::new(MemoryMediaStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
media_retention_policy: None,
|
||||
last_media_cleanup_time,
|
||||
})),
|
||||
media_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryMediaStore {
|
||||
/// Create a new empty MemoryMediaStore
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl MediaStore for MemoryMediaStore {
|
||||
type Error = MediaStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.add_media_content(self, request, data, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some(media_content) =
|
||||
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
|
||||
{
|
||||
media_content.uri = to.uri().to_owned();
|
||||
media_content.key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.media_service.get_media_content(self, request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) =
|
||||
inner.media.iter().position(|media_content| media_content.key == expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
inner.media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.media_service.get_media_content_for_uri(self, uri).await
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let positions = inner
|
||||
.media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
for position in positions.into_iter().rev() {
|
||||
inner.media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_media_retention_policy(self, policy).await
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.media_service.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn clean(&self) -> Result<(), Self::Error> {
|
||||
self.media_service.clean(self).await
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl MediaStoreInner for MemoryMediaStore {
|
||||
type Error = MediaStoreError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
|
||||
Ok(self.inner.read().unwrap().media_retention_policy)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().media_retention_policy = Some(policy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
last_access: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push(MediaContent {
|
||||
uri: request.uri().to_owned(),
|
||||
key: request.unique_key(),
|
||||
data,
|
||||
ignore_policy,
|
||||
last_access,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
|
||||
{
|
||||
media_content.ignore_policy = ignore_policy.is_yes();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
expected_uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn clean_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error> {
|
||||
if !policy.has_limitations() {
|
||||
// We can safely skip all the checks.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First, check media content that exceed the max filesize.
|
||||
if policy.computed_max_file_size().is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
// Then, clean up expired media content.
|
||||
if policy.last_access_expiry.is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy
|
||||
|| !policy.has_content_expired(current_time, content.last_access)
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, if the cache size is too big, remove old items until it fits.
|
||||
if let Some(max_cache_size) = policy.max_cache_size {
|
||||
// Reverse the iterator because in case the cache size is overflowing, we want
|
||||
// to count the number of old items to remove. Items are sorted by last access
|
||||
// and old items are at the start.
|
||||
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
|
||||
(0u64, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
|(mut cache_size, mut items_to_remove), (index, content)| {
|
||||
if content.ignore_policy {
|
||||
// Do not count it.
|
||||
return (cache_size, items_to_remove);
|
||||
}
|
||||
|
||||
let remove_item = if items_to_remove.is_empty() {
|
||||
// We have not reached the max cache size yet.
|
||||
if let Some(sum) = cache_size.checked_add(content.data.len() as u64) {
|
||||
cache_size = sum;
|
||||
// Start removing items if we have exceeded the max cache size.
|
||||
cache_size > max_cache_size
|
||||
} else {
|
||||
// The cache size is overflowing, remove the remaining items, since the
|
||||
// max cache size cannot be bigger than
|
||||
// usize::MAX.
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// We have reached the max cache size already, just remove it.
|
||||
true
|
||||
};
|
||||
|
||||
if remove_item {
|
||||
items_to_remove.push(index);
|
||||
}
|
||||
|
||||
(cache_size, items_to_remove)
|
||||
},
|
||||
);
|
||||
|
||||
// The indexes are already in reverse order so we can just iterate in that order
|
||||
// to remove them starting by the end.
|
||||
for index in items_to_remove {
|
||||
inner.media.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
inner.last_media_cleanup_time = current_time;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MemoryMediaStore, Result};
|
||||
use crate::{
|
||||
media_store_inner_integration_tests, media_store_integration_tests,
|
||||
media_store_integration_tests_time,
|
||||
};
|
||||
|
||||
async fn get_media_store() -> Result<MemoryMediaStore> {
|
||||
Ok(MemoryMediaStore::new())
|
||||
}
|
||||
|
||||
media_store_inner_integration_tests!();
|
||||
media_store_integration_tests!();
|
||||
media_store_integration_tests_time!();
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! The media store holds downloaded media when the cache was
|
||||
//! activated to save bandwidth at the cost of increased storage space usage.
|
||||
//!
|
||||
//! Implementing the `MediaStore` trait, you can plug any storage backend
|
||||
//! into the media store for the actual storage. By default this brings an
|
||||
//! in-memory store.
|
||||
|
||||
mod media_retention_policy;
|
||||
mod media_service;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
use std::fmt;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::cross_process_lock::{
|
||||
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
|
||||
CrossProcessLockState, TryLock,
|
||||
};
|
||||
use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner};
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::{MediaStoreInnerIntegrationTests, MediaStoreIntegrationTests};
|
||||
pub use self::{
|
||||
media_retention_policy::MediaRetentionPolicy,
|
||||
media_service::{IgnoreMediaRetentionPolicy, MediaService},
|
||||
memory_store::MemoryMediaStore,
|
||||
};
|
||||
|
||||
/// Media store specific error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MediaStoreError {
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the media store: {0}")]
|
||||
Encryption(#[from] StoreEncryptionError),
|
||||
|
||||
/// The store contains invalid data.
|
||||
#[error("The store contains invalid data: {details}")]
|
||||
InvalidData {
|
||||
/// Details why the data contained in the store was invalid.
|
||||
details: String,
|
||||
},
|
||||
|
||||
/// The store failed to serialize or deserialize some data.
|
||||
#[error("Error serializing or deserializing data from the media store: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl MediaStoreError {
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `MediaStoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaStoreError> for CrossProcessLockError {
|
||||
fn from(value: MediaStoreError) -> Self {
|
||||
Self::TryLock(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// An `MediaStore` specific result type.
|
||||
pub type Result<T, E = MediaStoreError> = std::result::Result<T, E>;
|
||||
|
||||
/// The high-level public type to represent an `MediaStore` lock.
|
||||
#[derive(Clone)]
|
||||
pub struct MediaStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `MediaStore`.
|
||||
cross_process_lock: Arc<CrossProcessLock<LockableMediaStore>>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
/// That's the only place where the store exists.
|
||||
store: Arc<DynMediaStore>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for MediaStoreLock {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("MediaStoreLock").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaStoreLock {
|
||||
/// Create a new lock around the [`MediaStore`].
|
||||
///
|
||||
/// The `holder` argument represents the holder inside the
|
||||
/// [`CrossProcessLock::new`].
|
||||
pub fn new<S>(store: S, holder: String) -> Self
|
||||
where
|
||||
S: IntoMediaStore,
|
||||
{
|
||||
let store = store.into_media_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: Arc::new(CrossProcessLock::new(
|
||||
LockableMediaStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
)),
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
|
||||
pub async fn lock(&self) -> Result<MediaStoreLockGuard<'_>, CrossProcessLockError> {
|
||||
let cross_process_lock_guard = match self.cross_process_lock.spin_lock(None).await?? {
|
||||
// The lock is clean: no other hold acquired it, all good!
|
||||
CrossProcessLockState::Clean(guard) => guard,
|
||||
|
||||
// The lock is dirty: another holder acquired it since the last time we acquired it.
|
||||
// It's not a problem in the case of the `MediaStore` because this API is “stateless” at
|
||||
// the time of writing (2025-11-11). There is nothing that can be out-of-sync: all the
|
||||
// state is in the database, nothing in memory.
|
||||
CrossProcessLockState::Dirty(guard) => {
|
||||
guard.clear_dirty();
|
||||
|
||||
guard
|
||||
}
|
||||
};
|
||||
|
||||
Ok(MediaStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
|
||||
}
|
||||
}
|
||||
|
||||
/// An RAII implementation of a “scoped lock” of an [`MediaStoreLock`].
|
||||
/// When this structure is dropped (falls out of scope), the lock will be
|
||||
/// unlocked.
|
||||
pub struct MediaStoreLockGuard<'a> {
|
||||
/// The cross process lock guard.
|
||||
#[allow(unused)]
|
||||
cross_process_lock_guard: CrossProcessLockGuard,
|
||||
|
||||
/// A reference to the store.
|
||||
store: &'a DynMediaStore,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for MediaStoreLockGuard<'_> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("MediaStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MediaStoreLockGuard<'_> {
|
||||
type Target = DynMediaStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.store
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that wraps the [`MediaStore`] but implements [`TryLock`] to
|
||||
/// make it usable inside the cross process lock.
|
||||
#[derive(Clone, Debug)]
|
||||
struct LockableMediaStore(Arc<DynMediaStore>);
|
||||
|
||||
impl TryLock for LockableMediaStore {
|
||||
type LockError = MediaStoreError;
|
||||
|
||||
async fn try_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> std::result::Result<Option<CrossProcessLockGeneration>, Self::LockError> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Types and traits regarding media caching of the media store.
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{AsyncTraitDeps, cross_process_lock::CrossProcessLockGeneration};
|
||||
use ruma::{MxcUri, time::SystemTime};
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::media::store::MediaService;
|
||||
use crate::media::{
|
||||
MediaRequestParameters,
|
||||
store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaStoreError},
|
||||
};
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the media of the SDK.
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait MediaStore: AsyncTraitDeps {
|
||||
/// The error type used by this media store.
|
||||
type Error: fmt::Debug + Into<MediaStoreError>;
|
||||
|
||||
/// Try to take a lock using the given store.
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Replaces the given media's content key with another one.
|
||||
///
|
||||
/// This should be used whenever a temporary (local) MXID has been used, and
|
||||
/// it must now be replaced with its actual remote counterpart (after
|
||||
/// uploading some content, or creating an empty MXC URI).
|
||||
///
|
||||
/// ⚠ No check is performed to ensure that the media formats are consistent,
|
||||
/// i.e. it's possible to update with a thumbnail key a media that was
|
||||
/// keyed as a file before. The caller is responsible of ensuring that
|
||||
/// the replacement makes sense, according to their use case.
|
||||
///
|
||||
/// This should not raise an error when the `from` parameter points to an
|
||||
/// unknown media, and it should silently continue in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `from` - The previous `MediaRequest` of the file.
|
||||
///
|
||||
/// * `to` - The new `MediaRequest` of the file.
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove a media file's content from the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// In theory, there could be several files stored using the same URI and a
|
||||
/// different `MediaFormat`. This API is meant to be used with a media file
|
||||
/// that has only been stored with a single format.
|
||||
///
|
||||
/// If there are several media files for a given URI in different formats,
|
||||
/// this API will only return one of them. Which one is left as an
|
||||
/// implementation detail.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
async fn get_media_content_for_uri(&self, uri: &MxcUri)
|
||||
-> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// This should not raise an error when the `uri` parameter points to an
|
||||
/// unknown media, and it should return an Ok result in this case.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
|
||||
/// keep media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the current `MediaRetentionPolicy`.
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
///
|
||||
/// If there is already an ongoing cleanup, this is a noop.
|
||||
async fn clean(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Perform database optimizations if any are available, i.e. vacuuming in
|
||||
/// SQLite.
|
||||
///
|
||||
/// **Warning:** this was added to check if SQLite fragmentation was the
|
||||
/// source of performance issues, **DO NOT use in production**.
|
||||
#[doc(hidden)]
|
||||
async fn optimize(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Returns the size of the store in bytes, if known.
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
|
||||
}
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the media cache of the SDK.
|
||||
///
|
||||
/// The main purposes of this trait are to be able to centralize where we handle
|
||||
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
|
||||
/// simplify the implementation of tests by being able to have complete control
|
||||
/// over the `SystemTime`s provided to the store.
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
pub trait MediaStoreInner: AsyncTraitDeps + Clone {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + fmt::Display + Into<MediaStoreError>;
|
||||
|
||||
/// The persisted media retention policy in the media cache.
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
|
||||
|
||||
/// Persist the media retention policy in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to persist.
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Add a media file's content in the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to set the last access time of the
|
||||
/// media.
|
||||
///
|
||||
/// * `policy` - The media retention policy, to check whether the media is
|
||||
/// too big to be cached.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
|
||||
/// for this media. This setting should be persisted alongside the media
|
||||
/// and taken into account whenever the policy is used.
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
current_time: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// If the media of the given request is not found, this should be a noop.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content out of the media cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
///
|
||||
/// * `current_time` - The current time, to update the last access time of
|
||||
/// the media.
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the given policy.
|
||||
///
|
||||
/// For the integration tests, it is expected that content that does not
|
||||
/// pass the last access expiry and max file size criteria will be
|
||||
/// removed first. After that, the remaining cache size should be
|
||||
/// computed to compare against the max cache size criteria.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The media retention policy to use for the cleanup. The
|
||||
/// `cleanup_frequency` will be ignored.
|
||||
///
|
||||
/// * `current_time` - The current time, to be used to check for expired
|
||||
/// content and to be stored as the time of the last media cache cleanup.
|
||||
async fn clean_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// The time of the last media cache cleanup.
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
struct EraseMediaStoreError<T>(T);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<T: fmt::Debug> fmt::Debug for EraseMediaStoreError<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
impl<T: MediaStore> MediaStore for EraseMediaStoreError<T> {
|
||||
type Error = MediaStoreError;
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
&self,
|
||||
from: &MediaRequestParameters,
|
||||
to: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.replace_media_key(from, to).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.0.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clean(&self) -> Result<(), Self::Error> {
|
||||
self.0.clean().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn optimize(&self) -> Result<(), Self::Error> {
|
||||
self.0.optimize().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
|
||||
self.0.get_size().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased [`MediaStore`].
|
||||
pub type DynMediaStore = dyn MediaStore<Error = MediaStoreError>;
|
||||
|
||||
/// A type that can be type-erased into `Arc<dyn MediaStore>`.
|
||||
///
|
||||
/// This trait is not meant to be implemented directly outside
|
||||
/// `matrix-sdk-base`, but it is automatically implemented for everything that
|
||||
/// implements `MediaStore`.
|
||||
pub trait IntoMediaStore {
|
||||
#[doc(hidden)]
|
||||
fn into_media_store(self) -> Arc<DynMediaStore>;
|
||||
}
|
||||
|
||||
impl IntoMediaStore for Arc<DynMediaStore> {
|
||||
fn into_media_store(self) -> Arc<DynMediaStore> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoMediaStore for T
|
||||
where
|
||||
T: MediaStore + Sized + 'static,
|
||||
{
|
||||
fn into_media_store(self) -> Arc<DynMediaStore> {
|
||||
Arc::new(EraseMediaStoreError(self))
|
||||
}
|
||||
}
|
||||
|
||||
// Turns a given `Arc<T>` into `Arc<DynMediaStore>` by attaching the
|
||||
// `MediaStore` impl vtable of `EraseMediaStoreError<T>`.
|
||||
impl<T> IntoMediaStore for Arc<T>
|
||||
where
|
||||
T: MediaStore + 'static,
|
||||
{
|
||||
fn into_media_store(self) -> Arc<DynMediaStore> {
|
||||
let ptr: *const T = Arc::into_raw(self);
|
||||
let ptr_erased = ptr as *const EraseMediaStoreError<T>;
|
||||
// SAFETY: EraseMediaStoreError is repr(transparent) so T and
|
||||
// EraseMediaStoreError<T> have the same layout and ABI
|
||||
unsafe { Arc::from_raw(ptr_erased) }
|
||||
}
|
||||
}
|
||||
@@ -570,7 +570,7 @@ fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool
|
||||
match event {
|
||||
AnySyncMessageLikeEvent::CallAnswer(_)
|
||||
| AnySyncMessageLikeEvent::CallInvite(_)
|
||||
| AnySyncMessageLikeEvent::CallNotify(_)
|
||||
| AnySyncMessageLikeEvent::RtcNotification(_)
|
||||
| AnySyncMessageLikeEvent::CallHangup(_)
|
||||
| AnySyncMessageLikeEvent::CallCandidates(_)
|
||||
| AnySyncMessageLikeEvent::CallNegotiate(_)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Data types used for handling the recently used emojis.
|
||||
//!
|
||||
//! There is no formal spec for this, only the implementation in Element Web:
|
||||
//! <https://github.com/element-hq/element-web/commit/a7f92f35f5a27a53a5a030ea7c471be97751a67a>
|
||||
|
||||
use ruma::{UInt, events::macros::EventContent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An event type containing a list of recently used emojis for reactions.
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
|
||||
#[ruma_event(type = "io.element.recent_emoji", kind = GlobalAccountData)]
|
||||
pub struct RecentEmojisContent {
|
||||
/// The list of recently used emojis, ordered by recency. The tuple of
|
||||
/// `String`, `UInt` values represent the actual emoji and the number of
|
||||
/// times it's been used in total, for those clients that might be
|
||||
/// interested.
|
||||
pub recent_emoji: Vec<(String, UInt)>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
impl RecentEmojisContent {
|
||||
/// Creates a new recent emojis event content given the provided recent
|
||||
/// emojis.
|
||||
pub fn new(recent_emoji: Vec<(String, UInt)>) -> Self {
|
||||
Self { recent_emoji }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::uint;
|
||||
use serde_json::{from_value, json, to_value};
|
||||
|
||||
use crate::recent_emojis::RecentEmojisContent;
|
||||
|
||||
#[test]
|
||||
fn serialization() {
|
||||
let content = RecentEmojisContent::new(vec![
|
||||
("😁".to_owned(), uint!(2)),
|
||||
("🎉".to_owned(), uint!(10)),
|
||||
]);
|
||||
let json = to_value(&content).expect("recent emoji serialization failed");
|
||||
let expected = json!({
|
||||
"recent_emoji": [
|
||||
["😁", 2],
|
||||
["🎉", 10],
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(json, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialization() {
|
||||
let json = json!({
|
||||
"recent_emoji": [
|
||||
["😁", 2],
|
||||
["🎉", 10],
|
||||
]
|
||||
});
|
||||
let content =
|
||||
from_value::<RecentEmojisContent>(json).expect("recent emoji deserialization failed");
|
||||
let expected = RecentEmojisContent::new(vec![
|
||||
("😁".to_owned(), uint!(2)),
|
||||
("🎉".to_owned(), uint!(10)),
|
||||
]);
|
||||
|
||||
assert_eq!(content.recent_emoji, expected.recent_emoji);
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ fn map_info<F: FnOnce(&mut RoomInfo)>(
|
||||
let mut info = room.clone_info();
|
||||
f(&mut info);
|
||||
changes.add_room(info);
|
||||
} else {
|
||||
} else if store.already_logged_missing_room.lock().insert(room_id.to_owned()) {
|
||||
debug!(room = %room_id, "couldn't find room in state changes or store");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_crypto::RoomEventDecryptionResult;
|
||||
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
use ruma::RoomId;
|
||||
|
||||
use super::{super::verification, E2EE};
|
||||
use crate::Result;
|
||||
@@ -26,21 +26,28 @@ use crate::Result;
|
||||
/// application, returns `Err`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
///
|
||||
/// The returned [`TimelineEvent`] has no push actions set up. It's the
|
||||
/// responsibility of the caller to set them.
|
||||
pub async fn sync_timeline_event(
|
||||
e2ee: E2EE<'_>,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
event: &TimelineEvent,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
|
||||
|
||||
Ok(Some(
|
||||
match olm
|
||||
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
|
||||
.try_decrypt_room_event(
|
||||
event.raw().cast_ref_unchecked(),
|
||||
room_id,
|
||||
e2ee.decryption_settings,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
// Note: the push actions are set by the caller.
|
||||
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
|
||||
let timeline_event = event.to_decrypted(decrypted, None);
|
||||
|
||||
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
|
||||
verification::process_if_relevant(&sync_timeline_event, e2ee, room_id).await?;
|
||||
@@ -48,9 +55,7 @@ pub async fn sync_timeline_event(
|
||||
|
||||
timeline_event
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::from_utd(event.clone(), utd_info)
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => event.to_utd(utd_info),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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,204 +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::YesCallNotify(_)
|
||||
| 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.
|
||||
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) => {
|
||||
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,6 @@ use std::collections::BTreeMap;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_common::timer;
|
||||
use ruma::{
|
||||
JsOption, OwnedRoomId, RoomId, UserId,
|
||||
@@ -30,11 +28,10 @@ use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
room::member::{MembershipState, PossiblyRedactedRoomMemberEventContent},
|
||||
},
|
||||
serde::Raw,
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::super::e2ee;
|
||||
@@ -42,13 +39,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::RawSyncStateEventWithKeys,
|
||||
};
|
||||
|
||||
/// Represent any kind of room updates.
|
||||
@@ -70,12 +65,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,7 +74,7 @@ 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;
|
||||
|
||||
@@ -96,12 +87,11 @@ pub async fn update_any_room(
|
||||
#[allow(unused_mut)] // Required for some feature flag combinations
|
||||
let (mut room, mut room_info, maybe_room_update_kind) = membership(
|
||||
context,
|
||||
&state_events,
|
||||
&mut raw_state_events,
|
||||
&invite_state_events,
|
||||
state_store,
|
||||
user_id,
|
||||
room_id,
|
||||
room_info_notable_update_sender,
|
||||
);
|
||||
|
||||
room_info.mark_state_partially_synced();
|
||||
@@ -115,7 +105,7 @@ 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,
|
||||
@@ -132,6 +122,7 @@ pub async fn update_any_room(
|
||||
(&raw_events, &events),
|
||||
&room,
|
||||
&mut room_info,
|
||||
user_id,
|
||||
notification::Notification::new(
|
||||
notification.push_rules,
|
||||
notification.notifications,
|
||||
@@ -154,18 +145,6 @@ pub async fn update_any_room(
|
||||
)
|
||||
.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 +197,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 +215,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],
|
||||
state_events: &mut [RawSyncStateEventWithKeys],
|
||||
invite_state_events: &Option<(Vec<Raw<AnyStrippedStateEvent>>, Vec<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,23 +230,23 @@ 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| {
|
||||
let own_membership_event = state_events.1.iter().find_map(|event| {
|
||||
if let AnyStrippedStateEvent::RoomMember(membership_event) = event
|
||||
&& membership_event.state_key == user_id
|
||||
{
|
||||
return Some(membership_event.content.clone());
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
|
||||
match membership_event {
|
||||
match own_membership_event {
|
||||
// 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(PossiblyRedactedRoomMemberEventContent {
|
||||
membership: 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();
|
||||
@@ -275,11 +260,7 @@ 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();
|
||||
@@ -294,61 +275,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,
|
||||
@@ -399,7 +350,7 @@ fn properties(
|
||||
}
|
||||
|
||||
if let Some(recency_stamp) = &room_response.bump_stamp {
|
||||
let recency_stamp: u64 = (*recency_stamp).into();
|
||||
let recency_stamp = u64::from(*recency_stamp).into();
|
||||
|
||||
if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
|
||||
room_info.update_recency_stamp(recency_stamp);
|
||||
@@ -418,147 +369,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::YesCallNotify(_)
|
||||
| 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!(
|
||||
"Failed to deserialize event as AnySyncTimelineEvent. ID={}",
|
||||
event.event_id().expect("Event has no ID!")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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},
|
||||
};
|
||||
|
||||
@@ -44,17 +43,12 @@ pub async fn update_joined_room(
|
||||
notification: notification::Notification<'_>,
|
||||
#[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,11 +61,11 @@ 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,
|
||||
@@ -155,17 +149,12 @@ pub async fn update_left_room(
|
||||
notification: notification::Notification<'_>,
|
||||
#[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;
|
||||
|
||||
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,11 +162,11 @@ 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 (),
|
||||
@@ -212,17 +201,13 @@ 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<'_>,
|
||||
) -> 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);
|
||||
|
||||
@@ -235,6 +220,7 @@ pub async fn update_invited_room(
|
||||
(&raw_events, &events),
|
||||
&room,
|
||||
&mut room_info,
|
||||
user_id,
|
||||
notification,
|
||||
)
|
||||
.await?;
|
||||
@@ -248,17 +234,13 @@ 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<'_>,
|
||||
) -> 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);
|
||||
|
||||
@@ -271,6 +253,7 @@ pub async fn update_knocked_room(
|
||||
(&raw_events, &events),
|
||||
&room,
|
||||
&mut room_info,
|
||||
user_id,
|
||||
notification,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -14,40 +14,41 @@
|
||||
|
||||
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, warn};
|
||||
|
||||
use super::Context;
|
||||
use crate::store::BaseStateStore;
|
||||
#[cfg(feature = "experimental-encrypted-state-events")]
|
||||
use super::e2ee;
|
||||
use crate::{store::BaseStateStore, utils::RawSyncStateEventWithKeys};
|
||||
|
||||
/// 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::RawSyncStateEventWithKeys,
|
||||
};
|
||||
|
||||
impl State {
|
||||
@@ -58,18 +59,23 @@ pub mod sync {
|
||||
pub(crate) fn collect(
|
||||
&self,
|
||||
timeline: &[Raw<AnySyncTimelineEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
) -> Vec<RawSyncStateEventWithKeys> {
|
||||
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(RawSyncStateEventWithKeys::try_from_raw_state_event)
|
||||
.chain(
|
||||
timeline
|
||||
.iter()
|
||||
.filter_map(RawSyncStateEventWithKeys::try_from_raw_timeline_event),
|
||||
)
|
||||
.collect(),
|
||||
Self::After(events) => events
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(RawSyncStateEventWithKeys::try_from_raw_state_event)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +92,7 @@ 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<RawSyncStateEventWithKeys>,
|
||||
room_info: &mut RoomInfo,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
new_users: &mut U,
|
||||
@@ -96,49 +102,41 @@ pub mod sync {
|
||||
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 +144,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 +164,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 +176,19 @@ pub mod sync {
|
||||
async fn dispatch_room_member<U>(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomMemberEventContent>,
|
||||
raw_event: &mut RawSyncStateEventWithKeys,
|
||||
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,20 +218,60 @@ 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 [RawSyncStateEventWithKeys],
|
||||
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 ruma::{events::AnyStrippedStateEvent, push::Action};
|
||||
use ruma::{RoomId, UserId, events::AnyStrippedStateEvent, push::Action};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{
|
||||
super::{notification, timeline},
|
||||
Context, Raw,
|
||||
};
|
||||
use crate::{Result, Room, RoomInfo};
|
||||
use crate::{Result, Room, RoomInfo, RoomInfoNotableUpdateReasons};
|
||||
|
||||
/// Collect [`Raw<AnyStrippedStateEvent>`] to [`AnyStrippedStateEvent`].
|
||||
pub fn collect(
|
||||
@@ -296,18 +302,22 @@ pub mod stripped {
|
||||
(raw_events, events): (&[Raw<AnyStrippedStateEvent>], &[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);
|
||||
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
}
|
||||
|
||||
own_membership(context, room.room_id(), user_id, events);
|
||||
|
||||
context
|
||||
.state_changes
|
||||
.stripped_state
|
||||
@@ -332,6 +342,36 @@ pub mod stripped {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
state_events: &[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 state_events.iter().rev().any(|event| {
|
||||
// Find the event that updates the current user's membership.
|
||||
if let AnyStrippedStateEvent::RoomMember(member) = &event
|
||||
&& member.state_key.as_str() == user_id.as_str()
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
|
||||
@@ -354,16 +394,22 @@ where
|
||||
/// 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 RawSyncStateEventWithKeys,
|
||||
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 +420,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 +437,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 +463,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 RawSyncStateEventWithKeys,
|
||||
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 +501,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,6 +518,67 @@ 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 RawSyncStateEventWithKeys,
|
||||
room_id: &RoomId,
|
||||
e2ee: &e2ee::E2EE<'_>,
|
||||
) -> Option<RawSyncStateEventWithKeys> {
|
||||
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 RawSyncStateEventWithKeys::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;
|
||||
|
||||
@@ -16,7 +16,7 @@ use matrix_sdk_common::{deserialized_responses::TimelineEvent, timer};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::SyncMessageLikeEvent;
|
||||
use ruma::{
|
||||
UInt, UserId, assign,
|
||||
MilliSecondsSinceUnixEpoch, UInt, UserId, assign,
|
||||
events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent},
|
||||
push::{Action, PushConditionRoomCtx},
|
||||
};
|
||||
@@ -31,6 +31,7 @@ use crate::{Result, Room, RoomInfo, sync::Timeline};
|
||||
///
|
||||
/// For each event:
|
||||
/// - will try to decrypt it,
|
||||
/// - will fix the `origin_server_ts` if considered invalid,
|
||||
/// - will process verification,
|
||||
/// - will process redaction,
|
||||
/// - will process notification.
|
||||
@@ -46,6 +47,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
) -> Result<Timeline> {
|
||||
let _timer = timer!(tracing::Level::TRACE, "build a timeline from sync");
|
||||
|
||||
let now = MilliSecondsSinceUnixEpoch::now();
|
||||
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
|
||||
let mut push_condition_room_ctx = get_push_room_context(context, room, room_info).await?;
|
||||
let room_id = room.room_id();
|
||||
@@ -53,7 +55,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
for raw_event in timeline_inputs.raw_events {
|
||||
// Start by assuming we have a plaintext event. We'll replace it with a
|
||||
// decrypted or UTD event below if necessary.
|
||||
let mut timeline_event = TimelineEvent::from_plaintext(raw_event);
|
||||
let mut timeline_event = TimelineEvent::from_plaintext_with_max_timestamp(raw_event, now);
|
||||
|
||||
// Do some special stuff on the `timeline_event` before collecting it.
|
||||
match timeline_event.raw().deserialize() {
|
||||
@@ -94,7 +96,7 @@ pub async fn build<'notification, 'e2ee>(
|
||||
if let Some(decrypted_timeline_event) =
|
||||
Box::pin(e2ee::decrypt::sync_timeline_event(
|
||||
e2ee.clone(),
|
||||
timeline_event.raw(),
|
||||
&timeline_event,
|
||||
room_id,
|
||||
))
|
||||
.await?
|
||||
|
||||
@@ -25,6 +25,15 @@ use crate::Result;
|
||||
|
||||
/// Process the given event as a verification event if it is a candidate. The
|
||||
/// event must be decrypted.
|
||||
///
|
||||
/// **Note**: If the supplied event is an `m.room.message` event with
|
||||
/// `msgtype: m.key.verification.request`, then the device information for
|
||||
/// the sending user must be up-to-date before calling this method
|
||||
/// (otherwise, the request will be ignored). It is hard to guarantee this
|
||||
/// is the case, but you can maximize your chances by explicitly making a
|
||||
/// request for this user's device info by calling
|
||||
/// [`OlmMachine::query_keys_for_users`], sending the request, and
|
||||
/// processing the response with [`OlmMachine::mark_request_as_sent`].
|
||||
pub async fn process_if_relevant(
|
||||
event: &AnySyncTimelineEvent,
|
||||
e2ee: E2EE<'_>,
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Room {
|
||||
/// Is there a non expired membership with application `m.call` and scope
|
||||
/// `m.room` in this room.
|
||||
pub fn has_active_room_call(&self) -> bool {
|
||||
self.inner.read().has_active_room_call()
|
||||
self.info.read().has_active_room_call()
|
||||
}
|
||||
|
||||
/// Returns a `Vec` of `OwnedUserId`'s that participate in the room call.
|
||||
@@ -32,7 +32,7 @@ impl Room {
|
||||
///
|
||||
/// The vector is ordered by oldest membership user to newest.
|
||||
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
|
||||
self.inner.read().active_room_call_participants()
|
||||
self.info.read().active_room_call_participants()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,25 +41,26 @@ mod tests {
|
||||
use std::{ops::Sub, sync::Arc, time::Duration};
|
||||
|
||||
use assign::assign;
|
||||
use matrix_sdk_test::{ALICE, BOB, CAROL};
|
||||
use matrix_sdk_test::{ALICE, BOB, CAROL, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId, device_id, event_id,
|
||||
events::{
|
||||
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
|
||||
AnySyncStateEvent,
|
||||
call::member::{
|
||||
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
|
||||
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
|
||||
LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
|
||||
LegacyMembershipDataInit, LivekitFocus,
|
||||
},
|
||||
},
|
||||
room_id,
|
||||
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::RawSyncStateEventWithKeys};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
@@ -99,19 +100,17 @@ mod tests {
|
||||
memberships: Vec<LegacyMembershipData>,
|
||||
ev_id: &EventId,
|
||||
user_id: &UserId,
|
||||
) -> AnySyncStateEvent {
|
||||
) -> Raw<AnySyncStateEvent> {
|
||||
let content = CallMemberEventContent::new_legacy(memberships);
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
EventFactory::new()
|
||||
.sender(user_id)
|
||||
.event(content)
|
||||
.state_key(CallMemberStateKey::new(user_id.to_owned(), None, false).as_ref())
|
||||
.event_id(ev_id)
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
.server_ts(timestamp(0))
|
||||
.into()
|
||||
}
|
||||
|
||||
struct InitData<'a> {
|
||||
@@ -123,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,
|
||||
@@ -156,16 +155,15 @@ mod tests {
|
||||
),
|
||||
};
|
||||
|
||||
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
|
||||
content,
|
||||
event_id: ev_id.to_owned(),
|
||||
sender: user_id.to_owned(),
|
||||
EventFactory::new()
|
||||
.sender(user_id)
|
||||
.event(content)
|
||||
.state_key(state_key.as_ref())
|
||||
.event_id(ev_id)
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key,
|
||||
unsigned: StateUnsigned::new(),
|
||||
}))
|
||||
.server_ts(timestamp(0))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn foci_and_application() -> (Application, Vec<Focus>) {
|
||||
@@ -181,11 +179,14 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
room.inner.update_if(|info| {
|
||||
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 RawSyncStateEventWithKeys::try_from_raw_state_event(ev)
|
||||
.expect("generated state event should be valid"),
|
||||
);
|
||||
}
|
||||
res
|
||||
});
|
||||
@@ -210,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
|
||||
}
|
||||
@@ -241,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
|
||||
}
|
||||
@@ -275,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::fmt;
|
||||
use as_variant::as_variant;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
OwnedMxcUri, OwnedUserId, UserId,
|
||||
OwnedMxcUri, OwnedUserId, RoomAliasId, UserId,
|
||||
events::{SyncStateEvent, member_hints::MemberHintsEventContent},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -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)
|
||||
@@ -57,7 +57,36 @@ impl Room {
|
||||
///
|
||||
/// This cache is refilled every time we call [`Self::display_name`].
|
||||
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
|
||||
self.inner.read().cached_display_name.clone()
|
||||
self.info.read().cached_display_name.clone()
|
||||
}
|
||||
|
||||
/// Computes the display name for a room using the provided fields.
|
||||
///
|
||||
/// This function is useful for reusing the same display name computation
|
||||
/// logic where full Rooms aren't available e.g. space summary rooms.
|
||||
pub fn compute_display_name_with_fields(
|
||||
name: Option<String>,
|
||||
canonical_alias: Option<&RoomAliasId>,
|
||||
heroes: Vec<RoomHero>,
|
||||
num_joined_members: u64,
|
||||
) -> RoomDisplayName {
|
||||
// Handle empty string names. The `Room` level implementation relies
|
||||
// on `RoomInfo` doing the same thing.
|
||||
let name = name.and_then(|name| (!name.is_empty()).then_some(name));
|
||||
|
||||
match (name, canonical_alias) {
|
||||
(Some(name), _) => RoomDisplayName::Named(name.trim().to_owned()),
|
||||
(None, Some(alias)) => RoomDisplayName::Aliased(alias.alias().trim().to_owned()),
|
||||
(None, None) => {
|
||||
let hero_display_names =
|
||||
heroes.into_iter().filter_map(|hero| hero.display_name).collect::<Vec<_>>();
|
||||
|
||||
compute_display_name_from_heroes(
|
||||
num_joined_members,
|
||||
hero_display_names.iter().map(|name| name.as_str()).collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Force recalculating a room's display name, taking into account its name,
|
||||
@@ -70,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),
|
||||
@@ -78,7 +107,7 @@ impl Room {
|
||||
}
|
||||
|
||||
let display_name_or_summary = {
|
||||
let inner = self.inner.read();
|
||||
let inner = self.info.read();
|
||||
|
||||
match (inner.name(), inner.canonical_alias()) {
|
||||
(Some(name), _) => {
|
||||
@@ -107,7 +136,7 @@ impl Room {
|
||||
// Update the cached display name before we return the newly computed value.
|
||||
let mut updated = false;
|
||||
|
||||
self.inner.update_if(|info| {
|
||||
self.info.update_if(|info| {
|
||||
if info.cached_display_name.as_ref() != Some(&display_name) {
|
||||
info.cached_display_name = Some(display_name.clone());
|
||||
updated = true;
|
||||
@@ -378,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
|
||||
@@ -515,9 +544,11 @@ 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,
|
||||
@@ -528,8 +559,7 @@ mod tests {
|
||||
|
||||
use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
|
||||
use crate::{
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomState, StateChanges, StateStore,
|
||||
store::MemoryStore,
|
||||
MinimalStateEvent, RoomHero, RoomState, StateChanges, StateStore, store::MemoryStore,
|
||||
};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
@@ -555,19 +585,23 @@ mod tests {
|
||||
}
|
||||
|
||||
fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
|
||||
MinimalStateEvent::Original(OriginalMinimalStateEvent {
|
||||
content: assign!(RoomCanonicalAliasEventContent::new(), {
|
||||
MinimalStateEvent {
|
||||
content: assign!(PossiblyRedactedRoomCanonicalAliasEventContent::new(), {
|
||||
alias: Some(room_alias_id!("#test:example.com").to_owned()),
|
||||
}),
|
||||
event_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
|
||||
MinimalStateEvent::Original(OriginalMinimalStateEvent {
|
||||
content: RoomNameEventContent::new("Test Room".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<PossiblyRedactedRoomNameEventContent> {
|
||||
make_name_event_with("Test Room")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -576,10 +610,34 @@ mod tests {
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_empty() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(None, None, vec![], 0),
|
||||
RoomDisplayName::Empty
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_is_empty_if_name_empty() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.info.update(|info| info.base_info.name = Some(make_name_event_with("")));
|
||||
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_empty_if_name_empty() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(Some("".to_owned()), None, vec![], 0),
|
||||
RoomDisplayName::Empty
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -587,16 +645,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_alias() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(
|
||||
None,
|
||||
Some(room_alias_id!("#test:example.com")),
|
||||
vec![],
|
||||
0,
|
||||
),
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_joined_room_prefers_name_over_alias() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Joined);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
room.info.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
// Display name wasn't cached when we asked for it above, and name overrides
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -604,6 +675,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_name_over_alias() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(
|
||||
Some("Test Room".to_owned()),
|
||||
Some(room_alias_id!("#test:example.com")),
|
||||
vec![],
|
||||
0
|
||||
),
|
||||
RoomDisplayName::Named("Test Room".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
@@ -614,11 +698,8 @@ 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,
|
||||
});
|
||||
room.inner.update(|info| info.base_info.name = Some(room_name));
|
||||
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);
|
||||
}
|
||||
@@ -626,7 +707,7 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -637,13 +718,13 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_display_name_for_invited_room_prefers_name_over_alias() {
|
||||
let (_, room) = make_room_test_helper(RoomState::Invited);
|
||||
room.inner
|
||||
room.info
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Aliased("test".to_owned())
|
||||
);
|
||||
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
room.info.update(|info| info.base_info.name = Some(make_name_event()));
|
||||
// Display name wasn't cached when we asked for it above, and name overrides
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -670,7 +751,7 @@ mod tests {
|
||||
changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
@@ -720,12 +801,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::Calculated("Matthew".to_owned())
|
||||
@@ -755,12 +836,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
|
||||
|
||||
let member_hints_content =
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
|
||||
changes
|
||||
.state
|
||||
.entry(room_id.to_owned())
|
||||
@@ -771,7 +852,7 @@ mod tests {
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
// Bot should not contribute to the display name.
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -801,11 +882,11 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
|
||||
|
||||
let member_hints_content =
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
|
||||
changes
|
||||
.state
|
||||
.entry(room_id.to_owned())
|
||||
@@ -816,7 +897,7 @@ mod tests {
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
// Bot should not contribute to the display name.
|
||||
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
|
||||
}
|
||||
@@ -837,8 +918,8 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
@@ -867,12 +948,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
|
||||
|
||||
let member_hints_content =
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
|
||||
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
|
||||
changes
|
||||
.state
|
||||
.entry(room_id.to_owned())
|
||||
@@ -914,10 +995,10 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -928,9 +1009,9 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into());
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -938,7 +1019,7 @@ mod tests {
|
||||
joined_member_count: Some(7u32.into()),
|
||||
heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
|
||||
});
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
@@ -946,6 +1027,47 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_compute_fields_name_deterministic() {
|
||||
assert_eq!(
|
||||
Room::compute_display_name_with_fields(
|
||||
None,
|
||||
None,
|
||||
vec![
|
||||
RoomHero {
|
||||
user_id: user_id!("@alice:example.org").to_owned(),
|
||||
display_name: Some("Alice".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@bob:example.org").to_owned(),
|
||||
display_name: Some("Bob".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@carol:example.org").to_owned(),
|
||||
display_name: Some("Carol".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@denis:example.org").to_owned(),
|
||||
display_name: Some("Denis".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
RoomHero {
|
||||
user_id: user_id!("@erica:example.org").to_owned(),
|
||||
display_name: Some("Erica".to_owned()),
|
||||
avatar_url: None,
|
||||
},
|
||||
],
|
||||
1234,
|
||||
),
|
||||
RoomDisplayName::Calculated(
|
||||
"Alice, Bob, Carol, Denis, Erica, and 1229 others".to_owned()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_display_name_deterministic_no_heroes() {
|
||||
let (store, room) = make_room_test_helper(RoomState::Joined);
|
||||
@@ -971,10 +1093,10 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(carol.into(), f.member(carol).display_name("Carol").into());
|
||||
members.insert(bob.into(), f.member(bob).display_name("Bob").into());
|
||||
members.insert(fred.into(), f.member(fred).display_name("Fred").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
@@ -986,9 +1108,9 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
|
||||
members.insert(alice.into(), f.member(alice).display_name("Alice").into());
|
||||
members.insert(erica.into(), f.member(erica).display_name("Erica").into());
|
||||
members.insert(denis.into(), f.member(denis).display_name("Denis").into());
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -1018,12 +1140,12 @@ mod tests {
|
||||
.or_default()
|
||||
.entry(StateEventType::RoomMember)
|
||||
.or_default();
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
|
||||
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
|
||||
members.insert(me.into(), f.member(me).display_name("Me").into());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
|
||||
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
|
||||
assert_eq!(
|
||||
room.compute_display_name().await.unwrap().into_inner(),
|
||||
RoomDisplayName::EmptyWas("Matthew".to_owned())
|
||||
|
||||
@@ -19,13 +19,13 @@ use super::Room;
|
||||
impl Room {
|
||||
/// Get the encryption state of this room.
|
||||
pub fn encryption_state(&self) -> EncryptionState {
|
||||
self.inner.read().encryption_state()
|
||||
self.info.read().encryption_state()
|
||||
}
|
||||
|
||||
/// Get the `m.room.encryption` content that enabled end to end encryption
|
||||
/// in the room.
|
||||
pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
|
||||
self.inner.read().base_info.encryption.clone()
|
||||
self.info.read().base_info.encryption.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,26 +80,23 @@ impl EncryptionState {
|
||||
mod tests {
|
||||
use std::{
|
||||
ops::{Not, Sub},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::ALICE;
|
||||
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||
events::{
|
||||
AnySyncStateEvent, EmptyStateKey, StateUnsigned, SyncStateEvent,
|
||||
room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
|
||||
},
|
||||
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, event_id,
|
||||
events::{AnySyncStateEvent, room::encryption::RoomEncryptionEventContent},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
time::SystemTime,
|
||||
user_id,
|
||||
};
|
||||
|
||||
use super::{EncryptionState, Room};
|
||||
use crate::{RoomState, store::MemoryStore};
|
||||
use crate::{RoomState, store::MemoryStore, utils::RawSyncStateEventWithKeys};
|
||||
|
||||
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
@@ -117,11 +114,14 @@ mod tests {
|
||||
.expect("date out of range")
|
||||
}
|
||||
|
||||
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
|
||||
room.inner.update_if(|info| {
|
||||
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 RawSyncStateEventWithKeys::try_from_raw_state_event(ev)
|
||||
.expect("generated state event should be valid"),
|
||||
);
|
||||
}
|
||||
res
|
||||
});
|
||||
@@ -135,19 +135,16 @@ mod tests {
|
||||
|
||||
let encryption_content =
|
||||
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
|
||||
OriginalSyncRoomEncryptionEvent {
|
||||
content: encryption_content,
|
||||
event_id: OwnedEventId::from_str("$1234_1").unwrap(),
|
||||
sender: ALICE.to_owned(),
|
||||
// we can simply use now here since this will be dropped when using a
|
||||
// MinimalStateEvent in the roomInfo
|
||||
origin_server_ts: timestamp(0),
|
||||
state_key: EmptyStateKey,
|
||||
unsigned: StateUnsigned::new(),
|
||||
},
|
||||
));
|
||||
receive_state_events(&room, vec![&encryption_event]);
|
||||
let encryption_event = EventFactory::new()
|
||||
.sender(*ALICE)
|
||||
.event(encryption_content)
|
||||
.state_key("")
|
||||
.event_id(event_id!("$1234_1"))
|
||||
// we can simply use now here since this will be dropped when using a MinimalStateEvent
|
||||
// in the roomInfo
|
||||
.server_ts(timestamp(0))
|
||||
.into();
|
||||
receive_state_events(&room, vec![encryption_event]);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
|
||||
}
|
||||
@@ -157,7 +154,7 @@ mod tests {
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
|
||||
room.inner.update_if(|info| {
|
||||
room.info.update_if(|info| {
|
||||
info.mark_encryption_state_synced();
|
||||
|
||||
false
|
||||
|
||||
@@ -12,283 +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};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw};
|
||||
use ruma::MilliSecondsSinceUnixEpoch;
|
||||
|
||||
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.inner.read().latest_event.as_deref().cloned()
|
||||
}
|
||||
|
||||
/// Return the [`LatestEventValue`] of this room.
|
||||
pub fn new_latest_event(&self) -> LatestEventValue {
|
||||
self.inner.read().new_latest_event.clone()
|
||||
///
|
||||
/// Note that it clones the [`LatestEventValue`]! This can add pressure
|
||||
/// on the memory if used in a hot path.
|
||||
pub fn latest_event(&self) -> LatestEventValue {
|
||||
self.info.read().latest_event_value.clone()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
/// Return the value of [`LatestEventValue::timestamp`].
|
||||
pub fn latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
|
||||
self.info.read().latest_event_value.timestamp()
|
||||
}
|
||||
|
||||
/// 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.inner.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ use std::{
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use futures_util::future;
|
||||
use ruma::{
|
||||
MxcUri, OwnedUserId, UserId,
|
||||
Int, MxcUri, OwnedUserId, UserId,
|
||||
events::{
|
||||
MessageLikeEventType, StateEventType,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
@@ -35,7 +36,7 @@ use tracing::debug;
|
||||
|
||||
use super::Room;
|
||||
use crate::{
|
||||
MinimalRoomMemberEvent,
|
||||
MinimalRoomMemberEvent, StoreError,
|
||||
deserialized_responses::{DisplayName, MemberEvent},
|
||||
store::{Result as StoreResult, StateStoreExt, ambiguity_map::is_display_name_ambiguous},
|
||||
};
|
||||
@@ -48,7 +49,7 @@ impl Room {
|
||||
///
|
||||
/// Returns true if no members are missing, false otherwise.
|
||||
pub fn are_members_synced(&self) -> bool {
|
||||
self.inner.read().members_synced
|
||||
self.info.read().members_synced
|
||||
}
|
||||
|
||||
/// Mark this Room as holding all member information.
|
||||
@@ -57,14 +58,14 @@ impl Room {
|
||||
/// about its members.
|
||||
#[cfg(feature = "testing")]
|
||||
pub fn mark_members_synced(&self) {
|
||||
self.inner.update(|info| {
|
||||
self.info.update(|info| {
|
||||
info.members_synced = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Mark this Room as still missing member information.
|
||||
pub fn mark_members_missing(&self) {
|
||||
self.inner.update_if(|info| {
|
||||
self.info.update_if(|info| {
|
||||
// notify observable subscribers only if the previous value was false
|
||||
mem::replace(&mut info.members_synced, false)
|
||||
})
|
||||
@@ -119,17 +120,17 @@ impl Room {
|
||||
/// Returns the number of members who have joined or been invited to the
|
||||
/// room.
|
||||
pub fn active_members_count(&self) -> u64 {
|
||||
self.inner.read().active_members_count()
|
||||
self.info.read().active_members_count()
|
||||
}
|
||||
|
||||
/// Returns the number of members who have been invited to the room.
|
||||
pub fn invited_members_count(&self) -> u64 {
|
||||
self.inner.read().invited_members_count()
|
||||
self.info.read().invited_members_count()
|
||||
}
|
||||
|
||||
/// Returns the number of members who have joined the room.
|
||||
pub fn joined_members_count(&self) -> u64 {
|
||||
self.inner.read().joined_members_count()
|
||||
self.info.read().joined_members_count()
|
||||
}
|
||||
|
||||
/// Get the `RoomMember` with the given `user_id`.
|
||||
@@ -140,17 +141,26 @@ impl Room {
|
||||
///
|
||||
/// Async because it can read from storage.
|
||||
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
|
||||
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
|
||||
debug!(%user_id, "Member event not found in state store");
|
||||
return Ok(None);
|
||||
let event = async {
|
||||
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await?
|
||||
else {
|
||||
debug!(%user_id, "Member event not found in state store");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(raw_event.deserialize()?))
|
||||
};
|
||||
let presence = async {
|
||||
let raw_event = self.store.get_presence_event(user_id).await?;
|
||||
Ok::<Option<PresenceEvent>, StoreError>(raw_event.and_then(|e| e.deserialize().ok()))
|
||||
};
|
||||
|
||||
let event = raw_event.deserialize()?;
|
||||
let profile = async { self.store.get_profile(self.room_id(), user_id).await };
|
||||
|
||||
let presence =
|
||||
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
|
||||
|
||||
let profile = self.store.get_profile(self.room_id(), user_id).await?;
|
||||
let (Some(event), presence, profile) = future::try_join3(event, presence, profile).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let display_names = [event.display_name()];
|
||||
let room_info = self.member_room_info(&display_names).await?;
|
||||
@@ -166,18 +176,23 @@ impl Room {
|
||||
display_names: &'a [DisplayName],
|
||||
) -> StoreResult<MemberRoomInfo<'a>> {
|
||||
let max_power_level = self.max_power_level();
|
||||
let power_levels = self.power_levels_or_default().await;
|
||||
let power_levels = async { Ok(self.power_levels_or_default().await) };
|
||||
|
||||
let users_display_names =
|
||||
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
|
||||
self.store.get_users_with_display_names(self.room_id(), display_names);
|
||||
|
||||
let ignored_users = self
|
||||
.store
|
||||
.get_account_data_event_static::<IgnoredUserListEventContent>()
|
||||
.await?
|
||||
.map(|c| c.deserialize())
|
||||
.transpose()?
|
||||
.map(|e| e.content.ignored_users.into_keys().collect());
|
||||
let ignored_users = async {
|
||||
Ok(self
|
||||
.store
|
||||
.get_account_data_event_static::<IgnoredUserListEventContent>()
|
||||
.await?
|
||||
.map(|c| c.deserialize())
|
||||
.transpose()?
|
||||
.map(|e| e.content.ignored_users.into_keys().collect()))
|
||||
};
|
||||
|
||||
let (power_levels, users_display_names, ignored_users) =
|
||||
future::try_join3(power_levels, users_display_names, ignored_users).await?;
|
||||
|
||||
Ok(MemberRoomInfo {
|
||||
power_levels: power_levels.into(),
|
||||
@@ -244,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,15 +292,13 @@ impl RoomMember {
|
||||
return UserPowerLevel::Infinite;
|
||||
};
|
||||
|
||||
let mut power_level = i64::from(power_level);
|
||||
let normalized_power_level = if self.max_power_level > 0 {
|
||||
normalize_power_level(power_level, self.max_power_level)
|
||||
} else {
|
||||
power_level
|
||||
};
|
||||
|
||||
if self.max_power_level > 0 {
|
||||
power_level = (power_level * 100) / self.max_power_level;
|
||||
}
|
||||
|
||||
UserPowerLevel::Int(
|
||||
power_level.try_into().expect("normalized power level should fit in Int"),
|
||||
)
|
||||
UserPowerLevel::Int(normalized_power_level)
|
||||
}
|
||||
|
||||
/// Get the power level of this member.
|
||||
@@ -468,3 +481,64 @@ impl RoomMemberships {
|
||||
memberships
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale the given `power_level` to a range between 0-100.
|
||||
pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int {
|
||||
let mut power_level = i64::from(power_level);
|
||||
power_level = (power_level * 100) / max_power_level;
|
||||
|
||||
Int::try_from(power_level.clamp(0, 100))
|
||||
.expect("We clamped the normalized power level so they must fit into the Int")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
prop_compose! {
|
||||
fn arb_int()(id in any::<i64>()) -> Int {
|
||||
id.try_into().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||
#[test]
|
||||
fn test_power_level_normalization_with_min_max_level(power_level in arb_int()) {
|
||||
let normalized = normalize_power_level(power_level, 1);
|
||||
let normalized = i64::from(normalized);
|
||||
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||
#[test]
|
||||
fn test_power_level_normalization(power_level in arb_int(), max_level in 1i64..) {
|
||||
let normalized = normalize_power_level(power_level, max_level);
|
||||
let normalized = i64::from(normalized);
|
||||
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_level_normalization_limits() {
|
||||
let level = Int::MIN;
|
||||
let normalized = normalize_power_level(level, 1);
|
||||
let normalized = i64::from(normalized);
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
|
||||
let level = Int::MAX;
|
||||
let normalized = normalize_power_level(level, 1);
|
||||
let normalized = i64::from(normalized);
|
||||
assert!(normalized >= 0);
|
||||
assert!(normalized <= 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ mod state;
|
||||
mod tags;
|
||||
mod tombstone;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::RwLock as SyncRwLock;
|
||||
use std::{
|
||||
collections::{BTreeMap, 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, apply_redaction,
|
||||
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,
|
||||
@@ -91,21 +85,15 @@ pub struct Room {
|
||||
/// Our own user ID.
|
||||
pub(super) own_user_id: OwnedUserId,
|
||||
|
||||
pub(super) inner: SharedObservable<RoomInfo>,
|
||||
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
pub(super) store: Arc<DynStateStore>,
|
||||
pub(super) info: SharedObservable<RoomInfo>,
|
||||
|
||||
/// 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
|
||||
@@ -140,11 +128,7 @@ impl Room {
|
||||
own_user_id: own_user_id.into(),
|
||||
room_id: room_info.room_id.clone(),
|
||||
store,
|
||||
inner: SharedObservable::new(room_info),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
|
||||
Self::MAX_ENCRYPTED_EVENTS,
|
||||
))),
|
||||
info: SharedObservable::new(room_info),
|
||||
room_info_notable_update_sender,
|
||||
seen_knock_request_ids_map: SharedObservable::new_async(None),
|
||||
room_member_updates_sender,
|
||||
@@ -158,7 +142,7 @@ impl Room {
|
||||
|
||||
/// Get a copy of the room creators.
|
||||
pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
|
||||
self.inner.read().creators()
|
||||
self.info.read().creators()
|
||||
}
|
||||
|
||||
/// Get our own user id.
|
||||
@@ -168,18 +152,18 @@ impl Room {
|
||||
|
||||
/// Whether this room's [`RoomType`] is `m.space`.
|
||||
pub fn is_space(&self) -> bool {
|
||||
self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
|
||||
self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
|
||||
}
|
||||
|
||||
/// Returns the room's type as defined in its creation event
|
||||
/// (`m.room.create`).
|
||||
pub fn room_type(&self) -> Option<RoomType> {
|
||||
self.inner.read().room_type().map(ToOwned::to_owned)
|
||||
self.info.read().room_type().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the unread notification counts.
|
||||
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
|
||||
self.inner.read().notification_counts
|
||||
self.info.read().notification_counts
|
||||
}
|
||||
|
||||
/// Get the number of unread messages (computed client-side).
|
||||
@@ -187,12 +171,12 @@ impl Room {
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_messages(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_unread
|
||||
self.info.read().read_receipts.num_unread
|
||||
}
|
||||
|
||||
/// Get the detailed information about read receipts for the room.
|
||||
pub fn read_receipts(&self) -> RoomReadReceipts {
|
||||
self.inner.read().read_receipts.clone()
|
||||
self.info.read().read_receipts.clone()
|
||||
}
|
||||
|
||||
/// Get the number of unread notifications (computed client-side).
|
||||
@@ -200,7 +184,7 @@ impl Room {
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_notifications(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_notifications
|
||||
self.info.read().read_receipts.num_notifications
|
||||
}
|
||||
|
||||
/// Get the number of unread mentions (computed client-side), that is,
|
||||
@@ -209,7 +193,7 @@ impl Room {
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
/// encrypted rooms.
|
||||
pub fn num_unread_mentions(&self) -> u64 {
|
||||
self.inner.read().read_receipts.num_mentions
|
||||
self.info.read().read_receipts.num_mentions
|
||||
}
|
||||
|
||||
/// Check if the room states have been synced
|
||||
@@ -220,40 +204,40 @@ impl Room {
|
||||
///
|
||||
/// Returns true if the state is fully synced, false otherwise.
|
||||
pub fn is_state_fully_synced(&self) -> bool {
|
||||
self.inner.read().sync_info == SyncInfo::FullySynced
|
||||
self.info.read().sync_info == SyncInfo::FullySynced
|
||||
}
|
||||
|
||||
/// Check if the room state has been at least partially synced.
|
||||
///
|
||||
/// See [`Room::is_state_fully_synced`] for more info.
|
||||
pub fn is_state_partially_or_fully_synced(&self) -> bool {
|
||||
self.inner.read().sync_info != SyncInfo::NoState
|
||||
self.info.read().sync_info != SyncInfo::NoState
|
||||
}
|
||||
|
||||
/// Get the `prev_batch` token that was received from the last sync. May be
|
||||
/// `None` if the last sync contained the full room history.
|
||||
pub fn last_prev_batch(&self) -> Option<String> {
|
||||
self.inner.read().last_prev_batch.clone()
|
||||
self.info.read().last_prev_batch.clone()
|
||||
}
|
||||
|
||||
/// Get the avatar url of this room.
|
||||
pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
|
||||
self.inner.read().avatar_url().map(ToOwned::to_owned)
|
||||
self.info.read().avatar_url().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get information about the avatar of this room.
|
||||
pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
|
||||
self.inner.read().avatar_info().map(ToOwned::to_owned)
|
||||
self.info.read().avatar_info().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the canonical alias of this room.
|
||||
pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
|
||||
self.inner.read().canonical_alias().map(ToOwned::to_owned)
|
||||
self.info.read().canonical_alias().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the canonical alias of this room.
|
||||
pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
|
||||
self.inner.read().alt_aliases().to_owned()
|
||||
self.info.read().alt_aliases().to_owned()
|
||||
}
|
||||
|
||||
/// Get the `m.room.create` content of this room.
|
||||
@@ -266,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.inner.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.
|
||||
@@ -279,7 +260,7 @@ impl Room {
|
||||
pub async fn is_direct(&self) -> StoreResult<bool> {
|
||||
match self.state() {
|
||||
RoomState::Joined | RoomState::Left | RoomState::Banned => {
|
||||
Ok(!self.inner.read().base_info.dm_targets.is_empty())
|
||||
Ok(!self.info.read().base_info.dm_targets.is_empty())
|
||||
}
|
||||
|
||||
RoomState::Invited => {
|
||||
@@ -316,41 +297,41 @@ impl Room {
|
||||
/// us to re-find a DM with a user even if they have left, since we may
|
||||
/// want to re-invite them.
|
||||
pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
|
||||
self.inner.read().base_info.dm_targets.clone()
|
||||
self.info.read().base_info.dm_targets.clone()
|
||||
}
|
||||
|
||||
/// If this room is a direct message, returns the number of members that
|
||||
/// we're sharing the room with.
|
||||
pub fn direct_targets_length(&self) -> usize {
|
||||
self.inner.read().base_info.dm_targets.len()
|
||||
self.info.read().base_info.dm_targets.len()
|
||||
}
|
||||
|
||||
/// Get the guest access policy of this room.
|
||||
pub fn guest_access(&self) -> GuestAccess {
|
||||
self.inner.read().guest_access().clone()
|
||||
self.info.read().guest_access().clone()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room.
|
||||
pub fn history_visibility(&self) -> Option<HistoryVisibility> {
|
||||
self.inner.read().history_visibility().cloned()
|
||||
self.info.read().history_visibility().cloned()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room, or a sensible default if
|
||||
/// the event is missing.
|
||||
pub fn history_visibility_or_default(&self) -> HistoryVisibility {
|
||||
self.inner.read().history_visibility_or_default().clone()
|
||||
self.info.read().history_visibility_or_default().clone()
|
||||
}
|
||||
|
||||
/// Is the room considered to be public.
|
||||
///
|
||||
/// May return `None` if the join rule event is not available.
|
||||
pub fn is_public(&self) -> Option<bool> {
|
||||
self.inner.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
|
||||
self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
|
||||
}
|
||||
|
||||
/// Get the join rule policy of this room, if available.
|
||||
pub fn join_rule(&self) -> Option<JoinRule> {
|
||||
self.inner.read().join_rule().cloned()
|
||||
self.info.read().join_rule().cloned()
|
||||
}
|
||||
|
||||
/// Get the maximum power level that this room contains.
|
||||
@@ -358,7 +339,7 @@ impl Room {
|
||||
/// This is useful if one wishes to normalize the power levels, e.g. from
|
||||
/// 0-100 where 100 would be the max power level.
|
||||
pub fn max_power_level(&self) -> i64 {
|
||||
self.inner.read().base_info.max_power_level
|
||||
self.info.read().base_info.max_power_level
|
||||
}
|
||||
|
||||
/// Get the current power levels of this room.
|
||||
@@ -370,7 +351,7 @@ impl Room {
|
||||
.ok_or(Error::InsufficientData)?
|
||||
.deserialize()?;
|
||||
let creators = self.creators().ok_or(Error::InsufficientData)?;
|
||||
let rules = self.inner.read().room_version_rules_or_default();
|
||||
let rules = self.info.read().room_version_rules_or_default();
|
||||
|
||||
Ok(power_levels_content.power_levels(&rules.authorization, creators))
|
||||
}
|
||||
@@ -383,7 +364,7 @@ impl Room {
|
||||
}
|
||||
|
||||
// As a fallback, create the default power levels of a room.
|
||||
let rules = self.inner.read().room_version_rules_or_default();
|
||||
let rules = self.info.read().room_version_rules_or_default();
|
||||
RoomPowerLevels::new(
|
||||
RoomPowerLevelsSource::None,
|
||||
&rules.authorization,
|
||||
@@ -396,12 +377,12 @@ impl Room {
|
||||
/// The returned string may be empty if the event has been redacted, or it's
|
||||
/// missing from storage.
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.inner.read().name().map(ToOwned::to_owned)
|
||||
self.info.read().name().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Get the topic of the room.
|
||||
pub fn topic(&self) -> Option<String> {
|
||||
self.inner.read().topic().map(ToOwned::to_owned)
|
||||
self.info.read().topic().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Update the cached user defined notification mode.
|
||||
@@ -410,7 +391,7 @@ impl Room {
|
||||
/// cached result can be retrieved in
|
||||
/// [`Self::cached_user_defined_notification_mode`].
|
||||
pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
|
||||
self.inner.update_if(|info| {
|
||||
self.info.update_if(|info| {
|
||||
if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
|
||||
info.cached_user_defined_notification_mode = Some(mode);
|
||||
|
||||
@@ -426,7 +407,20 @@ impl Room {
|
||||
/// This cache is refilled every time we call
|
||||
/// [`Self::update_cached_user_defined_notification_mode`].
|
||||
pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
|
||||
self.inner.read().cached_user_defined_notification_mode
|
||||
self.info.read().cached_user_defined_notification_mode
|
||||
}
|
||||
|
||||
/// Removes any existing cached value for the user defined notification
|
||||
/// mode.
|
||||
pub fn clear_user_defined_notification_mode(&self) {
|
||||
self.info.update_if(|info| {
|
||||
if info.cached_user_defined_notification_mode.is_some() {
|
||||
info.cached_user_defined_notification_mode = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the list of users ids that are considered to be joined members of
|
||||
@@ -437,7 +431,7 @@ impl Room {
|
||||
|
||||
/// Get the heroes for this room.
|
||||
pub fn heroes(&self) -> Vec<RoomHero> {
|
||||
self.inner.read().heroes().to_vec()
|
||||
self.info.read().heroes().to_vec()
|
||||
}
|
||||
|
||||
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
|
||||
@@ -468,19 +462,19 @@ impl Room {
|
||||
/// Returns a boolean indicating if this room has been manually marked as
|
||||
/// unread
|
||||
pub fn is_marked_unread(&self) -> bool {
|
||||
self.inner.read().base_info.is_marked_unread
|
||||
self.info.read().base_info.is_marked_unread
|
||||
}
|
||||
|
||||
/// Returns the [`RoomVersionId`] of the room, if known.
|
||||
pub fn version(&self) -> Option<RoomVersionId> {
|
||||
self.inner.read().room_version().cloned()
|
||||
self.info.read().room_version().cloned()
|
||||
}
|
||||
|
||||
/// Returns the recency stamp of the room.
|
||||
///
|
||||
/// Please read `RoomInfo::recency_stamp` to learn more.
|
||||
pub fn recency_stamp(&self) -> Option<u64> {
|
||||
self.inner.read().recency_stamp
|
||||
pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
|
||||
self.info.read().recency_stamp
|
||||
}
|
||||
|
||||
/// Returns the details about an invite to this room if the invite has been
|
||||
@@ -491,20 +485,20 @@ impl Room {
|
||||
/// - `None` if we didn't join this room using an invite or the invite
|
||||
/// wasn't accepted by this client.
|
||||
pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
|
||||
self.inner.read().invite_acceptance_details.clone()
|
||||
self.info.read().invite_acceptance_details.clone()
|
||||
}
|
||||
|
||||
/// Get a `Stream` of loaded pinned events for this room.
|
||||
/// If no pinned events are found a single empty `Vec` will be returned.
|
||||
pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
|
||||
self.inner
|
||||
self.info
|
||||
.subscribe()
|
||||
.map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns the current pinned event ids for this room.
|
||||
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
|
||||
self.inner.read().pinned_event_ids()
|
||||
self.info.read().pinned_event_ids()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user