Compare commits
744 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e938df90d | |||
| 5d55bb4955 | |||
| 8abd9fb303 | |||
| d5ee644443 | |||
| 06022aa23a | |||
| 8ea0d9542d | |||
| 63938bf2c7 | |||
| acff6c2e1d | |||
| 1013843071 | |||
| a0bfd3e21e | |||
| 68352e8339 | |||
| 6a5e24a64a | |||
| db6d1b4cd6 | |||
| b0c1eda682 | |||
| afecd6d508 | |||
| 52b490f5b6 | |||
| 097558ca1b | |||
| 2194a81d74 | |||
| 37146da27a | |||
| 9300f47b40 | |||
| 52f0aafb1e | |||
| bfbbe89989 | |||
| d25e236a75 | |||
| ab90c1b945 | |||
| 005e506c9b | |||
| 3fe457db83 | |||
| 69ab855efa | |||
| 66c7ba60a9 | |||
| 3e320b8289 | |||
| 2922389037 | |||
| 68e3cdebdd | |||
| 511cf78d51 | |||
| f4eea708fa | |||
| 1d3107ebcb | |||
| 073b4bae03 | |||
| 35e13b8730 | |||
| f266fb9c38 | |||
| 2dfd334ade | |||
| 89d0cc5a76 | |||
| 628440632d | |||
| 16a3d9d78b | |||
| d89a7d6c18 | |||
| 87c70789fe | |||
| 9cde7f46bc | |||
| 3e2b95cdb9 | |||
| dc3bd69af1 | |||
| da14cef6c1 | |||
| a1ac363383 | |||
| 9e5ef57a5f | |||
| da86552648 | |||
| c0f4e90965 | |||
| cf393bf8e1 | |||
| 2ee0e175fa | |||
| 5c011a8400 | |||
| b6b0c556b9 | |||
| 0c5f0b8d26 | |||
| e7e15ca280 | |||
| 222cffd502 | |||
| cbef772eaa | |||
| e7e9c7bcf2 | |||
| f1ea3e64d0 | |||
| 257deb4b94 | |||
| 3ece8e62b5 | |||
| f7f07e7389 | |||
| 8d0928ff7c | |||
| 39212db9ce | |||
| 056d4a79d0 | |||
| 5753ca3a64 | |||
| f3291d15c5 | |||
| 413070aecb | |||
| 288251b1b5 | |||
| 6c3e3bb519 | |||
| 2e8b396c09 | |||
| ed048af903 | |||
| 28e475b1fc | |||
| 5a13bd5e76 | |||
| 6966302467 | |||
| fd6ce02d70 | |||
| 2debfd4c4d | |||
| 19c40fd2da | |||
| d5d0368ba8 | |||
| 899eb04f05 | |||
| 187280d573 | |||
| 036d14e9e3 | |||
| 226229d63b | |||
| 0cf018cc1b | |||
| c3d7a760f7 | |||
| d51cf1e76e | |||
| 71b6b213c4 | |||
| ec23638567 | |||
| 688a56a077 | |||
| 8413e856fe | |||
| 3f16e77686 | |||
| a4779299f6 | |||
| 50934f5bc9 | |||
| fad63b9a64 | |||
| d999cf9180 | |||
| 3cd60c5b01 | |||
| 2e4587f824 | |||
| 3996f7c0d6 | |||
| 774bff00a0 | |||
| d6196e6c5c | |||
| b49fd2b473 | |||
| f31119a013 | |||
| fb4caf40aa | |||
| 238fbdbe82 | |||
| a1d42cdf06 | |||
| 1c134a78de | |||
| b5d1c14e29 | |||
| a28ec70816 | |||
| e1b393c39f | |||
| a345c47a31 | |||
| 64feee41ef | |||
| 9f947e019f | |||
| be74cb4a16 | |||
| 409f08dc2b | |||
| a94a03766b | |||
| 988fd18b78 | |||
| 68b848602a | |||
| f7d6fe2dbf | |||
| c2a9523cbb | |||
| ee879354b7 | |||
| c3fd571623 | |||
| 52ec6a4539 | |||
| a57322466c | |||
| 90ce6e85ad | |||
| e94fd64276 | |||
| 0c7cf58d4d | |||
| e3b2e0fa3e | |||
| 4619221429 | |||
| 9b316ed405 | |||
| 0a633ca75c | |||
| 2e57733f05 | |||
| b94be8d509 | |||
| 24e6d780fc | |||
| d9157e5b83 | |||
| b4a8089b40 | |||
| a736dc9f96 | |||
| dd094ea38e | |||
| b2cd81a992 | |||
| d5ceb5f99a | |||
| 956386a3ed | |||
| b2a4032432 | |||
| 38378f7bae | |||
| e12264bcb6 | |||
| 385df955c3 | |||
| 47a1db9e16 | |||
| ed82f07d7d | |||
| 4ddd1468c2 | |||
| b274d36e11 | |||
| 7ae82f3afb | |||
| 111306b411 | |||
| c8fb8ad9ca | |||
| 2f7525c3c8 | |||
| 511fc96835 | |||
| cb539bc72b | |||
| 2b450a0a6a | |||
| fc93690d1f | |||
| 43431b88da | |||
| 13ca27d66a | |||
| 9f7179263a | |||
| c8da9cb462 | |||
| 678938951e | |||
| 8883e081af | |||
| c4d9ec98c3 | |||
| dccd836dc6 | |||
| c719cd11f3 | |||
| 42133a60c8 | |||
| d30dc7177f | |||
| e42be87798 | |||
| 524040b33c | |||
| 0227b3f554 | |||
| 6a076b0989 | |||
| 9b1a5b7102 | |||
| 0bb72064b5 | |||
| 8af68d7389 | |||
| cde0a9e24b | |||
| 9423e41a06 | |||
| 3489cbd5d7 | |||
| 65be779bb0 | |||
| 45f1dca6a3 | |||
| 913b2a5f78 | |||
| 627e2ca5a6 | |||
| 3d8af1b972 | |||
| 03f5d0222e | |||
| af85447328 | |||
| d34e11b9f6 | |||
| 231073c9c3 | |||
| 34a3fb4efb | |||
| a38c3b5dc5 | |||
| 1a1310e205 | |||
| d7b6fae2a3 | |||
| bb87a728ac | |||
| d60810c2af | |||
| a6a4579ef9 | |||
| a084a5b08b | |||
| 4112162092 | |||
| f341572616 | |||
| a047784278 | |||
| 7ac3fa1f4a | |||
| c30ec0ed8a | |||
| d6f2fd4304 | |||
| cf5b8d3b33 | |||
| 2a67d7472a | |||
| 9f74be26c3 | |||
| fb04539418 | |||
| 192cf0154a | |||
| 9acd649742 | |||
| e8fcdf4360 | |||
| 43dbb6a021 | |||
| de615f2ffe | |||
| f11eec4caf | |||
| f445a5ca57 | |||
| 68605de596 | |||
| db97d616f6 | |||
| e2e5b39afa | |||
| 6fec953ff0 | |||
| 95befc9a25 | |||
| cb92971657 | |||
| 5a35fec894 | |||
| 43b8f83e4b | |||
| 0952255a50 | |||
| 8738c4dbfd | |||
| 99436f8e79 | |||
| 339b220488 | |||
| 661f381e34 | |||
| 8d4ccf6442 | |||
| bd6b7c2ce1 | |||
| 9152d84b06 | |||
| c044f81d7b | |||
| 5730f0e00e | |||
| 76f92ba9af | |||
| d599c72278 | |||
| e5243e32be | |||
| db18e7fd74 | |||
| f3baf7efd2 | |||
| f6e223edf6 | |||
| 4f3b40d6fb | |||
| 6e480271d3 | |||
| e60cf18337 | |||
| 6409adb879 | |||
| 915e0e83bc | |||
| 8323ecdc8b | |||
| e0e9c06ca4 | |||
| eb313efdeb | |||
| bc22ff1221 | |||
| 8c988beaf2 | |||
| 752c9baf7c | |||
| 766772f654 | |||
| f5b6767253 | |||
| 5ce045ee02 | |||
| b83889dcba | |||
| cfc839f71b | |||
| 660d4e7ccb | |||
| 404dd3949f | |||
| 693c8df8d0 | |||
| d587d5f145 | |||
| be6daa5930 | |||
| acee5415c5 | |||
| 6399a99452 | |||
| b5aa2113db | |||
| 1585b0c32e | |||
| 3c01f88ab8 | |||
| 785312856e | |||
| fc8a6dc9b1 | |||
| 7b8694e465 | |||
| 655f62c331 | |||
| 53732e0ff2 | |||
| 4cae122854 | |||
| 2a11494c33 | |||
| 1dddd97d96 | |||
| f8236a8b96 | |||
| aa07108c98 | |||
| 9aed0cc933 | |||
| f6c5addf55 | |||
| 9434a112d9 | |||
| c4ec32cb78 | |||
| c5b8c812b3 | |||
| fdc2ca0c9e | |||
| dcd0e078f6 | |||
| 78b79a758f | |||
| 29f6606d99 | |||
| 94f0beec51 | |||
| 590d1d7890 | |||
| 400c92fc89 | |||
| b3e82a05db | |||
| a8aa364757 | |||
| 7457ecb1a8 | |||
| 01caf56edc | |||
| 6f07d008c9 | |||
| b408087320 | |||
| 22cbce82ce | |||
| ecdc68aa1c | |||
| 4a0bf80ab0 | |||
| 095425f664 | |||
| ca4e212e98 | |||
| 0b0f84b784 | |||
| fbd4a7dc38 | |||
| cb90d7fee6 | |||
| 1a79ea94ed | |||
| c3328a03f6 | |||
| 6803538c2e | |||
| 9d27e9b379 | |||
| 8683ca4d13 | |||
| d4f5ac152a | |||
| 31a1724390 | |||
| c034818c92 | |||
| e1fe479008 | |||
| 530659b59d | |||
| 45dd96e30a | |||
| 3f4c1fd1bb | |||
| 5acaaf5865 | |||
| 156501dbbd | |||
| a0eb9340d5 | |||
| dbdbfd0b38 | |||
| 1d9d4d3b3a | |||
| 8d16b3265c | |||
| 9c37a0393c | |||
| 82ef6232e7 | |||
| 3b9ae3e65e | |||
| a539518cd4 | |||
| f61cd60147 | |||
| b9c970dc43 | |||
| ba5e395a59 | |||
| c46e6623fe | |||
| 7ad1b113dc | |||
| c0d3ed1a90 | |||
| 00d7a77ebe | |||
| f29d3fd666 | |||
| 47204830a9 | |||
| f4bb14a30e | |||
| 0a345a3124 | |||
| 450a66ad11 | |||
| 6f3694cfa9 | |||
| 1658610f93 | |||
| f9b1bdb22d | |||
| f8abb85e9e | |||
| 1b5e6462ee | |||
| fbdd8839e6 | |||
| d86117ac70 | |||
| 914b7125cf | |||
| ad0223cafb | |||
| a870c02eab | |||
| 002e77616d | |||
| d777e68c4a | |||
| cabb345a1c | |||
| 2f08f27b59 | |||
| 3a7b0e9404 | |||
| 7713ce768a | |||
| aea573d001 | |||
| 7ca6494efa | |||
| 6f44853bf7 | |||
| 2c6c818005 | |||
| abc4fbc2f7 | |||
| 9adff21f78 | |||
| 91f9ef85ae | |||
| 17c6ad6b70 | |||
| 8f61bdb046 | |||
| 53c36226cb | |||
| 5a22944f52 | |||
| 494f93d2a4 | |||
| d7849a1aa5 | |||
| bc9adaab06 | |||
| 1ec47ca24f | |||
| faa2fa2ef0 | |||
| 33e8b453ee | |||
| b9fadd0a10 | |||
| 294fd79947 | |||
| 8b6096729c | |||
| 6f370daaed | |||
| 3c694e7909 | |||
| c3245a4f22 | |||
| da89a53605 | |||
| 968582af01 | |||
| 07c7b6ab2a | |||
| 5aae0cbcd9 | |||
| 3ea842dae4 | |||
| 31e0bfa400 | |||
| d32b10de80 | |||
| 215853cf67 | |||
| a941cc824d | |||
| d3daa18bf8 | |||
| 2ac3b6e9a2 | |||
| e81817c1b2 | |||
| 01bb8093d0 | |||
| 1565067cee | |||
| ecc603171b | |||
| 7f3308bd2b | |||
| 06d5fdb5ff | |||
| 6047d369a6 | |||
| 961a893b8c | |||
| 2927974396 | |||
| 8c780fc5d5 | |||
| 8867d203e7 | |||
| cf5f14ef5d | |||
| 132f063769 | |||
| 915cb13d45 | |||
| 0089da10cc | |||
| 28293d0f2b | |||
| d3e64295cf | |||
| 6cd3217c2e | |||
| eba2a7a6e3 | |||
| a98b822eeb | |||
| 0a80021742 | |||
| 63e8fc84a3 | |||
| fe0fb641f3 | |||
| 1c43bc7e29 | |||
| d03ed3063c | |||
| ea8664c487 | |||
| ca025f8cca | |||
| 78e19fce32 | |||
| c8536e9e46 | |||
| 1caa6069db | |||
| abe8338e5c | |||
| 5373e39ce5 | |||
| 5875973c13 | |||
| 3fbf159d0e | |||
| b5c4fe3f7d | |||
| 516d066d4c | |||
| fbcd5a71aa | |||
| b5a23086fd | |||
| a9ce3f6963 | |||
| a27f8f79a4 | |||
| dd01479c6b | |||
| e7f85ba545 | |||
| 48767da6cc | |||
| 73754399be | |||
| 18f5668e3e | |||
| bc92e55b53 | |||
| 230feff430 | |||
| 8bb4387dc4 | |||
| 2506ba8364 | |||
| daad6d662f | |||
| 53853c2d9a | |||
| 40de714e81 | |||
| 27bde16843 | |||
| 5e8f8d5513 | |||
| 120970c4ea | |||
| 740e729606 | |||
| 60b140b684 | |||
| 9a165468eb | |||
| e15897b3f1 | |||
| 52f98582f1 | |||
| 2e72c23868 | |||
| 0967027feb | |||
| 6c9b1ef3c1 | |||
| 8cceded0ae | |||
| ff181475a0 | |||
| 074c0e59e0 | |||
| 1d7c60c46a | |||
| 377f34fae2 | |||
| 26cb805e0f | |||
| 81dbe2060c | |||
| fd0fca436b | |||
| 3d653d3fdc | |||
| b22bb3ee9f | |||
| 7f17b4be7b | |||
| fa3a9d81e3 | |||
| 892c99f0f3 | |||
| 8d8846a259 | |||
| 9d63af6271 | |||
| 37ad82adfc | |||
| 57953b9ae9 | |||
| 777fb920f6 | |||
| 05750e871b | |||
| 5a11b8b836 | |||
| 8a785ea855 | |||
| 1874a76f67 | |||
| 0b2b528962 | |||
| 2036c3da9d | |||
| 7694b016da | |||
| 6fdd59157a | |||
| 0d7096fa94 | |||
| a94dc4e89b | |||
| bf965b2a17 | |||
| 9c87625910 | |||
| 3f1543504a | |||
| 3773968d19 | |||
| d28d4ce799 | |||
| bffb19b23a | |||
| 6aea4c827a | |||
| ac3250c58b | |||
| 6fe0880e11 | |||
| 78282bf1e1 | |||
| 43d25127c3 | |||
| c33c61a256 | |||
| def4be5a9f | |||
| 9bc0d8b0d9 | |||
| 0924b2e343 | |||
| 8b6e75980b | |||
| 5fd0cb0ddb | |||
| b5edc86a52 | |||
| d09655989d | |||
| 83415ac6ca | |||
| cc7fb63c6d | |||
| f5195222a7 | |||
| cecf15a34a | |||
| 95b53d7e01 | |||
| 8cd70854ba | |||
| dbaa36ec3e | |||
| 8976233905 | |||
| 82d47d800c | |||
| e84ad97edf | |||
| d447342cbd | |||
| c74ecff3f0 | |||
| a0282ec71b | |||
| a67f9d5bbf | |||
| f7297edd61 | |||
| 87a6037924 | |||
| ee710e34dd | |||
| 55143e1790 | |||
| 7a0bf9b9b9 | |||
| b422b93c78 | |||
| 4742aa298a | |||
| f9f389d9ec | |||
| 7dba05f4c5 | |||
| f02a7d15ab | |||
| 54ab46dcb4 | |||
| 9b406cff87 | |||
| 5791ac9b76 | |||
| 6026b0c4b7 | |||
| 52909b0eeb | |||
| 1feb77bbef | |||
| 2e1b051a4d | |||
| 15fd892b63 | |||
| 4833403d65 | |||
| 061a2f739a | |||
| 86b5cb4dba | |||
| 74bc3dfb6e | |||
| 7841ed8637 | |||
| 19df945155 | |||
| 3e3bff76de | |||
| ea073f55f0 | |||
| c1e28aa156 | |||
| af62f09e37 | |||
| 9a33385697 | |||
| bfa89bc73f | |||
| e1d05fa53c | |||
| b0ccc94b26 | |||
| b2356a0232 | |||
| 3bb883387e | |||
| 506a36b210 | |||
| 8c1966a237 | |||
| 09513eaa5e | |||
| fda9177a70 | |||
| 21960a5ba2 | |||
| 0819ab1dad | |||
| 475ad79360 | |||
| 7b52306ff2 | |||
| e5f6d026ff | |||
| 5dd5710758 | |||
| 37b62dfed1 | |||
| d21a4152de | |||
| 8c2dcd7b5d | |||
| 019b4a20f6 | |||
| 30a9a972ce | |||
| 22ba1684b2 | |||
| 0b12ec2b38 | |||
| a71f5bf21f | |||
| 9bd7cfda5f | |||
| c1a13f7f98 | |||
| a362584bb3 | |||
| f9ce7628ff | |||
| 43c066e837 | |||
| f3f37a33fd | |||
| 39c6481f96 | |||
| 66b9d334ef | |||
| e64cb2c4f1 | |||
| 4f47868930 | |||
| 4c115b6ad5 | |||
| 242a1047bd | |||
| 2f3cab431f | |||
| 55f514897b | |||
| d4b92de8e4 | |||
| 25d39997a4 | |||
| 254ce8923b | |||
| 0a4db305b9 | |||
| 90ac2181e9 | |||
| bdf5fad992 | |||
| 05be62183a | |||
| d545419684 | |||
| f900db49dd | |||
| 1373f99288 | |||
| f56bc4c0d6 | |||
| 60efcbc55d | |||
| 30589ca899 | |||
| 61fa339163 | |||
| 3f5efc1ff6 | |||
| 23f72ba15f | |||
| a25acf7e62 | |||
| c3fc310f29 | |||
| b9c7ffe7c3 | |||
| 017a947fc1 | |||
| 5c57631a6c | |||
| 3495cab7ad | |||
| 7a06bdb695 | |||
| 6c57003d17 | |||
| 2eb2ae7959 | |||
| a055aa3e57 | |||
| 5c7a733f49 | |||
| 00ae386b74 | |||
| 9ad7ca8f11 | |||
| a43ce05200 | |||
| 7d4dfb5c2d | |||
| d9e9006e61 | |||
| bfec34db20 | |||
| 0aae72c161 | |||
| fbd8b9c816 | |||
| d6120a5985 | |||
| a8f7939126 | |||
| 217429c3fe | |||
| 716958bb86 | |||
| 1319558eb6 | |||
| 096d478593 | |||
| 8fcd5a91c4 | |||
| 155042e46c | |||
| e03d40e946 | |||
| 2671769d9f | |||
| 8d9d83f15f | |||
| 6bc9dc5c6a | |||
| d6566484a1 | |||
| 0e4d8ec62f | |||
| f9c6f897c8 | |||
| 7252a685a6 | |||
| bed4d5034e | |||
| e2a2f32e82 | |||
| 334c66b0a0 | |||
| ca392b08c9 | |||
| e9a34f6359 | |||
| 6411d27096 | |||
| 07f0017d30 | |||
| 59f9d12da5 | |||
| 1c114978e4 | |||
| 629421214f | |||
| 97d772dd05 | |||
| f28c64ba21 | |||
| 20dd15e256 | |||
| f33d10468d | |||
| d3b3b4db10 | |||
| 38e28643f1 | |||
| 9de6d28270 | |||
| 9f47201bab | |||
| 0b7140c123 | |||
| 28a4918ff6 | |||
| 534cd599f4 | |||
| 910a5ce90a | |||
| dadd01a4ea | |||
| c4a9059814 | |||
| 51a1cd3c67 | |||
| c6d2ab4637 | |||
| c6c7307d6e | |||
| 9c9944aa0c | |||
| b311197d41 | |||
| 1068d88c3e | |||
| 861078a95e | |||
| aa9aef44f7 | |||
| f2ad11a56a | |||
| 12c327292f | |||
| 31e78c2a1b | |||
| 8a64922130 | |||
| 2ae142f257 | |||
| faa0e6e554 | |||
| b95cf79a6d | |||
| 28cd8beb77 | |||
| 1918bd5f6b | |||
| d45addee10 | |||
| ed16e91aed | |||
| 714caae545 | |||
| 25bb607b27 | |||
| c9a6ae9549 | |||
| 58099fd6b5 | |||
| c5856a33f0 | |||
| a5f115f21f | |||
| ddd84e231b | |||
| 51e9df87f5 | |||
| aec4d37a2e | |||
| ceafc2155f | |||
| 4a37d6ebe2 | |||
| 10095f8627 | |||
| 84bb1ab595 | |||
| fce7999890 | |||
| 10b72ef4b4 | |||
| bfbb354c39 | |||
| 9db137af44 | |||
| 2999d10fb9 | |||
| 654885a925 | |||
| 8042abe5f5 | |||
| 65ee18a52d | |||
| 69588d5266 | |||
| 7b77b19bc0 | |||
| 357b36b287 | |||
| a5f0473e1b | |||
| 8d74d46d80 | |||
| 9f2c572709 | |||
| 4b6dd5c857 | |||
| 83dd11ea7d | |||
| 6c2a88cdc0 | |||
| e00d57fee2 | |||
| ce44c6e4e7 | |||
| f9ff4fff50 | |||
| 2291a61379 | |||
| d8f37509af | |||
| dddbcfbabb | |||
| ed8c1d543a | |||
| 3e02d90a27 | |||
| 954b16ad39 | |||
| ed18c5113f | |||
| 0f4b3aa187 | |||
| 8a7658745d | |||
| 2ea39877cc | |||
| 4212691cf0 | |||
| d66fe79579 | |||
| 88caf11842 | |||
| 5320d952e5 | |||
| 7eae832b8c | |||
| 1d18ab03d7 | |||
| 337bc2c097 | |||
| 7e59ae99d0 | |||
| 51feda1042 | |||
| 0a520e4f9f | |||
| e07212d356 | |||
| 1d52073b45 | |||
| d85d6cfbca | |||
| 892cb9116c | |||
| 5e9d291ca3 | |||
| b74d64a456 | |||
| cd8f5cf5d4 | |||
| 1dc20aa9aa | |||
| bdab9951af | |||
| f641a639cd | |||
| 049021fe27 | |||
| 4db32b15ba | |||
| 619346acad | |||
| 525f9866a4 | |||
| d7dc1c9b5b | |||
| 7da3aaaa8a | |||
| 5aaa6bf187 | |||
| af9a5edd59 | |||
| 6e764644b3 | |||
| 8dc2ec9dc4 | |||
| 4e1ae3d5e9 | |||
| 582b3a91d6 | |||
| f7467ff57a | |||
| 2e16021f14 |
+2
-3
@@ -9,8 +9,7 @@ exclude = [
|
||||
[advisories]
|
||||
version = 2
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2023-0071", reason = "We are not using RSA directly, nor do we depend on the RSA crate directly" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained backoff crate, not critical. We'll migrate soon." },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate, not critical." },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
@@ -60,7 +59,7 @@ allow-git = [
|
||||
# A patch override for the bindings: https://github.com/rodrimati1992/const_panic/pull/10
|
||||
"https://github.com/jplatte/const_panic",
|
||||
# A patch override for the bindings: https://github.com/smol-rs/async-compat/pull/22
|
||||
"https://github.com/jplatte/async-compat",
|
||||
"https://github.com/element-hq/async-compat",
|
||||
# We can release vodozemac whenever we need but let's not block development
|
||||
# on releases.
|
||||
"https://github.com/matrix-org/vodozemac",
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
toolchain: nightly-2025-02-20
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
+25
-22
@@ -295,7 +295,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.29.5
|
||||
uses: crate-ci/typos@v1.31.1
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -314,7 +314,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
toolchain: nightly-2025-02-20
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
@@ -342,27 +342,9 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
services:
|
||||
# synapse needs a postgres container
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: syncv3
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
@@ -401,3 +383,24 @@ jobs:
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
run: |
|
||||
cargo nextest run -p matrix-sdk-integration-testing
|
||||
|
||||
compile-bench:
|
||||
name: 🚄 Compile benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Compile benchmarks (no run)
|
||||
run: |
|
||||
cargo bench --profile dev --no-run
|
||||
|
||||
|
||||
@@ -26,26 +26,9 @@ jobs:
|
||||
name: Code Coverage
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'postgres'
|
||||
# maps to the postgres container, etc.
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
services:
|
||||
postgres:
|
||||
# Docker Hub image
|
||||
image: postgres
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: syncv3
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
# tests need a synapse: this is a service and not michaelkaye/setup-matrix-synapse@main as the
|
||||
# latter does not provide networking for services to communicate with it.
|
||||
synapse:
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Check if the path of changed file is longer than 260 characters
|
||||
# that windows filesystem allows
|
||||
|
||||
name: Detect long path among changed files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request: # focus on the changed files in current PR
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
long-path:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@6f67ee9ac810f0192ea7b3d2086406f97847bcf9 # v45
|
||||
- name: Detect long path
|
||||
env:
|
||||
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
|
||||
MAX_LENGTH: 120 # set max length to 120, considering the base path of app project that uses matrix-sdk
|
||||
run: |
|
||||
for file in ${ALL_CHANGED_FILES}; do
|
||||
if [ ${#file} -gt $MAX_LENGTH ]; then
|
||||
echo "File path is too long. Length: ${#file}, Path: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
@@ -9,4 +9,4 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
uses: bnjbvr/cargo-machete@v0.8.0
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
toolchain: nightly-2025-02-20
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
Generated
+263
-893
File diff suppressed because it is too large
Load Diff
+26
-23
@@ -18,7 +18,7 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.83"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.95"
|
||||
@@ -29,13 +29,13 @@ assert_matches2 = "0.1.2"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
as_variant = "1.2.0"
|
||||
as_variant = "1.3.0"
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.6.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.8.0"
|
||||
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"
|
||||
@@ -45,9 +45,9 @@ growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.2.0"
|
||||
imbl = "4.0.1"
|
||||
imbl = "5.0.0"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json"] }
|
||||
insta = { version = "1.42.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.69"
|
||||
mime = "0.3.17"
|
||||
@@ -60,7 +60,7 @@ reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.1", features = [
|
||||
ruma = { version = "0.12.2", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -75,7 +75,7 @@ ruma = { version = "0.12.1", features = [
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
ruma-common = { version = "0.15.1" }
|
||||
ruma-common = "0.15.2"
|
||||
serde = "1.0.217"
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
@@ -101,21 +101,17 @@ web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.10.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.10.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.10.0" }
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.11.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.11.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.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.10.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.10.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.10.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.10.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.10.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.10.0", default-features = false }
|
||||
|
||||
# Default release profile, select with `--release`
|
||||
[profile.release]
|
||||
lto = true
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.11.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.11.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.11.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.11.0", default-features = false }
|
||||
|
||||
# Default development profile; default for most Cargo commands, otherwise
|
||||
# selected with `--debug`
|
||||
@@ -143,8 +139,15 @@ debug = 2
|
||||
inherits = "dbg"
|
||||
opt-level = 3
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
# LTO is too slow to compile.
|
||||
lto = false
|
||||
# Get symbol names for profiling purposes.
|
||||
debug = true
|
||||
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
|
||||
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
|
||||
tracing = { git = "https://github.com/element-hq/tracing.git", rev = "ca9431f74d37c9d3b5e6a9f35b2c706711dab7dd" }
|
||||
|
||||
@@ -1,40 +1,68 @@
|
||||

|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
[](https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/)
|
||||
[](https://docs.rs/matrix-sdk)
|
||||
<h1 align="center">Matrix Rust SDK</h1>
|
||||
<div align="center">
|
||||
<i>Your all-in-one toolkit for creating Matrix clients with Rust, from simple bots to full-featured apps.</i>
|
||||
<br/><br/>
|
||||
<img src="contrib/logo.svg">
|
||||
<br>
|
||||
<hr>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/releases">
|
||||
<img src="https://img.shields.io/github/v/release/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub&logoColor=white"></a>
|
||||
<a href="https://crates.io/crates/matrix-sdk/">
|
||||
<img src="https://img.shields.io/crates/v/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://codecov.io/gh/matrix-org/matrix-rust-sdk">
|
||||
<img src="https://img.shields.io/codecov/c/gh/matrix-org/matrix-rust-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Codecov&logoColor=white"></a>
|
||||
<br>
|
||||
<a href="https://docs.rs/matrix-sdk/">
|
||||
<img src="https://img.shields.io/docsrs/matrix-sdk?style=flat&labelColor=1C2E27&color=66845F&logo=Rust&logoColor=white"></a>
|
||||
<a href="https://github.com/matrix-org/matrix-rust-sdk/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/matrix-org/matrix-rust-sdk/ci.yml?style=flat&labelColor=1C2E27&color=66845F&logo=GitHub%20Actions&logoColor=white"></a>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
# matrix-rust-sdk
|
||||
|
||||
**matrix-rust-sdk** is an implementation of a [Matrix][] client-server library in [Rust][].
|
||||
The Matrix Rust SDK is a collection of libraries that make it easier to build
|
||||
[Matrix] clients in [Rust]. It takes care of the low-level details like encryption,
|
||||
syncing, and room state, so you can focus on your app's logic and UI. Whether
|
||||
you're writing a small bot, a desktop client, or something in between, the SDK
|
||||
is designed to be flexible, async-friendly, and ready to use out of the box.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
## Project structure
|
||||
|
||||
The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
The Matrix Rust SDK is made up of several crates that build on top of each other. Here are the key ones:
|
||||
|
||||
- **matrix-sdk** - High level client library, with batteries included, you're most likely
|
||||
interested in this.
|
||||
- **matrix-sdk-base** - No (network) IO client state machine that can be used to embed a
|
||||
Matrix client in your project or build a full fledged network enabled client
|
||||
lib on top of it.
|
||||
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
|
||||
used to add Matrix E2EE support to your client or client library.
|
||||
- [matrix-sdk-ui](https://docs.rs/matrix-sdk-ui/latest/matrix_sdk_ui/) – A high-level client library that makes it easy to build
|
||||
full-featured UI clients with minimal setup. Check out our reference client,
|
||||
[multiverse](https://github.com/matrix-org/matrix-rust-sdk/tree/main/labs/multiverse), for an example.
|
||||
- [matrix-sdk](https://docs.rs/matrix-sdk/latest/matrix_sdk/) – A mid-level client library, ideal for building bots, custom
|
||||
clients, or higher-level abstractions. You can find example usage in the
|
||||
[examples directory](https://github.com/matrix-org/matrix-rust-sdk/tree/main/examples).
|
||||
- [matrix-sdk-crypto](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/) – A standalone encryption state machine with no network I/O,
|
||||
providing end-to-end encryption support for Matrix clients and libraries.
|
||||
See the [crypto tutorial](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/tutorial/index.html)
|
||||
for a step-by-step introduction.
|
||||
|
||||
## Status
|
||||
|
||||
The library is considered production ready and backs multiple client implementations such as Element X [[1]](https://github.com/element-hq/element-x-ios) [[2]](https://github.com/element-hq/element-x-android) and [Fractal](https://gitlab.gnome.org/World/fractal). Client developers should feel confident to build upon it.
|
||||
The library is considered production ready and backs multiple client
|
||||
implementations such as Element X
|
||||
[[1]](https://github.com/element-hq/element-x-ios)
|
||||
[[2]](https://github.com/element-hq/element-x-android),
|
||||
[Fractal](https://gitlab.gnome.org/World/fractal) and [iamb](https://github.com/ulyssa/iamb). Client developers should feel
|
||||
confident to build upon it.
|
||||
|
||||
Development of the SDK has been primarily sponsored by Element though accepts contributions from all.
|
||||
Development of the SDK has been primarily sponsored by Element though accepts
|
||||
contributions from all.
|
||||
|
||||
## Bindings
|
||||
|
||||
Some crates of the **matrix-rust-sdk** can be embedded inside other
|
||||
environments, like Swift, Kotlin, JavaScript, Node.js etc. Please,
|
||||
explore the [`bindings/`](./bindings/) directory to learn more.
|
||||
The higher-level crates of the Matrix Rust SDK can be embedded in other
|
||||
environments such as Swift, Kotlin, JavaScript, and Node.js. Check out the
|
||||
[bindings/](./bindings/) directory to learn more about how to integrate the SDK
|
||||
into your language of choice.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+19
-18
@@ -13,35 +13,36 @@ The procedure is as follows:
|
||||
|
||||
1. Switch to a release branch:
|
||||
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
```bash
|
||||
git switch -c release-x.y.z
|
||||
```
|
||||
|
||||
2. Prepare the release. This will update the `README.md`, set the versions in
|
||||
the `CHANGELOG.md` file, and bump the version in the `Cargo.toml` file.
|
||||
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
```bash
|
||||
cargo xtask release prepare --execute minor|patch|rc
|
||||
```
|
||||
|
||||
3. Double-check and edit the `CHANGELOG.md` and `README.md` if necessary. Once you are
|
||||
satisfied, push the branch and open a PR.
|
||||
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
```bash
|
||||
git push --set-upstream origin/release-x.y.z
|
||||
```
|
||||
|
||||
4. Pass the review and merge the branch as you would with any other branch.
|
||||
|
||||
5. Create tags for your new release, publish the release on crates.io and push
|
||||
the tags:
|
||||
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
```bash
|
||||
# Switch to main first.
|
||||
git switch main
|
||||
# Pull in the now-merged release commit(s).
|
||||
git pull
|
||||
# Create tags, publish the release on crates.io, and push the tags.
|
||||
cargo xtask release publish --execute
|
||||
```
|
||||
|
||||
For more information on cargo-release: https://github.com/crate-ci/cargo-release
|
||||
|
||||
@@ -14,7 +14,7 @@ matrix-sdk-crypto = { workspace = true }
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true }
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite"] }
|
||||
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -29,6 +29,10 @@ pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "linked_chunk"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
@@ -37,5 +41,9 @@ harness = false
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "timeline"
|
||||
harness = false
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
+10
-3
@@ -8,7 +8,7 @@ can be found [here](https://bheisler.github.io/criterion.rs/book/criterion_rs.ht
|
||||
|
||||
## Running the benchmarks
|
||||
|
||||
The benchmark can be simply run by using the `bench` command of `cargo`:
|
||||
The benchmark can be run by using the `bench` command of `cargo`:
|
||||
|
||||
```bash
|
||||
$ cargo bench
|
||||
@@ -16,6 +16,13 @@ $ cargo bench
|
||||
|
||||
This will work from the workspace directory of the rust-sdk.
|
||||
|
||||
To lower compile times, you might be interested in using the `profiling` profile, that's optimized
|
||||
for a fair tradeoff between compile times and runtime performance:
|
||||
|
||||
```bash
|
||||
$ cargo bench --profile profiling
|
||||
```
|
||||
|
||||
If you want to pass options to the benchmark [you'll need to specify the name of
|
||||
the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options):
|
||||
|
||||
@@ -23,7 +30,7 @@ the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench
|
||||
$ cargo bench --bench crypto_bench -- # Your options go here
|
||||
```
|
||||
|
||||
If you want to run only a specific benchmark, simply pass the name of the
|
||||
If you want to run only a specific benchmark, pass the name of the
|
||||
benchmark as an argument:
|
||||
|
||||
```bash
|
||||
@@ -65,7 +72,7 @@ permisive value is `-1`:
|
||||
$ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
```
|
||||
|
||||
To generate flame graphs feature simply enable the profiling mode using the
|
||||
To generate flame graphs feature, enable the profiling mode using the
|
||||
`--profile-time` command line flag:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
linked_chunk::{lazy_loader, LinkedChunk, Update},
|
||||
SqliteEventCacheStore,
|
||||
};
|
||||
use matrix_sdk_base::event_cache::{
|
||||
store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore, DEFAULT_CHUNK_CAPACITY},
|
||||
Event, Gap,
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE};
|
||||
use ruma::{room_id, EventId};
|
||||
use tempfile::tempdir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Operation {
|
||||
PushItemsBack(Vec<Event>),
|
||||
PushGapBack(Gap),
|
||||
}
|
||||
|
||||
fn writing(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("writing");
|
||||
group.sample_size(10).measurement_time(Duration::from_secs(30));
|
||||
|
||||
for number_of_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Option<Arc<DynEventCacheStore>>); 3] = [
|
||||
("none", None),
|
||||
("memory store", Some(MemoryStore::default().into_event_cache_store())),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
Some(
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store(),
|
||||
)
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Create the operations we want to bench.
|
||||
let mut operations = Vec::new();
|
||||
|
||||
{
|
||||
let mut events = (0..number_of_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let mut gap_nth = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
{
|
||||
let events_to_push_back = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
|
||||
if events_to_push_back.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
operations.push(Operation::PushItemsBack(events_to_push_back));
|
||||
}
|
||||
|
||||
{
|
||||
operations.push(Operation::PushGapBack(Gap {
|
||||
prev_token: format!("gap{gap_nth}"),
|
||||
}));
|
||||
gap_nth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(number_of_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new(store_name, number_of_events),
|
||||
&operations,
|
||||
|bencher, operations| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter_batched(
|
||||
|| operations.clone(),
|
||||
|operations| async {
|
||||
// The routine to bench!
|
||||
|
||||
let mut linked_chunk = LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::PushItemsBack(events) => linked_chunk.push_items_back(events),
|
||||
Operation::PushGapBack(gap) => linked_chunk.push_gap_back(gap),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(store) = &store {
|
||||
let updates = linked_chunk.updates().unwrap().take();
|
||||
store.handle_linked_chunk_updates(room_id, updates).await.unwrap();
|
||||
// Empty the store.
|
||||
store.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await.unwrap();
|
||||
}
|
||||
|
||||
},
|
||||
BatchSize::SmallInput
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn reading(c: &mut Criterion) {
|
||||
// Create a new asynchronous runtime.
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.enable_time()
|
||||
.enable_io()
|
||||
.build()
|
||||
.expect("Failed to create an asynchronous runtime");
|
||||
|
||||
let room_id = room_id!("!foo:bar.baz");
|
||||
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
|
||||
|
||||
let mut group = c.benchmark_group("reading");
|
||||
group.sample_size(10);
|
||||
|
||||
for num_events in [10, 100, 1000, 10_000, 100_000] {
|
||||
let sqlite_temp_dir = tempdir().unwrap();
|
||||
|
||||
// Declare new stores for this set of events.
|
||||
let stores: [(&str, Arc<DynEventCacheStore>); 2] = [
|
||||
("memory store", MemoryStore::default().into_event_cache_store()),
|
||||
(
|
||||
"sqlite store",
|
||||
runtime.block_on(async {
|
||||
SqliteEventCacheStore::open(sqlite_temp_dir.path().join("bench"), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_event_cache_store()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (store_name, store) in stores {
|
||||
// Store some events and gap chunks in the store.
|
||||
{
|
||||
let mut events = (0..num_events)
|
||||
.map(|nth| {
|
||||
event_factory
|
||||
.text_msg("foo")
|
||||
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
|
||||
.into_event()
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let mut lc =
|
||||
LinkedChunk::<DEFAULT_CHUNK_CAPACITY, Event, Gap>::new_with_update_history();
|
||||
let mut num_gaps = 0;
|
||||
|
||||
while events.peek().is_some() {
|
||||
let events_chunk = events.by_ref().take(80).collect::<Vec<_>>();
|
||||
if events_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
lc.push_items_back(events_chunk);
|
||||
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
|
||||
num_gaps += 1;
|
||||
}
|
||||
|
||||
// Now persist the updates to recreate this full linked chunk.
|
||||
let updates = lc.updates().unwrap().take();
|
||||
runtime.block_on(store.handle_linked_chunk_updates(room_id, updates)).unwrap();
|
||||
}
|
||||
|
||||
// Define the throughput.
|
||||
group.throughput(Throughput::Elements(num_events));
|
||||
|
||||
// Get a bencher.
|
||||
group.bench_function(BenchmarkId::new(store_name, num_events), |bencher| {
|
||||
// Bench the routine.
|
||||
bencher.to_async(&runtime).iter(|| async {
|
||||
// Load the last chunk first,
|
||||
let (last_chunk, chunk_id_gen) = store.load_last_chunk(room_id).await.unwrap();
|
||||
|
||||
let mut lc =
|
||||
lazy_loader::from_last_chunk::<128, _, _>(last_chunk, chunk_id_gen)
|
||||
.expect("no error when reconstructing the linked chunk")
|
||||
.expect("there is a linked chunk in the store");
|
||||
|
||||
// Then load until the start of the linked chunk.
|
||||
let mut cur_chunk_id = lc.chunks().next().unwrap().identifier();
|
||||
while let Some(prev) =
|
||||
store.load_previous_chunk(room_id, cur_chunk_id).await.unwrap()
|
||||
{
|
||||
cur_chunk_id = prev.identifier;
|
||||
lazy_loader::insert_new_first_chunk(&mut lc, prev)
|
||||
.expect("no error when linking the previous lazy-loaded chunk");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(store);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = event_cache;
|
||||
config = criterion();
|
||||
targets = writing, reading,
|
||||
}
|
||||
|
||||
criterion_main!(event_cache);
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
|
||||
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::{
|
||||
event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder,
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::{timeline::TimelineFocus, Timeline};
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
@@ -18,13 +16,9 @@ use ruma::{
|
||||
serde::Raw,
|
||||
user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
use wiremock::{
|
||||
matchers::{header, method, path, path_regex, query_param, query_param_is_missing},
|
||||
Mock, MockServer, Request, ResponseTemplate,
|
||||
};
|
||||
use wiremock::{Request, ResponseTemplate};
|
||||
|
||||
pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
@@ -61,17 +55,18 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::with_store_config(
|
||||
let base_client = BaseClient::new(
|
||||
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
|
||||
.state_store(sqlite_store),
|
||||
);
|
||||
|
||||
runtime
|
||||
.block_on(base_client.set_session_meta(
|
||||
.block_on(base_client.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
))
|
||||
.expect("Could not set session meta");
|
||||
@@ -109,9 +104,7 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id).sender(&sender_id);
|
||||
let (client, server) = runtime.block_on(logged_in_client_with_server());
|
||||
|
||||
let mut sync_response_builder = SyncResponseBuilder::new();
|
||||
let mut joined_room_builder =
|
||||
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
|
||||
|
||||
@@ -133,17 +126,15 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
}
|
||||
)));
|
||||
let response_json =
|
||||
sync_response_builder.add_joined_room(joined_room_builder).build_json_sync_response();
|
||||
runtime.block_on(mock_sync(&server, response_json, None));
|
||||
|
||||
let sync_settings = SyncSettings::default();
|
||||
runtime.block_on(client.sync_once(sync_settings)).expect("Could not sync");
|
||||
runtime.block_on(server.reset());
|
||||
let (server, client, room) = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
runtime.block_on(
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(r"/_matrix/client/r0/rooms/.*/event/.*"))
|
||||
let room = server.sync_room(&client, joined_room_builder).await;
|
||||
|
||||
server
|
||||
.mock_room_event()
|
||||
.respond_with(move |r: &Request| {
|
||||
let segments: Vec<&str> = r.url.path_segments().expect("Invalid path").collect();
|
||||
let event_id_str = segments[6];
|
||||
@@ -157,10 +148,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
.set_delay(Duration::from_millis(50))
|
||||
.set_body_json(event.json())
|
||||
})
|
||||
.mount(&server),
|
||||
);
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
(server, client, room)
|
||||
});
|
||||
|
||||
let room = client.get_room(&room_id).expect("Room not found");
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
assert!(!pinned_event_ids.is_empty());
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
@@ -171,15 +166,6 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(10);
|
||||
|
||||
let client = Arc::new(client);
|
||||
|
||||
{
|
||||
let client = client.clone();
|
||||
runtime.spawn_blocking(move || {
|
||||
client.event_cache().subscribe().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
group.bench_function(BenchmarkId::new("load_pinned_events", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
|
||||
@@ -187,7 +173,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
assert_eq!(pinned_event_ids.len(), PINNED_EVENTS_COUNT);
|
||||
|
||||
// Reset cache so it always loads the events from the mocked endpoint
|
||||
client.event_cache().empty_immutable_cache().await;
|
||||
client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.clear_all_rooms_chunks()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let timeline = Timeline::builder(&room)
|
||||
.with_focus(TimelineFocus::PinnedEvents {
|
||||
@@ -206,40 +199,25 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
runtime.block_on(server.reset());
|
||||
drop(server);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Option<String>) {
|
||||
let mut mock_builder = Mock::given(method("GET"))
|
||||
.and(path("/_matrix/client/r0/sync"))
|
||||
.and(header("authorization", "Bearer 1234"));
|
||||
|
||||
if let Some(since) = since {
|
||||
mock_builder = mock_builder.and(query_param("since", since));
|
||||
} else {
|
||||
mock_builder = mock_builder.and(query_param_is_missing("since"));
|
||||
}
|
||||
|
||||
mock_builder
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
criterion
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
|
||||
@@ -2,9 +2,8 @@ use std::sync::Arc;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::{MatrixSession, MatrixSessionTokens},
|
||||
config::StoreConfig,
|
||||
Client, RoomInfo, RoomState, StateChanges,
|
||||
authentication::matrix::MatrixSession, config::StoreConfig, Client, RoomInfo, RoomState,
|
||||
SessionTokens, StateChanges,
|
||||
};
|
||||
use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
@@ -51,7 +50,7 @@ pub fn restore_session(c: &mut Criterion) {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
},
|
||||
tokens: MatrixSessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
tokens: SessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
|
||||
};
|
||||
|
||||
// Start the benchmark.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::test_utils::mocks::MatrixMockServer;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, JoinedRoomBuilder, StateTestEvent};
|
||||
use matrix_sdk_ui::Timeline;
|
||||
use ruma::{
|
||||
events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id, owned_user_id,
|
||||
EventId,
|
||||
};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
/// Benchmark the time it takes to create a timeline (with read receipt
|
||||
/// support), when there are many initial events at rest in the event cache.
|
||||
///
|
||||
/// `NUM_EVENTS` is the number of events that will be stored initially in the
|
||||
/// event cache. It will be a mix of messages, reactions, edits and redactions,
|
||||
/// so there are some aggregations to take into account by the timeline as well.
|
||||
pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|
||||
const NUM_EVENTS: usize = 10000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let sender_id = owned_user_id!("@sender:example.com");
|
||||
let other_sender_id = owned_user_id!("@other_sender:example.com");
|
||||
let another_sender_id = owned_user_id!("@another_sender:example.com");
|
||||
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
|
||||
let mut events = Vec::new();
|
||||
for i in 0..NUM_EVENTS {
|
||||
let sender = match i % 3 {
|
||||
0 => &sender_id,
|
||||
1 => &other_sender_id,
|
||||
2 => &another_sender_id,
|
||||
_ => unreachable!("math genius over here"),
|
||||
};
|
||||
|
||||
let event_id = EventId::parse(format!("$event{i}")).unwrap();
|
||||
|
||||
let j = i % 10;
|
||||
if j < 6 {
|
||||
// Messages.
|
||||
events.push(
|
||||
f.text_msg(format!("Message {i}"))
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j < 8 {
|
||||
// Reactions.
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 2)).unwrap();
|
||||
events.push(
|
||||
f.reaction(&prev_event, "👍").sender(sender).event_id(&event_id).into_raw_sync(),
|
||||
);
|
||||
} else if j == 8 {
|
||||
// Edit.
|
||||
// Note: (i-3)%3 is the same as i%3 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 3)).unwrap();
|
||||
events.push(
|
||||
f.text_msg(format!("* Message {}v2", i - 3))
|
||||
.edit(
|
||||
&prev_event,
|
||||
RoomMessageEventContentWithoutRelation::text_plain(format!(
|
||||
"Message {}v2",
|
||||
i - 3
|
||||
)),
|
||||
)
|
||||
.sender(sender)
|
||||
.event_id(&event_id)
|
||||
.into_raw_sync(),
|
||||
);
|
||||
} else if j == 9 {
|
||||
// Redaction.
|
||||
// Note: (i-6)%3 is the same as i%6 -> same sender!
|
||||
let prev_event = EventId::parse(format!("$event{}", i - 6)).unwrap();
|
||||
events
|
||||
.push(f.redaction(&prev_event).sender(sender).event_id(&event_id).into_raw_sync());
|
||||
}
|
||||
}
|
||||
|
||||
let builder = JoinedRoomBuilder::new(&room_id)
|
||||
.add_state_event(StateTestEvent::Encryption)
|
||||
.add_timeline_bulk(events);
|
||||
|
||||
let room = runtime.block_on(async move {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
|
||||
client.event_cache().subscribe().unwrap();
|
||||
|
||||
let room = server.sync_room(&client, builder).await;
|
||||
drop(server);
|
||||
|
||||
room
|
||||
});
|
||||
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(NUM_EVENTS as _));
|
||||
group.sample_size(10);
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new("create_timeline_with_initial_events", format!("{NUM_EVENTS} events")),
|
||||
|b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let timeline = Timeline::builder(&room)
|
||||
.track_read_marker_and_receipts()
|
||||
.build()
|
||||
.await
|
||||
.expect("Could not create timeline");
|
||||
|
||||
let (items, _) = timeline.subscribe().await;
|
||||
assert_eq!(items.len(), 20);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Criterion::default().with_profiler(pprof::criterion::PProfProfiler::new(
|
||||
100,
|
||||
pprof::criterion::Output::Flamegraph(None),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Criterion::default()
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = room;
|
||||
config = criterion();
|
||||
targets = create_timeline_with_initial_events
|
||||
}
|
||||
criterion_main!(room);
|
||||
@@ -507,6 +507,7 @@ fn collect_sessions(
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
shared_history: false,
|
||||
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
# unreleased
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- `TracingConfiguration` now includes a new field `trace_log_packs`, which gives a convenient way
|
||||
to set the TRACE log level for multiple targets related to a given feature.
|
||||
([#4824](https://github.com/matrix-org/matrix-rust-sdk/pull/4824))
|
||||
|
||||
- `setup_tracing` has been renamed `init_platform`; in addition to the `TracingConfiguration`
|
||||
parameter it also now takes a boolean indicating whether to spawn a minimal tokio runtime for the
|
||||
application; in general for main app processes this can be set to `false`, and memory-constrained
|
||||
programs can set it to `true`.
|
||||
|
||||
- Matrix client API errors coming from API responses will now be mapped to `ClientError::MatrixApi`, containing both the
|
||||
original message and the associated error code and kind.
|
||||
|
||||
@@ -29,12 +46,59 @@ Breaking changes:
|
||||
- There is a new `abortOidcLogin` method that should be called if the webview is dismissed without a callback (
|
||||
or fails to present).
|
||||
- The rest of `AuthenticationError` is now found in the OidcError type.
|
||||
|
||||
- `OidcAuthenticationData` is now called `OidcAuthorizationData`.
|
||||
|
||||
- The `get_element_call_required_permissions` function now requires the device_id.
|
||||
|
||||
- Some `OidcPrompt` cases have been removed (`None`, `SelectAccount`).
|
||||
|
||||
- `Room::is_encrypted` is replaced by `Room::latest_encryption_state`
|
||||
which returns a value of the new `EncryptionState` enum; another
|
||||
`Room::encryption_state` non-async and infallible method is added to get the
|
||||
`EncryptionState` without running a network request.
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777)). One can
|
||||
safely replace:
|
||||
|
||||
```rust
|
||||
room.is_encrypted().await?
|
||||
```
|
||||
|
||||
by
|
||||
|
||||
```rust
|
||||
room.latest_encryption_state().await?.is_encrypted()
|
||||
```
|
||||
|
||||
- `ClientBuilder::passphrase` is renamed `session_passphrase`
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
|
||||
- Merge `Timeline::send_thread_reply` into `Timeline::send_reply`. This
|
||||
changes the parameters of `send_reply` which now requires passing the
|
||||
event ID (and thread reply behaviour) inside a `ReplyParameters` struct.
|
||||
([#4880](https://github.com/matrix-org/matrix-rust-sdk/pull/4880/))
|
||||
|
||||
- The `dynamic_registrations_file` field of `OidcConfiguration` was removed.
|
||||
Clients are supposed to re-register with the homeserver for every login.
|
||||
|
||||
- `RoomPreview::own_membership_details` is now `RoomPreview::member_with_sender_info`, takes any user id and returns an `Option<RoomMemberWithSenderInfo>`.
|
||||
|
||||
Additions:
|
||||
|
||||
- Add `Encryption::get_user_identity` which returns `UserIdentity`
|
||||
- Add `ClientBuilder::room_key_recipient_strategy`
|
||||
- Add `Room::send_raw`
|
||||
- Add `NotificationSettings::set_custom_push_rule`
|
||||
- Expose `withdraw_verification` to `UserIdentity`
|
||||
- Expose `report_room` to `Room`
|
||||
- Add `RoomInfo::encryption_state`
|
||||
([#4788](https://github.com/matrix-org/matrix-rust-sdk/pull/4788))
|
||||
- Add `Timeline::send_thread_reply` for clients that need to start threads
|
||||
themselves.
|
||||
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
|
||||
- Add `ClientBuilder::session_pool_max_size`, `::session_cache_size` and `::session_journal_size_limit` to control the stores configuration, especially their memory consumption
|
||||
([#4870](https://github.com/matrix-org/matrix-rust-sdk/pull/4870/))
|
||||
- Add `ClientBuilder::system_is_memory_constrained` to indicate that the system
|
||||
has less memory available than the current standard
|
||||
([#4894](https://github.com/matrix-org/matrix-rust-sdk/pull/4894))
|
||||
- Add `Room::member_with_sender_info` to get both a room member's info and for the user who sent the `m.room.member` event the `RoomMember` is based on.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.2.0"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
@@ -8,6 +8,7 @@ license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
rust-version = { workspace = true }
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
@@ -23,7 +24,7 @@ vergen = { version = "8.1.3", features = ["build", "git", "gitcl"] }
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
as_variant = { workspace = true }
|
||||
async-compat = "0.2.1"
|
||||
async-compat = "0.2.4"
|
||||
eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-util = { workspace = true }
|
||||
@@ -55,7 +56,6 @@ workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"rustls-tls", # note: differ from block below
|
||||
@@ -69,7 +69,6 @@ workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"native-tls", # note: differ from block above
|
||||
@@ -82,4 +81,4 @@ features = [
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
release = true
|
||||
|
||||
@@ -5,18 +5,14 @@ use std::{
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oidc::{
|
||||
registrations::OidcRegistrationsError,
|
||||
types::{
|
||||
iana::oauth::OAuthClientAuthenticationMethod,
|
||||
oidc::ApplicationType,
|
||||
registration::{ClientMetadata, Localized, VerifiedClientMetadata},
|
||||
requests::GrantType,
|
||||
},
|
||||
OidcError as SdkOidcError,
|
||||
authentication::oauth::{
|
||||
error::OAuthAuthorizationCodeError,
|
||||
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
|
||||
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
use ruma::serde::Raw;
|
||||
use url::Url;
|
||||
|
||||
use crate::client::{Client, OidcPrompt, SlidingSyncVersion};
|
||||
@@ -116,7 +112,7 @@ pub struct OidcConfiguration {
|
||||
/// successful.
|
||||
pub redirect_uri: String,
|
||||
/// A URI that contains information about the client.
|
||||
pub client_uri: Option<String>,
|
||||
pub client_uri: String,
|
||||
/// A URI that contains the client's logo.
|
||||
pub logo_uri: Option<String>,
|
||||
/// A URI that contains the client's terms of service.
|
||||
@@ -126,50 +122,68 @@ pub struct OidcConfiguration {
|
||||
/// An array of e-mail addresses of people responsible for this client.
|
||||
pub contacts: Option<Vec<String>>,
|
||||
|
||||
/// Pre-configured registrations for use with issuers that don't support
|
||||
/// Pre-configured registrations for use with homeservers that don't support
|
||||
/// dynamic client registration.
|
||||
pub static_registrations: HashMap<String, String>,
|
||||
|
||||
/// A file path where any dynamic registrations should be stored.
|
||||
///
|
||||
/// Suggested value: `{base_path}/oidc/registrations.json`
|
||||
pub dynamic_registrations_file: String,
|
||||
/// The keys of the map should be the URLs of the homeservers, but keys
|
||||
/// using `issuer` URLs are also supported.
|
||||
pub static_registrations: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl TryInto<VerifiedClientMetadata> for &OidcConfiguration {
|
||||
type Error = OidcError;
|
||||
impl OidcConfiguration {
|
||||
pub(crate) fn redirect_uri(&self) -> Result<Url, OidcError> {
|
||||
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)
|
||||
}
|
||||
|
||||
fn try_into(self) -> Result<VerifiedClientMetadata, Self::Error> {
|
||||
let redirect_uri =
|
||||
Url::parse(&self.redirect_uri).map_err(|_| OidcError::CallbackUrlInvalid)?;
|
||||
pub(crate) fn client_metadata(&self) -> Result<Raw<ClientMetadata>, OidcError> {
|
||||
let redirect_uri = self.redirect_uri()?;
|
||||
let client_name = self.client_name.as_ref().map(|n| Localized::new(n.to_owned(), []));
|
||||
let client_uri = self.client_uri.localized_url()?;
|
||||
let logo_uri = self.logo_uri.localized_url()?;
|
||||
let policy_uri = self.policy_uri.localized_url()?;
|
||||
let tos_uri = self.tos_uri.localized_url()?;
|
||||
let contacts = self.contacts.clone();
|
||||
|
||||
ClientMetadata {
|
||||
application_type: Some(ApplicationType::Native),
|
||||
redirect_uris: Some(vec![redirect_uri]),
|
||||
grant_types: Some(vec![
|
||||
GrantType::RefreshToken,
|
||||
GrantType::AuthorizationCode,
|
||||
GrantType::DeviceCode,
|
||||
]),
|
||||
// A native client shouldn't use authentication as the credentials could be intercepted.
|
||||
token_endpoint_auth_method: Some(OAuthClientAuthenticationMethod::None),
|
||||
let metadata = ClientMetadata {
|
||||
// The server should display the following fields when getting the user's consent.
|
||||
client_name,
|
||||
contacts,
|
||||
client_uri,
|
||||
logo_uri,
|
||||
policy_uri,
|
||||
tos_uri,
|
||||
..Default::default()
|
||||
..ClientMetadata::new(
|
||||
ApplicationType::Native,
|
||||
vec![
|
||||
OAuthGrantType::AuthorizationCode { redirect_uris: vec![redirect_uri] },
|
||||
OAuthGrantType::DeviceCode,
|
||||
],
|
||||
client_uri,
|
||||
)
|
||||
};
|
||||
|
||||
Raw::new(&metadata).map_err(|_| OidcError::MetadataInvalid)
|
||||
}
|
||||
|
||||
pub(crate) fn registration_data(&self) -> Result<ClientRegistrationData, OidcError> {
|
||||
let client_metadata = self.client_metadata()?;
|
||||
|
||||
let mut registration_data = ClientRegistrationData::new(client_metadata);
|
||||
|
||||
if !self.static_registrations.is_empty() {
|
||||
let static_registrations = self
|
||||
.static_registrations
|
||||
.iter()
|
||||
.filter_map(|(issuer, client_id)| {
|
||||
let Ok(issuer) = Url::parse(issuer) else {
|
||||
tracing::error!("Failed to parse {:?}", issuer);
|
||||
return None;
|
||||
};
|
||||
Some((issuer, ClientId::new(client_id.clone())))
|
||||
})
|
||||
.collect();
|
||||
|
||||
registration_data.static_registrations = Some(static_registrations);
|
||||
}
|
||||
.validate()
|
||||
.map_err(|_| OidcError::MetadataInvalid)
|
||||
|
||||
Ok(registration_data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +196,6 @@ pub enum OidcError {
|
||||
NotSupported,
|
||||
#[error("Unable to use OIDC as the supplied client metadata is invalid.")]
|
||||
MetadataInvalid,
|
||||
#[error("Failed to use the supplied registrations file path.")]
|
||||
RegistrationsPathInvalid,
|
||||
#[error("The supplied callback URL used to complete OIDC is invalid.")]
|
||||
CallbackUrlInvalid,
|
||||
#[error("The OIDC login was cancelled by the user.")]
|
||||
@@ -193,23 +205,17 @@ pub enum OidcError {
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
impl From<SdkOidcError> for OidcError {
|
||||
fn from(e: SdkOidcError) -> OidcError {
|
||||
impl From<SdkOAuthError> for OidcError {
|
||||
fn from(e: SdkOAuthError) -> OidcError {
|
||||
match e {
|
||||
SdkOidcError::MissingAuthenticationIssuer => OidcError::NotSupported,
|
||||
SdkOidcError::MissingRedirectUri => OidcError::MetadataInvalid,
|
||||
SdkOidcError::InvalidCallbackUrl => OidcError::CallbackUrlInvalid,
|
||||
SdkOidcError::InvalidState => OidcError::CallbackUrlInvalid,
|
||||
SdkOidcError::CancelledAuthorization => OidcError::Cancelled,
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcRegistrationsError> for OidcError {
|
||||
fn from(e: OidcRegistrationsError) -> OidcError {
|
||||
match e {
|
||||
OidcRegistrationsError::InvalidFilePath => OidcError::RegistrationsPathInvalid,
|
||||
SdkOAuthError::Discovery(error) if error.is_not_supported() => OidcError::NotSupported,
|
||||
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::RedirectUri(_))
|
||||
| SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::InvalidState) => {
|
||||
OidcError::CallbackUrlInvalid
|
||||
}
|
||||
SdkOAuthError::AuthorizationCode(OAuthAuthorizationCodeError::Cancelled) => {
|
||||
OidcError::Cancelled
|
||||
}
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -218,7 +224,7 @@ impl From<OidcRegistrationsError> for OidcError {
|
||||
impl From<Error> for OidcError {
|
||||
fn from(e: Error) -> OidcError {
|
||||
match e {
|
||||
Error::Oidc(e) => e.into(),
|
||||
Error::OAuth(e) => (*e).into(),
|
||||
_ => OidcError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
@@ -227,17 +233,25 @@ impl From<Error> for OidcError {
|
||||
/* Helpers */
|
||||
|
||||
trait OptionExt {
|
||||
/// Convenience method to convert a string to a URL and returns it as a
|
||||
/// Localized URL. No localization is actually performed.
|
||||
/// Convenience method to convert an `Option<String>` to a URL and returns
|
||||
/// it as a Localized URL. No localization is actually performed.
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError>;
|
||||
}
|
||||
|
||||
impl OptionExt for Option<String> {
|
||||
fn localized_url(&self) -> Result<Option<Localized<Url>>, OidcError> {
|
||||
self.as_deref()
|
||||
.map(|uri| -> Result<Localized<Url>, OidcError> {
|
||||
Ok(Localized::new(Url::parse(uri).map_err(|_| OidcError::MetadataInvalid)?, []))
|
||||
})
|
||||
.transpose()
|
||||
self.as_deref().map(StrExt::localized_url).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
trait StrExt {
|
||||
/// Convenience method to convert a string to a URL and returns it as a
|
||||
/// Localized URL. No localization is actually performed.
|
||||
fn localized_url(&self) -> Result<Localized<Url>, OidcError>;
|
||||
}
|
||||
|
||||
impl StrExt for str {
|
||||
fn localized_url(&self) -> Result<Localized<Url>, OidcError> {
|
||||
Ok(Localized::new(Url::parse(self).map_err(|_| OidcError::MetadataInvalid)?, []))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,41 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
path::Path,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use async_compat::get_runtime_handle;
|
||||
use matrix_sdk::{
|
||||
authentication::oidc::{
|
||||
registrations::{ClientId, OidcRegistrations},
|
||||
requests::account_management::AccountManagementActionFull,
|
||||
types::{
|
||||
client_credentials::ClientCredentials,
|
||||
registration::{
|
||||
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
|
||||
},
|
||||
requests::Prompt as SdkOidcPrompt,
|
||||
},
|
||||
OidcAuthorizationData, OidcSession,
|
||||
authentication::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
event_cache::EventCacheError,
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaThumbnailSettings,
|
||||
MediaRetentionPolicy, MediaThumbnailSettings,
|
||||
},
|
||||
reqwest::StatusCode,
|
||||
ruma::{
|
||||
api::client::{
|
||||
discovery::get_authorization_server_metadata::msc2965::Prompt as RumaOidcPrompt,
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
session::get_login_types,
|
||||
user_directory::search_users,
|
||||
},
|
||||
events::{
|
||||
room::{avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent},
|
||||
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
|
||||
message::MessageType,
|
||||
},
|
||||
AnyInitialStateEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
AuthApi, AuthSession, Client as MatrixClient, HttpError, SessionChange, SessionTokens,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
NotificationClient as MatrixNotificationClient,
|
||||
@@ -47,16 +43,16 @@ use matrix_sdk_ui::notification_client::{
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
alias::get_alias, discovery::discover_homeserver::AuthenticationServerInfo,
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier},
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
key::verification::request::ToDeviceKeyVerificationRequestEvent,
|
||||
room::{
|
||||
history_visibility::RoomHistoryVisibilityEventContent,
|
||||
join_rules::{
|
||||
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
|
||||
},
|
||||
message::OriginalSyncRoomMessageEvent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
GlobalAccountDataEventType,
|
||||
@@ -70,13 +66,14 @@ use tokio::sync::broadcast::error::RecvError;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
|
||||
use super::{room::Room, session_verification::SessionVerificationController, RUNTIME};
|
||||
use super::{room::Room, session_verification::SessionVerificationController};
|
||||
use crate::{
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
notification::NotificationClient,
|
||||
notification_settings::NotificationSettings,
|
||||
room::RoomHistoryVisibility,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
room_preview::RoomPreview,
|
||||
ruma::{AuthData, MediaSource},
|
||||
@@ -204,12 +201,27 @@ impl Client {
|
||||
tokio::sync::RwLock<Option<SessionVerificationController>>,
|
||||
> = Default::default();
|
||||
let controller = session_verification_controller.clone();
|
||||
sdk_client.add_event_handler(
|
||||
move |event: ToDeviceKeyVerificationRequestEvent| async move {
|
||||
if let Some(session_verification_controller) = &*controller.clone().read().await {
|
||||
session_verification_controller
|
||||
.process_incoming_verification_request(
|
||||
&event.sender,
|
||||
event.content.transaction_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
sdk_client.add_event_handler(move |ev: AnyToDeviceEvent| async move {
|
||||
if let Some(session_verification_controller) = &*controller.clone().read().await {
|
||||
session_verification_controller.process_to_device_message(ev).await;
|
||||
} else {
|
||||
debug!("received to-device message, but verification controller isn't ready");
|
||||
let controller = session_verification_controller.clone();
|
||||
sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
|
||||
if let MessageType::VerificationRequest(_) = &event.content.msgtype {
|
||||
if let Some(session_verification_controller) = &*controller.clone().read().await {
|
||||
session_verification_controller
|
||||
.process_incoming_verification_request(&event.sender, event.event_id)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -231,7 +243,7 @@ impl Client {
|
||||
|
||||
client
|
||||
.inner
|
||||
.oidc()
|
||||
.oauth()
|
||||
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
|
||||
.await?;
|
||||
}
|
||||
@@ -264,27 +276,16 @@ impl Client {
|
||||
impl Client {
|
||||
/// Information about login options for the client's homeserver.
|
||||
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
|
||||
let oidc = self.inner.oidc();
|
||||
let (supports_oidc_login, supported_oidc_prompts) = match oidc
|
||||
.fetch_authentication_issuer()
|
||||
.await
|
||||
{
|
||||
Ok(issuer) => match &oidc.given_provider_metadata(&issuer).await {
|
||||
Ok(metadata) => {
|
||||
let prompts = metadata
|
||||
.prompt_values_supported
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |prompts| prompts.iter().map(Into::into).collect());
|
||||
let oauth = self.inner.oauth();
|
||||
let (supports_oidc_login, supported_oidc_prompts) = match oauth.server_metadata().await {
|
||||
Ok(metadata) => {
|
||||
let prompts =
|
||||
metadata.prompt_values_supported.into_iter().map(Into::into).collect();
|
||||
|
||||
(true, prompts)
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed to fetch OIDC provider metadata: {error}");
|
||||
(true, Default::default())
|
||||
}
|
||||
},
|
||||
(true, prompts)
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed to fetch authentication issuer: {error}");
|
||||
error!("Failed to fetch OIDC provider metadata: {error}");
|
||||
(false, Default::default())
|
||||
}
|
||||
};
|
||||
@@ -389,51 +390,46 @@ impl Client {
|
||||
/// view has succeeded, call `login_with_oidc_callback` with the callback it
|
||||
/// returns. If a failure occurs and a callback isn't available, make sure
|
||||
/// to call `abort_oidc_auth` to inform the client of this.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `oidc_configuration` - The configuration used to load the credentials
|
||||
/// of the client if it is already registered with the authorization
|
||||
/// server, or register the client and store its credentials if it isn't.
|
||||
///
|
||||
/// * `prompt` - The desired user experience in the web UI. No value means
|
||||
/// that the user wishes to login into an existing account, and a value of
|
||||
/// `Create` means that the user wishes to register a new account.
|
||||
pub async fn url_for_oidc(
|
||||
&self,
|
||||
oidc_configuration: &OidcConfiguration,
|
||||
prompt: OidcPrompt,
|
||||
) -> Result<Arc<OidcAuthorizationData>, OidcError> {
|
||||
let oidc_metadata: VerifiedClientMetadata = oidc_configuration.try_into()?;
|
||||
let registrations_file = Path::new(&oidc_configuration.dynamic_registrations_file);
|
||||
let static_registrations = oidc_configuration
|
||||
.static_registrations
|
||||
.iter()
|
||||
.filter_map(|(issuer, client_id)| {
|
||||
let Ok(issuer) = Url::parse(issuer) else {
|
||||
tracing::error!("Failed to parse {:?}", issuer);
|
||||
return None;
|
||||
};
|
||||
Some((issuer, ClientId(client_id.clone())))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
let registrations = OidcRegistrations::new(
|
||||
registrations_file,
|
||||
oidc_metadata.clone(),
|
||||
static_registrations,
|
||||
)?;
|
||||
prompt: Option<OidcPrompt>,
|
||||
) -> Result<Arc<OAuthAuthorizationData>, OidcError> {
|
||||
let registration_data = oidc_configuration.registration_data()?;
|
||||
let redirect_uri = oidc_configuration.redirect_uri()?;
|
||||
|
||||
let data =
|
||||
self.inner.oidc().url_for_oidc(oidc_metadata, registrations, prompt.into()).await?;
|
||||
let mut url_builder = self.inner.oauth().login(redirect_uri, None, Some(registration_data));
|
||||
|
||||
if let Some(prompt) = prompt {
|
||||
url_builder = url_builder.prompt(vec![prompt.into()]);
|
||||
}
|
||||
|
||||
let data = url_builder.build().await?;
|
||||
|
||||
Ok(Arc::new(data))
|
||||
}
|
||||
|
||||
/// Aborts an existing OIDC login operation that might have been cancelled,
|
||||
/// failed etc.
|
||||
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OidcAuthorizationData>) {
|
||||
self.inner.oidc().abort_authorization(&authorization_data.state).await;
|
||||
pub async fn abort_oidc_auth(&self, authorization_data: Arc<OAuthAuthorizationData>) {
|
||||
self.inner.oauth().abort_login(&authorization_data.state).await;
|
||||
}
|
||||
|
||||
/// Completes the OIDC login process.
|
||||
pub async fn login_with_oidc_callback(
|
||||
&self,
|
||||
authorization_data: Arc<OidcAuthorizationData>,
|
||||
callback_url: String,
|
||||
) -> Result<(), OidcError> {
|
||||
pub async fn login_with_oidc_callback(&self, callback_url: String) -> Result<(), OidcError> {
|
||||
let url = Url::parse(&callback_url).or(Err(OidcError::CallbackUrlInvalid))?;
|
||||
|
||||
self.inner.oidc().login_with_oidc_callback(&authorization_data, url).await?;
|
||||
self.inner.oauth().finish_login(url.into()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -465,11 +461,34 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
///
|
||||
/// It reloads the entire set of rooms from the previous session.
|
||||
///
|
||||
/// If you want to control the amount of rooms to reloads, check
|
||||
/// [`Client::restore_session_with`].
|
||||
pub async fn restore_session(&self, session: Session) -> Result<(), ClientError> {
|
||||
self.restore_session_with(session, RoomLoadSettings::All).await
|
||||
}
|
||||
|
||||
/// Restores the client from a `Session`.
|
||||
///
|
||||
/// It reloads a set of rooms controlled by [`RoomLoadSettings`].
|
||||
pub async fn restore_session_with(
|
||||
&self,
|
||||
session: Session,
|
||||
room_load_settings: RoomLoadSettings,
|
||||
) -> Result<(), ClientError> {
|
||||
let sliding_sync_version = session.sliding_sync_version.clone();
|
||||
let auth_session: AuthSession = session.try_into()?;
|
||||
|
||||
self.restore_session_inner(auth_session).await?;
|
||||
self.inner
|
||||
.restore_session_with(
|
||||
auth_session,
|
||||
room_load_settings
|
||||
.try_into()
|
||||
.map_err(|error| ClientError::Generic { msg: error })?,
|
||||
)
|
||||
.await?;
|
||||
self.inner.set_sliding_sync_version(sliding_sync_version.try_into()?);
|
||||
|
||||
Ok(())
|
||||
@@ -498,7 +517,7 @@ impl Client {
|
||||
let q = self.inner.send_queue();
|
||||
let mut subscriber = q.subscribe_errors();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
// Respawn tasks for rooms that had unsent events. At this point we've just
|
||||
// created the subscriber, so it'll be notified about errors.
|
||||
q.respawn_tasks_for_rooms_with_unsent_requests().await;
|
||||
@@ -533,15 +552,6 @@ impl Client {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Restores the client from an `AuthSession`.
|
||||
pub(crate) async fn restore_session_inner(
|
||||
&self,
|
||||
session: impl Into<AuthSession>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.inner.restore_session(session).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether or not the client's homeserver supports the password login flow.
|
||||
pub(crate) async fn supports_password_login(&self) -> anyhow::Result<bool> {
|
||||
let login_types = self.inner.matrix_auth().get_login_types().await?;
|
||||
@@ -578,7 +588,7 @@ impl Client {
|
||||
delegate.map(|delegate| {
|
||||
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
|
||||
let client_clone = self.clone();
|
||||
let session_change_task = RUNTIME.spawn(async move {
|
||||
let session_change_task = get_runtime_handle().spawn(async move {
|
||||
loop {
|
||||
match session_change_receiver.recv().await {
|
||||
Ok(session_change) => client_clone.process_session_change(session_change),
|
||||
@@ -604,17 +614,24 @@ impl Client {
|
||||
&self,
|
||||
action: Option<AccountManagementAction>,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
if !matches!(self.inner.auth_api(), Some(AuthApi::Oidc(..))) {
|
||||
if !matches!(self.inner.auth_api(), Some(AuthApi::OAuth(..))) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match self.inner.oidc().account_management_url(action.map(Into::into)).await {
|
||||
Ok(url) => Ok(url.map(|u| u.to_string())),
|
||||
let mut url_builder = match self.inner.oauth().account_management_url().await {
|
||||
Ok(Some(url_builder)) => url_builder,
|
||||
Ok(None) => return Ok(None),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed retrieving account management URL: {e}");
|
||||
Err(e.into())
|
||||
error!("Failed retrieving account management URL: {e}");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(action) = action {
|
||||
url_builder = url_builder.action(action.into());
|
||||
}
|
||||
|
||||
Ok(Some(url_builder.build().to_string()))
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> Result<String, ClientError> {
|
||||
@@ -663,8 +680,8 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Retrieves an avatar cached from a previous call to [`Self::avatar_url`].
|
||||
pub fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
|
||||
Ok(RUNTIME.block_on(self.inner.account().get_cached_avatar_url())?.map(Into::into))
|
||||
pub async fn cached_avatar_url(&self) -> Result<Option<String>, ClientError> {
|
||||
Ok(self.inner.account().get_cached_avatar_url().await?.map(Into::into))
|
||||
}
|
||||
|
||||
pub fn device_id(&self) -> Result<String, ClientError> {
|
||||
@@ -710,7 +727,7 @@ impl Client {
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
@@ -777,8 +794,11 @@ impl Client {
|
||||
.await?
|
||||
.context("Failed retrieving user identity")?;
|
||||
|
||||
let session_verification_controller =
|
||||
SessionVerificationController::new(self.inner.encryption(), user_identity);
|
||||
let session_verification_controller = SessionVerificationController::new(
|
||||
self.inner.encryption(),
|
||||
user_identity,
|
||||
self.inner.account(),
|
||||
);
|
||||
|
||||
*self.session_verification_controller.write().await =
|
||||
Some(session_verification_controller.clone());
|
||||
@@ -786,34 +806,9 @@ impl Client {
|
||||
Ok(Arc::new(session_verification_controller))
|
||||
}
|
||||
|
||||
/// Log out the current user. This method returns an optional URL that
|
||||
/// should be presented to the user to complete logout (in the case of
|
||||
/// Session having been authenticated using OIDC).
|
||||
pub async fn logout(&self) -> Result<Option<String>, ClientError> {
|
||||
let Some(auth_api) = self.inner.auth_api() else {
|
||||
return Err(anyhow!("Missing authentication API").into());
|
||||
};
|
||||
|
||||
match auth_api {
|
||||
AuthApi::Matrix(a) => {
|
||||
tracing::info!("Logging out via the homeserver.");
|
||||
a.logout().await?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
AuthApi::Oidc(api) => {
|
||||
tracing::info!("Logging out via OIDC.");
|
||||
let end_session_builder = api.logout().await?;
|
||||
|
||||
if let Some(builder) = end_session_builder {
|
||||
let url = builder.build()?.url;
|
||||
return Ok(Some(url.to_string()));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
_ => Err(anyhow!("Unknown authentication API").into()),
|
||||
}
|
||||
/// Log the current user out.
|
||||
pub async fn logout(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.logout().await?)
|
||||
}
|
||||
|
||||
/// Registers a pusher with given parameters
|
||||
@@ -870,6 +865,24 @@ impl Client {
|
||||
self.inner.rooms().into_iter().map(|room| Arc::new(Room::new(room))).collect()
|
||||
}
|
||||
|
||||
/// Get a room by its ID.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The ID of the room to get.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Result` containing an optional room, or a `ClientError`.
|
||||
/// This method will not initialize the room's timeline or populate it with
|
||||
/// events.
|
||||
pub fn get_room(&self, room_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let sdk_room = self.inner.get_room(&room_id);
|
||||
let room = sdk_room.map(|room| Arc::new(Room::new(room)));
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let sdk_room = self.inner.get_dm_room(&user_id);
|
||||
@@ -913,8 +926,8 @@ impl Client {
|
||||
SyncServiceBuilder::new((*self.inner).clone())
|
||||
}
|
||||
|
||||
pub fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
let inner = RUNTIME.block_on(self.inner.notification_settings());
|
||||
pub async fn get_notification_settings(&self) -> Arc<NotificationSettings> {
|
||||
let inner = self.inner.notification_settings().await;
|
||||
|
||||
Arc::new(NotificationSettings::new((*self.inner).clone(), inner))
|
||||
}
|
||||
@@ -959,7 +972,7 @@ impl Client {
|
||||
listener: Box<dyn IgnoredUsersListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_to_ignore_user_list_changes();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(user_ids) = subscriber.next().await {
|
||||
listener.call(user_ids);
|
||||
}
|
||||
@@ -1044,11 +1057,12 @@ impl Client {
|
||||
let room_alias = RoomAliasId::parse(&room_alias)?;
|
||||
match self.inner.resolve_room_alias(&room_alias).await {
|
||||
Ok(response) => Ok(Some(response.into())),
|
||||
Err(HttpError::Reqwest(http_error)) => match http_error.status() {
|
||||
Some(StatusCode::NOT_FOUND) => Ok(None),
|
||||
_ => Err(http_error.into()),
|
||||
Err(error) => match error.client_api_error_kind() {
|
||||
// The room alias wasn't found, so we return None.
|
||||
Some(ErrorKind::NotFound) => Ok(None),
|
||||
// In any other case we just return the error, mapped.
|
||||
_ => Err(error.into()),
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1152,6 +1166,42 @@ impl Client {
|
||||
let alias = RoomAliasId::parse(alias)?;
|
||||
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Set the media retention policy.
|
||||
pub async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, EventCacheError> {
|
||||
let store = self.inner.event_cache_store().lock().await?;
|
||||
Ok(store.set_media_retention_policy(policy).await?)
|
||||
};
|
||||
|
||||
Ok(closure().await?)
|
||||
}
|
||||
|
||||
/// Clear all the non-critical caches for this Client instance.
|
||||
///
|
||||
/// - This will empty all the room's persisted event caches, so all rooms
|
||||
/// will start as if they were empty.
|
||||
/// - This will empty the media cache according to the current media
|
||||
/// retention policy.
|
||||
pub async fn clear_caches(&self) -> Result<(), ClientError> {
|
||||
let closure = async || -> Result<_, EventCacheError> {
|
||||
// Clean up the media cache according to the current media retention policy.
|
||||
self.inner.event_cache_store().lock().await?.clean_up_media_cache().await?;
|
||||
|
||||
// Clear all the room chunks. It's important to *not* call
|
||||
// `EventCacheStore::clear_all_rooms_chunks` here, because there might be live
|
||||
// observers of the linked chunks, and that would cause some very bad state
|
||||
// mismatch.
|
||||
self.inner.event_cache().clear_all_rooms().await?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Ok(closure().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -1232,7 +1282,7 @@ impl Client {
|
||||
fn process_session_change(&self, session_change: SessionChange) {
|
||||
if let Some(delegate) = self.delegate.read().unwrap().clone() {
|
||||
debug!("Applying session change: {session_change:?}");
|
||||
RUNTIME.spawn_blocking(move || match session_change {
|
||||
get_runtime_handle().spawn_blocking(move || match session_change {
|
||||
SessionChange::UnknownToken { soft_logout } => {
|
||||
delegate.did_receive_auth_error(soft_logout);
|
||||
}
|
||||
@@ -1251,13 +1301,7 @@ impl Client {
|
||||
session_delegate: Arc<dyn ClientSessionDelegate>,
|
||||
user_id: &UserId,
|
||||
) -> anyhow::Result<SessionTokens> {
|
||||
let session = session_delegate.retrieve_session_from_keychain(user_id.to_string())?;
|
||||
let auth_session = TryInto::<AuthSession>::try_into(session)?;
|
||||
match auth_session {
|
||||
AuthSession::Oidc(session) => Ok(SessionTokens::Oidc(session.user.tokens)),
|
||||
AuthSession::Matrix(session) => Ok(SessionTokens::Matrix(session.tokens)),
|
||||
_ => anyhow::bail!("Unexpected session kind."),
|
||||
}
|
||||
Ok(session_delegate.retrieve_session_from_keychain(user_id.to_string())?.into_tokens())
|
||||
}
|
||||
|
||||
fn session_inner(client: matrix_sdk::Client) -> Result<Session, ClientError> {
|
||||
@@ -1279,6 +1323,38 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure how many rooms will be restored when restoring the session with
|
||||
/// [`Client::restore_session_with`].
|
||||
///
|
||||
/// Please, see the documentation of [`matrix_sdk::store::RoomLoadSettings`] to
|
||||
/// learn more.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomLoadSettings {
|
||||
/// Load all rooms from the `StateStore` into the in-memory state store
|
||||
/// `BaseStateStore`.
|
||||
All,
|
||||
|
||||
/// Load a single room from the `StateStore` into the in-memory state
|
||||
/// store `BaseStateStore`.
|
||||
///
|
||||
/// Please, be careful with this option. Read the documentation of
|
||||
/// [`RoomLoadSettings`].
|
||||
One { room_id: String },
|
||||
}
|
||||
|
||||
impl TryInto<SdkRoomLoadSettings> for RoomLoadSettings {
|
||||
type Error = String;
|
||||
|
||||
fn try_into(self) -> Result<SdkRoomLoadSettings, Self::Error> {
|
||||
Ok(match self {
|
||||
Self::All => SdkRoomLoadSettings::All,
|
||||
Self::One { room_id } => {
|
||||
SdkRoomLoadSettings::One(RoomId::parse(room_id).map_err(|error| error.to_string())?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct NotificationPowerLevels {
|
||||
pub room: i32,
|
||||
@@ -1378,6 +1454,8 @@ pub struct CreateRoomParameters {
|
||||
#[uniffi(default = None)]
|
||||
pub join_rule_override: Option<JoinRule>,
|
||||
#[uniffi(default = None)]
|
||||
pub history_visibility_override: Option<RoomHistoryVisibility>,
|
||||
#[uniffi(default = None)]
|
||||
pub canonical_alias: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1425,6 +1503,12 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
|
||||
initial_state.push(InitialStateEvent::new(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());
|
||||
}
|
||||
|
||||
request.initial_state = initial_state;
|
||||
|
||||
if let Some(power_levels) = value.power_level_content_override {
|
||||
@@ -1537,11 +1621,7 @@ impl Session {
|
||||
AuthApi::Matrix(a) => {
|
||||
let matrix_sdk::authentication::matrix::MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
|
||||
} = a.session().context("Missing session")?;
|
||||
|
||||
Ok(Session {
|
||||
@@ -1555,30 +1635,13 @@ impl Session {
|
||||
})
|
||||
}
|
||||
// Build the session from the OIDC UserSession.
|
||||
AuthApi::Oidc(api) => {
|
||||
let matrix_sdk::authentication::oidc::UserSession {
|
||||
AuthApi::OAuth(api) => {
|
||||
let matrix_sdk::authentication::oauth::UserSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::authentication::oidc::OidcSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
},
|
||||
issuer,
|
||||
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
|
||||
} = api.user_session().context("Missing session")?;
|
||||
let client_id = api
|
||||
.client_credentials()
|
||||
.context("OIDC client credentials are missing.")?
|
||||
.client_id()
|
||||
.to_owned();
|
||||
let client_metadata =
|
||||
api.client_metadata().context("OIDC client metadata is missing.")?.clone();
|
||||
let oidc_data = OidcSessionData {
|
||||
client_id,
|
||||
client_metadata,
|
||||
latest_id_token: latest_id_token.map(|t| t.to_string()),
|
||||
issuer,
|
||||
};
|
||||
let client_id = api.client_id().context("OIDC client ID is missing.")?.clone();
|
||||
let oidc_data = OidcSessionData { client_id };
|
||||
|
||||
let oidc_data = serde_json::to_string(&oidc_data).ok();
|
||||
Ok(Session {
|
||||
@@ -1594,6 +1657,10 @@ impl Session {
|
||||
_ => Err(anyhow!("Unknown authentication API").into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_tokens(self) -> matrix_sdk::SessionTokens {
|
||||
SessionTokens { access_token: self.access_token, refresh_token: self.refresh_token }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Session> for AuthSession {
|
||||
@@ -1611,35 +1678,19 @@ impl TryFrom<Session> for AuthSession {
|
||||
|
||||
if let Some(oidc_data) = oidc_data {
|
||||
// Create an OidcSession.
|
||||
let oidc_data = serde_json::from_str::<OidcUnvalidatedSessionData>(&oidc_data)?
|
||||
.validate()
|
||||
.context("OIDC metadata validation failed.")?;
|
||||
let latest_id_token = oidc_data
|
||||
.latest_id_token
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.context("OIDC latest_id_token is invalid.")?;
|
||||
let oidc_data = serde_json::from_str::<OidcSessionData>(&oidc_data)?;
|
||||
|
||||
let user_session = matrix_sdk::authentication::oidc::UserSession {
|
||||
let user_session = matrix_sdk::authentication::oauth::UserSession {
|
||||
meta: matrix_sdk::SessionMeta {
|
||||
user_id: user_id.try_into()?,
|
||||
device_id: device_id.into(),
|
||||
},
|
||||
tokens: matrix_sdk::authentication::oidc::OidcSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
latest_id_token,
|
||||
},
|
||||
issuer: oidc_data.issuer,
|
||||
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
|
||||
};
|
||||
|
||||
let session = OidcSession {
|
||||
credentials: ClientCredentials::None { client_id: oidc_data.client_id },
|
||||
metadata: oidc_data.client_metadata,
|
||||
user: user_session,
|
||||
};
|
||||
let session = OAuthSession { client_id: oidc_data.client_id, user: user_session };
|
||||
|
||||
Ok(AuthSession::Oidc(session.into()))
|
||||
Ok(AuthSession::OAuth(session.into()))
|
||||
} else {
|
||||
// Create a regular Matrix Session.
|
||||
let session = matrix_sdk::authentication::matrix::MatrixSession {
|
||||
@@ -1647,10 +1698,7 @@ impl TryFrom<Session> for AuthSession {
|
||||
user_id: user_id.try_into()?,
|
||||
device_id: device_id.into(),
|
||||
},
|
||||
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
|
||||
};
|
||||
|
||||
Ok(AuthSession::Matrix(session))
|
||||
@@ -1660,64 +1708,9 @@ impl TryFrom<Session> for AuthSession {
|
||||
|
||||
/// Represents a client registration against an OpenID Connect authentication
|
||||
/// issuer.
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct OidcSessionData {
|
||||
client_id: String,
|
||||
client_metadata: VerifiedClientMetadata,
|
||||
latest_id_token: Option<String>,
|
||||
issuer: String,
|
||||
}
|
||||
|
||||
/// Represents an unverified client registration against an OpenID Connect
|
||||
/// authentication issuer. Call `validate` on this to use it for restoration.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(try_from = "OidcUnvalidatedSessionDataDeHelper")]
|
||||
pub(crate) struct OidcUnvalidatedSessionData {
|
||||
client_id: String,
|
||||
client_metadata: ClientMetadata,
|
||||
latest_id_token: Option<String>,
|
||||
issuer: String,
|
||||
}
|
||||
|
||||
impl OidcUnvalidatedSessionData {
|
||||
/// Validates the data so that it can be used.
|
||||
fn validate(self) -> Result<OidcSessionData, ClientMetadataVerificationError> {
|
||||
Ok(OidcSessionData {
|
||||
client_id: self.client_id,
|
||||
client_metadata: self.client_metadata.validate()?,
|
||||
latest_id_token: self.latest_id_token,
|
||||
issuer: self.issuer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OidcUnvalidatedSessionDataDeHelper {
|
||||
client_id: String,
|
||||
client_metadata: ClientMetadata,
|
||||
latest_id_token: Option<String>,
|
||||
issuer_info: Option<AuthenticationServerInfo>,
|
||||
issuer: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<OidcUnvalidatedSessionDataDeHelper> for OidcUnvalidatedSessionData {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: OidcUnvalidatedSessionDataDeHelper) -> Result<Self, Self::Error> {
|
||||
let OidcUnvalidatedSessionDataDeHelper {
|
||||
client_id,
|
||||
client_metadata,
|
||||
latest_id_token,
|
||||
issuer_info,
|
||||
issuer,
|
||||
} = value;
|
||||
|
||||
let issuer = issuer
|
||||
.or(issuer_info.map(|info| info.issuer))
|
||||
.ok_or_else(|| "missing field `issuer`".to_owned())?;
|
||||
|
||||
Ok(Self { client_id, client_metadata, latest_id_token, issuer })
|
||||
}
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -1735,8 +1728,12 @@ impl From<AccountManagementAction> for AccountManagementActionFull {
|
||||
match value {
|
||||
AccountManagementAction::Profile => Self::Profile,
|
||||
AccountManagementAction::SessionsList => Self::SessionsList,
|
||||
AccountManagementAction::SessionView { device_id } => Self::SessionView { device_id },
|
||||
AccountManagementAction::SessionEnd { device_id } => Self::SessionEnd { device_id },
|
||||
AccountManagementAction::SessionView { device_id } => {
|
||||
Self::SessionView { device_id: device_id.into() }
|
||||
}
|
||||
AccountManagementAction::SessionEnd { device_id } => {
|
||||
Self::SessionEnd { device_id: device_id.into() }
|
||||
}
|
||||
AccountManagementAction::AccountDeactivate => Self::AccountDeactivate,
|
||||
AccountManagementAction::CrossSigningReset => Self::CrossSigningReset,
|
||||
}
|
||||
@@ -1798,7 +1795,6 @@ impl MediaFileHandle {
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SlidingSyncVersion {
|
||||
None,
|
||||
Proxy { url: String },
|
||||
Native,
|
||||
}
|
||||
|
||||
@@ -1806,7 +1802,6 @@ impl From<SdkSlidingSyncVersion> for SlidingSyncVersion {
|
||||
fn from(value: SdkSlidingSyncVersion) -> Self {
|
||||
match value {
|
||||
SdkSlidingSyncVersion::None => Self::None,
|
||||
SdkSlidingSyncVersion::Proxy { url } => Self::Proxy { url: url.to_string() },
|
||||
SdkSlidingSyncVersion::Native => Self::Native,
|
||||
}
|
||||
}
|
||||
@@ -1818,9 +1813,6 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
|
||||
fn try_from(value: SlidingSyncVersion) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SlidingSyncVersion::None => Self::None,
|
||||
SlidingSyncVersion::Proxy { url } => Self::Proxy {
|
||||
url: Url::parse(&url).map_err(|e| ClientError::Generic { msg: e.to_string() })?,
|
||||
},
|
||||
SlidingSyncVersion::Native => Self::Native,
|
||||
})
|
||||
}
|
||||
@@ -1828,9 +1820,11 @@ impl TryFrom<SlidingSyncVersion> for SdkSlidingSyncVersion {
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum OidcPrompt {
|
||||
/// The Authorization Server must not display any authentication or consent
|
||||
/// user interface pages.
|
||||
None,
|
||||
/// The Authorization Server should prompt the End-User to create a user
|
||||
/// account.
|
||||
///
|
||||
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
|
||||
Create,
|
||||
|
||||
/// The Authorization Server should prompt the End-User for
|
||||
/// reauthentication.
|
||||
@@ -1840,47 +1834,30 @@ pub enum OidcPrompt {
|
||||
/// returning information to the Client.
|
||||
Consent,
|
||||
|
||||
/// The Authorization Server should prompt the End-User to select a user
|
||||
/// account.
|
||||
///
|
||||
/// This enables an End-User who has multiple accounts at the Authorization
|
||||
/// Server to select amongst the multiple accounts that they might have
|
||||
/// current sessions for.
|
||||
SelectAccount,
|
||||
|
||||
/// The Authorization Server should prompt the End-User to create a user
|
||||
/// account.
|
||||
///
|
||||
/// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html).
|
||||
Create,
|
||||
|
||||
/// An unknown value.
|
||||
Unknown { value: String },
|
||||
}
|
||||
|
||||
impl From<&SdkOidcPrompt> for OidcPrompt {
|
||||
fn from(value: &SdkOidcPrompt) -> Self {
|
||||
impl From<RumaOidcPrompt> for OidcPrompt {
|
||||
fn from(value: RumaOidcPrompt) -> Self {
|
||||
match value {
|
||||
SdkOidcPrompt::None => Self::None,
|
||||
SdkOidcPrompt::Login => Self::Login,
|
||||
SdkOidcPrompt::Consent => Self::Consent,
|
||||
SdkOidcPrompt::SelectAccount => Self::SelectAccount,
|
||||
SdkOidcPrompt::Create => Self::Create,
|
||||
SdkOidcPrompt::Unknown(value) => Self::Unknown { value: value.to_owned() },
|
||||
_ => Self::Unknown { value: value.to_string() },
|
||||
RumaOidcPrompt::Create => Self::Create,
|
||||
value => match value.as_str() {
|
||||
"consent" => Self::Consent,
|
||||
"login" => Self::Login,
|
||||
_ => Self::Unknown { value: value.to_string() },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcPrompt> for SdkOidcPrompt {
|
||||
impl From<OidcPrompt> for RumaOidcPrompt {
|
||||
fn from(value: OidcPrompt) -> Self {
|
||||
match value {
|
||||
OidcPrompt::None => Self::None,
|
||||
OidcPrompt::Login => Self::Login,
|
||||
OidcPrompt::Consent => Self::Consent,
|
||||
OidcPrompt::SelectAccount => Self::SelectAccount,
|
||||
OidcPrompt::Create => Self::Create,
|
||||
OidcPrompt::Unknown { value } => Self::Unknown(value),
|
||||
OidcPrompt::Consent => Self::from("consent"),
|
||||
OidcPrompt::Login => Self::from("login"),
|
||||
OidcPrompt::Unknown { value } => value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
authentication::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
|
||||
crypto::{
|
||||
types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
|
||||
CollectStrategy, TrustRequirement,
|
||||
@@ -16,14 +17,13 @@ use matrix_sdk::{
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError,
|
||||
RumaApiError, SqliteStoreConfig,
|
||||
};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{client::Client, RUNTIME};
|
||||
use super::client::Client;
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, client::ClientSessionDelegate, error::ClientError,
|
||||
helpers::unwrap_or_clone_arc, task_handle::TaskHandle,
|
||||
@@ -104,7 +104,7 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
|
||||
_ => HumanQrLoginError::Unknown,
|
||||
},
|
||||
|
||||
QRCodeLoginError::Oidc(e) => {
|
||||
QRCodeLoginError::OAuth(e) => {
|
||||
if let Some(e) = e.as_request_token_error() {
|
||||
match e {
|
||||
DeviceCodeErrorResponseType::AccessDenied => HumanQrLoginError::Declined,
|
||||
@@ -153,8 +153,8 @@ pub enum QrLoginProgress {
|
||||
/// first digit is a zero.
|
||||
check_code_string: String,
|
||||
},
|
||||
/// We are waiting for the login and for the OIDC provider to give us an
|
||||
/// access token.
|
||||
/// We are waiting for the login and for the OAuth 2.0 authorization server
|
||||
/// to give us an access token.
|
||||
WaitingForToken { user_code: String },
|
||||
/// The login has successfully finished.
|
||||
Done,
|
||||
@@ -255,9 +255,13 @@ 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>,
|
||||
system_is_memory_constrained: bool,
|
||||
username: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
user_agent: Option<String>,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder,
|
||||
proxy: Option<String>,
|
||||
@@ -284,9 +288,13 @@ impl ClientBuilder {
|
||||
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,
|
||||
system_is_memory_constrained: false,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
|
||||
proxy: None,
|
||||
@@ -363,6 +371,74 @@ impl ClientBuilder {
|
||||
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`].
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn username(self: Arc<Self>, username: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.username = Some(username);
|
||||
@@ -387,12 +463,6 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -521,11 +591,29 @@ impl ClientBuilder {
|
||||
fs::create_dir_all(data_path)?;
|
||||
fs::create_dir_all(cache_path)?;
|
||||
|
||||
inner_builder = inner_builder.sqlite_store_with_cache_path(
|
||||
data_path,
|
||||
cache_path,
|
||||
builder.passphrase.as_deref(),
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
} else {
|
||||
debug!("Not using a store path.");
|
||||
}
|
||||
@@ -604,22 +692,10 @@ impl ClientBuilder {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::None)
|
||||
}
|
||||
SlidingSyncVersionBuilder::Proxy { url } => {
|
||||
inner_builder = inner_builder.sliding_sync_version_builder(
|
||||
MatrixSlidingSyncVersionBuilder::Proxy {
|
||||
url: Url::parse(&url)
|
||||
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
|
||||
},
|
||||
)
|
||||
}
|
||||
SlidingSyncVersionBuilder::Native => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::Native)
|
||||
}
|
||||
SlidingSyncVersionBuilder::DiscoverProxy => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverProxy)
|
||||
}
|
||||
SlidingSyncVersionBuilder::DiscoverNative => {
|
||||
inner_builder = inner_builder
|
||||
.sliding_sync_version_builder(MatrixSlidingSyncVersionBuilder::DiscoverNative)
|
||||
@@ -629,7 +705,8 @@ impl ClientBuilder {
|
||||
if let Some(config) = builder.request_config {
|
||||
let mut updated_config = matrix_sdk::config::RequestConfig::default();
|
||||
if let Some(retry_limit) = config.retry_limit {
|
||||
updated_config = updated_config.retry_limit(retry_limit);
|
||||
updated_config =
|
||||
updated_config.retry_limit(retry_limit.try_into().unwrap_or(usize::MAX));
|
||||
}
|
||||
if let Some(timeout) = config.timeout {
|
||||
updated_config = updated_config.timeout(Duration::from_millis(timeout));
|
||||
@@ -641,8 +718,9 @@ impl ClientBuilder {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(retry_timeout) = config.retry_timeout {
|
||||
updated_config = updated_config.retry_timeout(Duration::from_millis(retry_timeout));
|
||||
if let Some(max_retry_time) = config.max_retry_time {
|
||||
updated_config =
|
||||
updated_config.max_retry_time(Duration::from_millis(max_retry_time));
|
||||
}
|
||||
inner_builder = inner_builder.request_config(updated_config);
|
||||
}
|
||||
@@ -673,8 +751,8 @@ impl ClientBuilder {
|
||||
///
|
||||
/// This method will build the client and immediately attempt to log the
|
||||
/// client in using the provided [`QrCodeData`] using the login
|
||||
/// mechanism described in [MSC4108]. As such this methods requires OIDC
|
||||
/// support as well as sliding sync support.
|
||||
/// mechanism described in [MSC4108]. As such this methods requires OAuth
|
||||
/// 2.0 support as well as sliding sync support.
|
||||
///
|
||||
/// The usage of the progress_listener is required to transfer the
|
||||
/// [`CheckCode`] to the existing client.
|
||||
@@ -700,17 +778,18 @@ impl ClientBuilder {
|
||||
}
|
||||
})?;
|
||||
|
||||
let client_metadata =
|
||||
oidc_configuration.try_into().map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
let registration_data = oidc_configuration
|
||||
.registration_data()
|
||||
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
|
||||
|
||||
let oidc = client.inner.oidc();
|
||||
let login = oidc.login_with_qr_code(&qr_code_data.inner, client_metadata);
|
||||
let oauth = client.inner.oauth();
|
||||
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(®istration_data));
|
||||
|
||||
let mut progress = login.subscribe_to_progress();
|
||||
|
||||
// We create this task, which will get cancelled once it's dropped, just in case
|
||||
// the progress stream doesn't end.
|
||||
let _progress_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = progress.next().await {
|
||||
progress_listener.on_update(state.into());
|
||||
}
|
||||
@@ -722,8 +801,8 @@ impl ClientBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// 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,
|
||||
@@ -742,14 +821,12 @@ pub struct RequestConfig {
|
||||
/// Max number of concurrent requests. No value means no limits.
|
||||
max_concurrent_requests: Option<u64>,
|
||||
/// Base delay between retries.
|
||||
retry_timeout: Option<u64>,
|
||||
max_retry_time: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SlidingSyncVersionBuilder {
|
||||
None,
|
||||
Proxy { url: String },
|
||||
Native,
|
||||
DiscoverProxy,
|
||||
DiscoverNative,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption,
|
||||
@@ -9,7 +10,6 @@ use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -230,7 +230,7 @@ impl Encryption {
|
||||
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.backups().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
let Ok(state) = state else { continue };
|
||||
listener.on_update(state.into());
|
||||
@@ -267,7 +267,7 @@ impl Encryption {
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut stream = self.inner.recovery().state_stream();
|
||||
|
||||
let stream_task = TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let stream_task = TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(state) = stream.next().await {
|
||||
listener.on_update(state.into());
|
||||
}
|
||||
@@ -294,7 +294,7 @@ impl Encryption {
|
||||
let task = if let Some(listener) = progress_listener {
|
||||
let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
|
||||
|
||||
Some(RUNTIME.spawn(async move {
|
||||
Some(get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
listener.on_update(progress.into());
|
||||
@@ -335,7 +335,7 @@ impl Encryption {
|
||||
|
||||
let mut progress_stream = enable.subscribe_to_progress();
|
||||
|
||||
let task = RUNTIME.spawn(async move {
|
||||
let task = get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = progress_stream.next().await {
|
||||
let Ok(progress) = progress else { continue };
|
||||
progress_listener.on_update(progress.into());
|
||||
@@ -400,7 +400,7 @@ impl Encryption {
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.verification_state();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(verification_state) = subscriber.next().await {
|
||||
listener.on_update(verification_state.into());
|
||||
}
|
||||
@@ -478,15 +478,6 @@ impl UserIdentity {
|
||||
Ok(self.inner.pin().await?)
|
||||
}
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
///
|
||||
/// If an identity was previously verified and is not anymore it will be
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.withdraw_verification().await?)
|
||||
}
|
||||
|
||||
/// Get the public part of the Master key of this user identity.
|
||||
///
|
||||
/// The public part of the Master key is usually used to uniquely identify
|
||||
@@ -504,6 +495,28 @@ impl UserIdentity {
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.inner.is_verified()
|
||||
}
|
||||
|
||||
/// True if we verified this identity at some point in the past.
|
||||
///
|
||||
/// To reset this latch back to `false`, one must call
|
||||
/// [`UserIdentity::withdraw_verification()`].
|
||||
pub fn was_previously_verified(&self) -> bool {
|
||||
self.inner.was_previously_verified()
|
||||
}
|
||||
|
||||
/// Remove the requirement for this identity to be verified.
|
||||
///
|
||||
/// If an identity was previously verified and is not anymore it will be
|
||||
/// reported to the user. In order to remove this notice users have to
|
||||
/// verify again or to withdraw the verification requirement.
|
||||
pub(crate) async fn withdraw_verification(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.withdraw_verification().await?)
|
||||
}
|
||||
|
||||
/// Was this identity previously verified, and is no longer?
|
||||
pub fn has_verification_violation(&self) -> bool {
|
||||
self.inner.has_verification_violation()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -557,7 +570,7 @@ impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningRe
|
||||
fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
|
||||
match value {
|
||||
encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
|
||||
encryption::CrossSigningResetAuthType::Oidc(info) => Self::Oidc { info: info.into() },
|
||||
encryption::CrossSigningResetAuthType::OAuth(info) => Self::Oidc { info: info.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,8 +581,8 @@ pub struct OidcCrossSigningResetInfo {
|
||||
pub approval_url: String,
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
|
||||
fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
|
||||
impl From<&matrix_sdk::encryption::OAuthCrossSigningResetInfo> for OidcCrossSigningResetInfo {
|
||||
fn from(value: &matrix_sdk::encryption::OAuthCrossSigningResetInfo) -> Self {
|
||||
Self { approval_url: value.approval_url.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
@@ -137,8 +137,8 @@ impl From<sync_service::Error> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OidcError> for ClientError {
|
||||
fn from(e: OidcError) -> Self {
|
||||
impl From<OAuthError> for ClientError {
|
||||
fn from(e: OAuthError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
@@ -288,6 +288,8 @@ pub enum RoomError {
|
||||
TimelineUnavailable,
|
||||
#[error("Invalid thumbnail data")]
|
||||
InvalidThumbnailData,
|
||||
#[error("Invalid replied to event ID")]
|
||||
InvalidRepliedToEventId,
|
||||
#[error("Failed sending attachment")]
|
||||
FailedSendingAttachment,
|
||||
}
|
||||
|
||||
@@ -30,12 +30,10 @@ mod session_verification;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod timeline_event_filter;
|
||||
mod tracing;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
|
||||
|
||||
use self::{
|
||||
|
||||
@@ -5,7 +5,11 @@ use matrix_sdk_ui::notification_client::{
|
||||
};
|
||||
use ruma::{EventId, RoomId};
|
||||
|
||||
use crate::{client::Client, error::ClientError, event::TimelineEvent};
|
||||
use crate::{
|
||||
client::{Client, JoinRule},
|
||||
error::ClientError,
|
||||
event::TimelineEvent,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum NotificationEvent {
|
||||
@@ -25,9 +29,11 @@ pub struct NotificationRoomInfo {
|
||||
pub display_name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub canonical_alias: Option<String>,
|
||||
pub join_rule: Option<JoinRule>,
|
||||
pub joined_members_count: u64,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_public: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -42,6 +48,7 @@ pub struct NotificationItem {
|
||||
/// information to create a push context.
|
||||
pub is_noisy: Option<bool>,
|
||||
pub has_mention: Option<bool>,
|
||||
pub thread_id: Option<String>,
|
||||
}
|
||||
|
||||
impl NotificationItem {
|
||||
@@ -54,7 +61,6 @@ impl NotificationItem {
|
||||
NotificationEvent::Invite { sender: event.sender.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
event,
|
||||
sender_info: NotificationSenderInfo {
|
||||
@@ -66,12 +72,15 @@ impl NotificationItem {
|
||||
display_name: item.room_computed_display_name,
|
||||
avatar_url: item.room_avatar_url,
|
||||
canonical_alias: item.room_canonical_alias,
|
||||
join_rule: item.room_join_rule.try_into().ok(),
|
||||
joined_members_count: item.joined_members_count,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_public: item.is_room_public,
|
||||
},
|
||||
is_noisy: item.is_noisy,
|
||||
has_mention: item.has_mention,
|
||||
thread_id: item.thread_id.map(|t| t.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,350 @@ use matrix_sdk::{
|
||||
Client as MatrixClient,
|
||||
};
|
||||
use ruma::{
|
||||
push::{PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind},
|
||||
RoomId,
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
|
||||
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
|
||||
},
|
||||
Int, RoomId, UInt,
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
|
||||
use crate::error::NotificationSettingsError;
|
||||
|
||||
#[derive(Clone, Default, uniffi::Enum)]
|
||||
pub enum ComparisonOperator {
|
||||
/// Equals
|
||||
#[default]
|
||||
Eq,
|
||||
|
||||
/// Less than
|
||||
Lt,
|
||||
|
||||
/// Greater than
|
||||
Gt,
|
||||
|
||||
/// Greater or equal
|
||||
Ge,
|
||||
|
||||
/// Less or equal
|
||||
Le,
|
||||
}
|
||||
|
||||
impl From<SdkComparisonOperator> for ComparisonOperator {
|
||||
fn from(value: SdkComparisonOperator) -> Self {
|
||||
match value {
|
||||
SdkComparisonOperator::Eq => Self::Eq,
|
||||
SdkComparisonOperator::Lt => Self::Lt,
|
||||
SdkComparisonOperator::Gt => Self::Gt,
|
||||
SdkComparisonOperator::Ge => Self::Ge,
|
||||
SdkComparisonOperator::Le => Self::Le,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComparisonOperator> for SdkComparisonOperator {
|
||||
fn from(value: ComparisonOperator) -> Self {
|
||||
match value {
|
||||
ComparisonOperator::Eq => Self::Eq,
|
||||
ComparisonOperator::Lt => Self::Lt,
|
||||
ComparisonOperator::Gt => Self::Gt,
|
||||
ComparisonOperator::Ge => Self::Ge,
|
||||
ComparisonOperator::Le => Self::Le,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, uniffi::Enum)]
|
||||
pub enum JsonValue {
|
||||
/// Represents a `null` value.
|
||||
#[default]
|
||||
Null,
|
||||
|
||||
/// Represents a boolean.
|
||||
Bool { value: bool },
|
||||
|
||||
/// Represents an integer.
|
||||
Integer { value: i64 },
|
||||
|
||||
/// Represents a string.
|
||||
String { value: String },
|
||||
}
|
||||
|
||||
impl From<SdkJsonValue> for JsonValue {
|
||||
fn from(value: SdkJsonValue) -> Self {
|
||||
match value {
|
||||
SdkJsonValue::Null => Self::Null,
|
||||
SdkJsonValue::Bool(b) => Self::Bool { value: b },
|
||||
SdkJsonValue::Integer(i) => Self::Integer { value: i.into() },
|
||||
SdkJsonValue::String(s) => Self::String { value: s },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonValue> for SdkJsonValue {
|
||||
fn from(value: JsonValue) -> Self {
|
||||
match value {
|
||||
JsonValue::Null => Self::Null,
|
||||
JsonValue::Bool { value } => Self::Bool(value),
|
||||
JsonValue::Integer { value } => Self::Integer(Int::new(value).unwrap_or_default()),
|
||||
JsonValue::String { value } => Self::String(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum PushCondition {
|
||||
/// A glob pattern match on a field of the event.
|
||||
EventMatch {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The glob-style pattern to match against.
|
||||
///
|
||||
/// Patterns with no special glob characters should be treated as having
|
||||
/// asterisks prepended and appended when testing the condition.
|
||||
pattern: String,
|
||||
},
|
||||
|
||||
/// Matches unencrypted messages where `content.body` contains the owner's
|
||||
/// display name in that room.
|
||||
ContainsDisplayName,
|
||||
|
||||
/// Matches the current number of members in the room.
|
||||
RoomMemberCount { prefix: ComparisonOperator, count: u64 },
|
||||
|
||||
/// Takes into account the current power levels in the room, ensuring the
|
||||
/// sender of the event has high enough power to trigger the
|
||||
/// notification.
|
||||
SenderNotificationPermission {
|
||||
/// The field in the power level event the user needs a minimum power
|
||||
/// level for.
|
||||
///
|
||||
/// Fields must be specified under the `notifications` property in the
|
||||
/// power level event's `content`.
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// Exact value match on a property of the event.
|
||||
EventPropertyIs {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: JsonValue,
|
||||
},
|
||||
|
||||
/// Exact value match on a value in an array property of the event.
|
||||
EventPropertyContains {
|
||||
/// The [dot-separated path] of the property of the event to match.
|
||||
///
|
||||
/// [dot-separated path]: https://spec.matrix.org/latest/appendices/#dot-separated-property-paths
|
||||
key: String,
|
||||
|
||||
/// The value to match against.
|
||||
value: JsonValue,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
SdkPushCondition::RoomMemberCount { is } => {
|
||||
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
|
||||
}
|
||||
SdkPushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
}
|
||||
SdkPushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
}
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PushCondition> for SdkPushCondition {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
|
||||
is: RoomMemberCountIs {
|
||||
prefix: prefix.into(),
|
||||
count: UInt::new(count).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key }
|
||||
}
|
||||
PushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
}
|
||||
PushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RuleKind {
|
||||
/// User-configured rules that override all other kinds.
|
||||
Override,
|
||||
|
||||
/// Lowest priority user-defined rules.
|
||||
Underride,
|
||||
|
||||
/// Sender-specific rules.
|
||||
Sender,
|
||||
|
||||
/// Room-specific rules.
|
||||
Room,
|
||||
|
||||
/// Content-specific rules.
|
||||
Content,
|
||||
|
||||
Custom {
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SdkRuleKind> for RuleKind {
|
||||
fn from(value: SdkRuleKind) -> Self {
|
||||
match value {
|
||||
SdkRuleKind::Override => Self::Override,
|
||||
SdkRuleKind::Underride => Self::Underride,
|
||||
SdkRuleKind::Sender => Self::Sender,
|
||||
SdkRuleKind::Room => Self::Room,
|
||||
SdkRuleKind::Content => Self::Content,
|
||||
SdkRuleKind::_Custom(_) => Self::Custom { value: value.as_str().to_owned() },
|
||||
_ => Self::Custom { value: value.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RuleKind> for SdkRuleKind {
|
||||
fn from(value: RuleKind) -> Self {
|
||||
match value {
|
||||
RuleKind::Override => Self::Override,
|
||||
RuleKind::Underride => Self::Underride,
|
||||
RuleKind::Sender => Self::Sender,
|
||||
RuleKind::Room => Self::Room,
|
||||
RuleKind::Content => Self::Content,
|
||||
RuleKind::Custom { value } => SdkRuleKind::from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
/// Enum representing the push notification tweaks for a rule.
|
||||
pub enum Tweak {
|
||||
/// A string representing the sound to be played when this notification
|
||||
/// arrives.
|
||||
///
|
||||
/// A value of "default" means to play a default sound. A device may choose
|
||||
/// to alert the user by some other means if appropriate, eg. vibration.
|
||||
Sound { value: String },
|
||||
|
||||
/// A boolean representing whether or not this message should be highlighted
|
||||
/// in the UI.
|
||||
Highlight { value: bool },
|
||||
|
||||
/// A custom tweak
|
||||
Custom {
|
||||
/// The name of the custom tweak (`set_tweak` field)
|
||||
name: String,
|
||||
|
||||
/// The value of the custom tweak as an encoded JSON string
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<SdkTweak> for Tweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkTweak::Sound(sound) => Self::Sound { value: sound },
|
||||
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
|
||||
SdkTweak::Custom { name, value } => {
|
||||
let json_string = serde_json::to_string(&value)
|
||||
.map_err(|e| format!("Failed to serialize custom tweak value: {}", e))?;
|
||||
|
||||
Self::Custom { name, value: json_string }
|
||||
}
|
||||
_ => return Err("Unsupported tweak type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Tweak> for SdkTweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Tweak::Sound { value } => Self::Sound(value),
|
||||
Tweak::Highlight { value } => Self::Highlight(value),
|
||||
Tweak::Custom { name, value } => {
|
||||
let json_value: serde_json::Value = serde_json::from_str(&value)
|
||||
.map_err(|e| format!("Failed to deserialize custom tweak value: {}", e))?;
|
||||
let value = serde_json::from_value(json_value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {}", e))?;
|
||||
|
||||
Self::Custom { name, value }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
/// Enum representing the push notification actions for a rule.
|
||||
pub enum Action {
|
||||
/// Causes matching events to generate a notification.
|
||||
Notify,
|
||||
/// Sets an entry in the 'tweaks' dictionary sent to the push gateway.
|
||||
SetTweak { value: Tweak },
|
||||
}
|
||||
|
||||
impl TryFrom<SdkAction> for Action {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkAction) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkAction::Notify => Self::Notify,
|
||||
SdkAction::SetTweak(tweak) => Self::SetTweak {
|
||||
value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
|
||||
},
|
||||
_ => return Err("Unsupported action type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Action> for SdkAction {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Action) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Action::Notify => Self::Notify,
|
||||
Action::SetTweak { value } => Self::SetTweak(
|
||||
value.try_into().map_err(|e| format!("Failed to convert tweak: {}", e))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum representing the push notification modes for a room.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RoomNotificationMode {
|
||||
@@ -267,7 +604,7 @@ impl NotificationSettings {
|
||||
pub async fn is_room_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -280,7 +617,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsRoomMention,
|
||||
enabled,
|
||||
)
|
||||
@@ -292,7 +629,7 @@ impl NotificationSettings {
|
||||
pub async fn is_user_mention_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -304,14 +641,14 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
// Check stable identifier
|
||||
if let Ok(enabled) = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, ".m.rule.encrypted_event")
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, ".m.rule.encrypted_event")
|
||||
.await
|
||||
{
|
||||
enabled
|
||||
} else {
|
||||
// Check unstable identifier
|
||||
notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Override, ".org.matrix.msc4028.encrypted_event")
|
||||
.is_push_rule_enabled(SdkRuleKind::Override, ".org.matrix.msc4028.encrypted_event")
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -332,7 +669,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::IsUserMention,
|
||||
enabled,
|
||||
)
|
||||
@@ -344,7 +681,7 @@ impl NotificationSettings {
|
||||
pub async fn is_call_enabled(&self) -> Result<bool, NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call)
|
||||
.is_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call)
|
||||
.await?;
|
||||
Ok(enabled)
|
||||
}
|
||||
@@ -353,7 +690,7 @@ impl NotificationSettings {
|
||||
pub async fn set_call_enabled(&self, enabled: bool) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
|
||||
.set_push_rule_enabled(SdkRuleKind::Underride, PredefinedUnderrideRuleId::Call, enabled)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -363,7 +700,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let enabled = notification_settings
|
||||
.is_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
)
|
||||
.await?;
|
||||
@@ -378,7 +715,7 @@ impl NotificationSettings {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
notification_settings
|
||||
.set_push_rule_enabled(
|
||||
RuleKind::Override,
|
||||
SdkRuleKind::Override,
|
||||
PredefinedOverrideRuleId::InviteForMe.as_str(),
|
||||
enabled,
|
||||
)
|
||||
@@ -386,6 +723,30 @@ impl NotificationSettings {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets a custom push rule with the given actions and conditions.
|
||||
pub async fn set_custom_push_rule(
|
||||
&self,
|
||||
rule_id: String,
|
||||
rule_kind: RuleKind,
|
||||
actions: Vec<Action>,
|
||||
conditions: Vec<PushCondition>,
|
||||
) -> Result<(), NotificationSettingsError> {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
let actions: Result<Vec<_>, _> =
|
||||
actions.into_iter().map(|action| action.try_into()).collect();
|
||||
let actions = actions.map_err(|e| NotificationSettingsError::Generic { msg: e })?;
|
||||
|
||||
notification_settings
|
||||
.create_custom_conditional_push_rule(
|
||||
rule_id,
|
||||
rule_kind.into(),
|
||||
actions,
|
||||
conditions.into_iter().map(|condition| condition.into()).collect(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmute a room.
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -233,20 +233,34 @@ pub struct TracingFileConfiguration {
|
||||
|
||||
#[derive(PartialEq, PartialOrd)]
|
||||
enum LogTarget {
|
||||
// External crates.
|
||||
Hyper,
|
||||
|
||||
// FFI modules.
|
||||
MatrixSdkFfi,
|
||||
|
||||
// SDK base modules.
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkBaseStoreAmbiguityMap,
|
||||
|
||||
// SDK common modules.
|
||||
MatrixSdkCommonStoreLocks,
|
||||
|
||||
// SDK modules.
|
||||
MatrixSdk,
|
||||
MatrixSdkClient,
|
||||
MatrixSdkCrypto,
|
||||
MatrixSdkCryptoAccount,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkSlidingSync,
|
||||
MatrixSdkBaseSlidingSync,
|
||||
MatrixSdkUiTimeline,
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkBaseEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkSendQueue,
|
||||
MatrixSdkSlidingSync,
|
||||
|
||||
// SDK UI modules.
|
||||
MatrixSdkUiTimeline,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
@@ -254,6 +268,10 @@ impl LogTarget {
|
||||
match self {
|
||||
LogTarget::Hyper => "hyper",
|
||||
LogTarget::MatrixSdkFfi => "matrix_sdk_ffi",
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
|
||||
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
|
||||
LogTarget::MatrixSdk => "matrix_sdk",
|
||||
LogTarget::MatrixSdkClient => "matrix_sdk::client",
|
||||
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
|
||||
@@ -261,11 +279,10 @@ impl LogTarget {
|
||||
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
|
||||
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
|
||||
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
|
||||
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
|
||||
LogTarget::MatrixSdkBaseEventCache => "matrix_sdk_base::event_cache",
|
||||
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,25 +299,64 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::MatrixSdkSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseSlidingSync, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkSendQueue, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkCommonStoreLocks, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
|
||||
];
|
||||
|
||||
const IMMUTABLE_TARGET_LOG_LEVELS: &[LogTarget] = &[
|
||||
LogTarget::Hyper, // Too verbose
|
||||
LogTarget::MatrixSdk, // Too generic
|
||||
LogTarget::MatrixSdkFfi, // Too verbose
|
||||
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
|
||||
];
|
||||
|
||||
/// A log pack can be used to set the trace log level for a group of multiple
|
||||
/// log targets at once, for debugging purposes.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TraceLogPacks {
|
||||
/// Enables all the logs relevant to the event cache.
|
||||
EventCache,
|
||||
/// Enables all the logs relevant to the send queue.
|
||||
SendQueue,
|
||||
/// Enables all the logs relevant to the timeline.
|
||||
Timeline,
|
||||
}
|
||||
|
||||
impl TraceLogPacks {
|
||||
// Note: all the log targets returned here must be part of
|
||||
// `DEFAULT_TARGET_LOG_LEVELS`.
|
||||
fn targets(&self) -> &[LogTarget] {
|
||||
match self {
|
||||
TraceLogPacks::EventCache => &[
|
||||
LogTarget::MatrixSdkEventCache,
|
||||
LogTarget::MatrixSdkBaseEventCache,
|
||||
LogTarget::MatrixSdkEventCacheStore,
|
||||
],
|
||||
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
|
||||
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TracingConfiguration {
|
||||
/// The desired log level
|
||||
/// The desired log level.
|
||||
log_level: LogLevel,
|
||||
|
||||
/// Additional targets that the FFI client would like to use e.g.
|
||||
/// the target names for created [`crate::tracing::Span`]
|
||||
extra_targets: Option<Vec<String>>,
|
||||
/// All the log packs, that will be set to `TRACE` when they're enabled.
|
||||
trace_log_packs: Vec<TraceLogPacks>,
|
||||
|
||||
/// Additional targets that the FFI client would like to use.
|
||||
///
|
||||
/// This can include, for instance, the target names for created
|
||||
/// [`crate::tracing::Span`]. These targets will use the global log level by
|
||||
/// default.
|
||||
extra_targets: Vec<String>,
|
||||
|
||||
/// Whether to log to stdout, or in the logcat on Android.
|
||||
write_to_stdout_or_system: bool,
|
||||
@@ -316,47 +372,111 @@ fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
|
||||
let mut filters = vec!["panic=error".to_owned()];
|
||||
|
||||
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, level)| {
|
||||
// Use the default if the log level shouldn't be changed for this target or
|
||||
// if it's already logging more than requested
|
||||
let level = if IMMUTABLE_TARGET_LOG_LEVELS.contains(target) || level > &config.log_level {
|
||||
level.as_str()
|
||||
let global_level = config.log_level;
|
||||
|
||||
DEFAULT_TARGET_LOG_LEVELS.iter().for_each(|(target, default_level)| {
|
||||
let level = if IMMUTABLE_LOG_TARGETS.contains(target) {
|
||||
// If the target is immutable, keep the log level.
|
||||
*default_level
|
||||
} else if config.trace_log_packs.iter().any(|pack| pack.targets().contains(target)) {
|
||||
// If a log pack includes that target, set the associated log level to TRACE.
|
||||
LogLevel::Trace
|
||||
} else if *default_level > global_level {
|
||||
// If the default level is more verbose than the global level, keep the default.
|
||||
*default_level
|
||||
} else {
|
||||
config.log_level.as_str()
|
||||
// Otherwise, use the global level.
|
||||
global_level
|
||||
};
|
||||
|
||||
filters.push(format!("{}={}", target.as_str(), level));
|
||||
filters.push(format!("{}={}", target.as_str(), level.as_str()));
|
||||
});
|
||||
|
||||
// Finally append the extra targets requested by the client
|
||||
if let Some(extra_targets) = &config.extra_targets {
|
||||
for target in extra_targets {
|
||||
filters.push(format!("{}={}", target, config.log_level.as_str()));
|
||||
}
|
||||
// Finally append the extra targets requested by the client.
|
||||
for target in &config.extra_targets {
|
||||
filters.push(format!("{}={}", target, config.log_level.as_str()));
|
||||
}
|
||||
|
||||
filters.join(",")
|
||||
}
|
||||
|
||||
/// Sets up logs and the tokio runtime for the current application.
|
||||
///
|
||||
/// If `use_lightweight_tokio_runtime` is set to true, this will set up a
|
||||
/// lightweight tokio runtime, for processes that have memory limitations (like
|
||||
/// the NSE process on iOS). Otherwise, this can remain false, in which case a
|
||||
/// multithreaded tokio runtime will be set up.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn setup_tracing(config: TracingConfiguration) {
|
||||
pub fn init_platform(config: TracingConfiguration, use_lightweight_tokio_runtime: bool) {
|
||||
log_panics();
|
||||
|
||||
let env_filter = build_tracing_filter(&config);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(build_tracing_filter(&config)))
|
||||
.with(EnvFilter::new(&env_filter))
|
||||
.with(text_layers(config))
|
||||
.init();
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
|
||||
if use_lightweight_tokio_runtime {
|
||||
setup_lightweight_tokio_runtime();
|
||||
} else {
|
||||
setup_multithreaded_tokio_runtime();
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_multithreaded_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a multithreaded tokio runtime");
|
||||
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
builder.enable_all();
|
||||
builder
|
||||
}));
|
||||
}
|
||||
|
||||
fn setup_lightweight_tokio_runtime() {
|
||||
async_compat::set_runtime_builder(Box::new(|| {
|
||||
eprintln!("spawning a lightweight tokio runtime");
|
||||
|
||||
// Get the number of available cores through the system, if possible.
|
||||
let num_available_cores =
|
||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
|
||||
// The number of worker threads will be either that or 4, whichever is smaller.
|
||||
let num_worker_threads = num_available_cores.min(4);
|
||||
|
||||
// Chosen by a fair dice roll.
|
||||
let num_blocking_threads = 2;
|
||||
|
||||
// 1 MiB of memory per worker thread. Should be enough for everyone™.
|
||||
let max_memory_bytes = 1024 * 1024;
|
||||
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
|
||||
builder
|
||||
.enable_all()
|
||||
.worker_threads(num_worker_threads)
|
||||
.thread_stack_size(max_memory_bytes)
|
||||
.max_blocking_threads(num_blocking_threads);
|
||||
|
||||
builder
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_tracing_filter;
|
||||
use crate::platform::TraceLogPacks;
|
||||
|
||||
#[test]
|
||||
fn test_default_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Error,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned()]),
|
||||
trace_log_packs: Vec::new(),
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
};
|
||||
@@ -377,9 +497,12 @@ mod tests {
|
||||
matrix_sdk::sliding_sync=info,\
|
||||
matrix_sdk_base::sliding_sync=info,\
|
||||
matrix_sdk_ui::timeline=info,\
|
||||
matrix_sdk::send_queue=info,\
|
||||
matrix_sdk::event_cache=info,\
|
||||
matrix_sdk_base::event_cache=info,\
|
||||
matrix_sdk_sqlite::event_cache_store=info,\
|
||||
matrix_sdk_common::store_locks=warn,\
|
||||
matrix_sdk_base::store::ambiguity_map=warn,\
|
||||
super_duper_app=error"
|
||||
);
|
||||
}
|
||||
@@ -388,7 +511,8 @@ mod tests {
|
||||
fn test_trace_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Trace,
|
||||
extra_targets: Some(vec!["super_duper_app".to_owned(), "some_other_span".to_owned()]),
|
||||
trace_log_packs: Vec::new(),
|
||||
extra_targets: vec!["super_duper_app".to_owned(), "some_other_span".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
};
|
||||
@@ -409,11 +533,54 @@ mod tests {
|
||||
matrix_sdk::sliding_sync=trace,\
|
||||
matrix_sdk_base::sliding_sync=trace,\
|
||||
matrix_sdk_ui::timeline=trace,\
|
||||
matrix_sdk::send_queue=trace,\
|
||||
matrix_sdk::event_cache=trace,\
|
||||
matrix_sdk_base::event_cache=trace,\
|
||||
matrix_sdk_sqlite::event_cache_store=trace,\
|
||||
matrix_sdk_common::store_locks=warn,\
|
||||
matrix_sdk_base::store::ambiguity_map=warn,\
|
||||
super_duper_app=trace,\
|
||||
some_other_span=trace"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trace_log_packs() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Info,
|
||||
trace_log_packs: vec![TraceLogPacks::EventCache, TraceLogPacks::SendQueue],
|
||||
extra_targets: vec!["super_duper_app".to_owned()],
|
||||
write_to_stdout_or_system: true,
|
||||
write_to_files: None,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
r#"panic=error,
|
||||
hyper=warn,
|
||||
matrix_sdk_ffi=info,
|
||||
matrix_sdk=info,
|
||||
matrix_sdk::client=trace,
|
||||
matrix_sdk_crypto=debug,
|
||||
matrix_sdk_crypto::olm::account=trace,
|
||||
matrix_sdk::oidc=trace,
|
||||
matrix_sdk::http_client=debug,
|
||||
matrix_sdk::sliding_sync=info,
|
||||
matrix_sdk_base::sliding_sync=info,
|
||||
matrix_sdk_ui::timeline=info,
|
||||
matrix_sdk::send_queue=trace,
|
||||
matrix_sdk::event_cache=trace,
|
||||
matrix_sdk_base::event_cache=trace,
|
||||
matrix_sdk_sqlite::event_cache_store=trace,
|
||||
matrix_sdk_common::store_locks=warn,
|
||||
matrix_sdk_base::store::ambiguity_map=warn,
|
||||
super_duper_app=info"#
|
||||
.split('\n')
|
||||
.map(|s| s.trim())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
crypto::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
TryFromReportedContentScoreError,
|
||||
},
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
|
||||
RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::{
|
||||
call::notify,
|
||||
@@ -30,7 +31,6 @@ use ruma::{
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
@@ -39,10 +39,10 @@ use crate::{
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
timeline::{
|
||||
configuration::{AllowedMessageTypes, TimelineConfiguration},
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
ReceiptType, SendHandle, Timeline,
|
||||
},
|
||||
utils::u64_to_uint,
|
||||
@@ -110,8 +110,8 @@ impl Room {
|
||||
self.inner.avatar_url().map(|m| m.to_string())
|
||||
}
|
||||
|
||||
pub fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(self.inner.is_direct()).unwrap_or(false)
|
||||
pub async fn is_direct(&self) -> bool {
|
||||
self.inner.is_direct().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_public(&self) -> bool {
|
||||
@@ -161,21 +161,6 @@ impl Room {
|
||||
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
|
||||
}
|
||||
|
||||
/// For rooms one is invited to, retrieves the room member information for
|
||||
/// the user who invited the logged-in user to a room.
|
||||
pub async fn inviter(&self) -> Option<RoomMember> {
|
||||
if self.inner.state() == RoomState::Invited {
|
||||
self.inner
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.inviter)
|
||||
.and_then(|m| m.try_into().ok())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Forces the currently active room key, which is used to encrypt messages,
|
||||
/// to be rotated.
|
||||
///
|
||||
@@ -206,30 +191,50 @@ impl Room {
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
|
||||
builder = builder.with_focus(configuration.focus.try_into()?);
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
if let AllowedMessageTypes::Only { types } = configuration.allowed_message_types {
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
types.contains(&content.msgtype.into())
|
||||
if configuration.track_read_receipts {
|
||||
builder = builder.track_read_marker_and_receipts();
|
||||
}
|
||||
|
||||
match configuration.filter {
|
||||
TimelineFilter::All => {
|
||||
// #nofilter.
|
||||
}
|
||||
|
||||
TimelineFilter::OnlyMessage { types } => {
|
||||
builder = builder.event_filter(move |event, room_version_id| {
|
||||
default_event_filter(event, room_version_id)
|
||||
&& match event {
|
||||
AnySyncTimelineEvent::MessageLike(msg) => {
|
||||
match msg.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
types.contains(&content.msgtype.into())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TimelineFilter::EventTypeFilter { filter: event_type_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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(internal_id_prefix) = configuration.internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
builder = builder.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
let timeline = builder.build().await?;
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
@@ -237,8 +242,12 @@ impl Room {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
|
||||
pub fn encryption_state(&self) -> EncryptionState {
|
||||
self.inner.encryption_state()
|
||||
}
|
||||
|
||||
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
|
||||
Ok(self.inner.latest_encryption_state().await?)
|
||||
}
|
||||
|
||||
pub async fn members(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
|
||||
@@ -252,13 +261,13 @@ impl Room {
|
||||
}
|
||||
|
||||
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
Ok(member.try_into().context("Unknown state membership")?)
|
||||
}
|
||||
|
||||
pub async fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
let avatar_url_string = member.avatar_url().map(|m| m.to_string());
|
||||
Ok(avatar_url_string)
|
||||
@@ -268,12 +277,28 @@ impl Room {
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("User not found")?;
|
||||
let avatar_url_string = member.display_name().map(|m| m.to_owned());
|
||||
Ok(avatar_url_string)
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
///
|
||||
/// Returns:
|
||||
/// - If the user was present in the room, a
|
||||
/// [`matrix_sdk::room::RoomMemberWithSenderInfo`] containing both the
|
||||
/// user info and the member info of the sender of the `m.room.member`
|
||||
/// event.
|
||||
/// - If the current user is not present, an error.
|
||||
pub async fn member_with_sender_info(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<RoomMemberWithSenderInfo, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id)?;
|
||||
self.inner.member_with_sender_info(&user_id).await?.try_into()
|
||||
}
|
||||
|
||||
pub async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
RoomInfo::new(&self.inner).await
|
||||
}
|
||||
@@ -283,7 +308,7 @@ impl Room {
|
||||
listener: Box<dyn RoomInfoListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_info();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while subscriber.next().await.is_some() {
|
||||
match self.room_info().await {
|
||||
Ok(room_info) => listener.call(room_info),
|
||||
@@ -375,17 +400,34 @@ impl Room {
|
||||
score: Option<i32>,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let int_score = score.map(|value| value.into());
|
||||
self.inner
|
||||
.client()
|
||||
.send(report_content::v3::Request::new(
|
||||
self.inner.room_id().into(),
|
||||
event_id,
|
||||
int_score,
|
||||
.report_content(
|
||||
EventId::parse(event_id)?,
|
||||
score.map(TryFrom::try_from).transpose().map_err(
|
||||
|error: TryFromReportedContentScoreError| ClientError::Generic {
|
||||
msg: error.to_string(),
|
||||
},
|
||||
)?,
|
||||
reason,
|
||||
))
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reports a room as inappropriate to the server.
|
||||
/// The caller is not required to be joined to the room to report it.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reason` - The reason the room is being reported.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the room is not found or on rate limit
|
||||
pub async fn report_room(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.report_room(reason).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -569,7 +611,7 @@ impl Room {
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn TypingNotificationsListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let (_event_handler_drop_guard, mut subscriber) =
|
||||
self.inner.subscribe_to_typing_notifications();
|
||||
while let Ok(typing_user_ids) = subscriber.recv().await {
|
||||
@@ -580,29 +622,28 @@ impl Room {
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn subscribe_to_identity_status_changes(
|
||||
pub async fn subscribe_to_identity_status_changes(
|
||||
&self,
|
||||
listener: Box<dyn IdentityStatusChangeListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let room = self.inner.clone();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let status_changes = room.subscribe_to_identity_status_changes().await;
|
||||
if let Ok(status_changes) = status_changes {
|
||||
// TODO: what to do with failures?
|
||||
let mut status_changes = pin!(status_changes);
|
||||
while let Some(identity_status_changes) = status_changes.next().await {
|
||||
listener.call(
|
||||
identity_status_changes
|
||||
.into_iter()
|
||||
.map(|change| {
|
||||
let user_id = change.user_id.to_string();
|
||||
IdentityStatusChange { user_id, changed_to: change.changed_to }
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
let status_changes = room.subscribe_to_identity_status_changes().await?;
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut status_changes = pin!(status_changes);
|
||||
while let Some(identity_status_changes) = status_changes.next().await {
|
||||
listener.call(
|
||||
identity_status_changes
|
||||
.into_iter()
|
||||
.map(|change| {
|
||||
let user_id = change.user_id.to_string();
|
||||
IdentityStatusChange { user_id, changed_to: change.changed_to }
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
})))
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Set (or unset) a flag on the room to indicate that the user has
|
||||
@@ -853,7 +894,7 @@ impl Room {
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?;
|
||||
|
||||
let handle = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let handle = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(stream);
|
||||
while let Some(requests) = stream.next().await {
|
||||
listener.call(requests.into_iter().map(Into::into).collect());
|
||||
@@ -1003,9 +1044,9 @@ impl Room {
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let subscription = room.observe_live_location_shares();
|
||||
let mut stream = subscription.subscribe();
|
||||
let stream = subscription.subscribe();
|
||||
let mut pinned_stream = pin!(stream);
|
||||
|
||||
while let Some(event) = pinned_stream.next().await {
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use ruma::ServerName;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -137,11 +137,11 @@ impl RoomDirectorySearch {
|
||||
) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.read().await.results();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
listener.on_update(vec![RoomDirectorySearchEntryUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
listener.on_update(vec![RoomDirectorySearchEntryUpdate::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(|diff| diff.into()).collect());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use matrix_sdk::{EncryptionState, RoomState};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomInfo {
|
||||
id: String,
|
||||
encryption_state: EncryptionState,
|
||||
creator: Option<String>,
|
||||
/// The room's name from the room state event if received from sync, or one
|
||||
/// that's been computed otherwise.
|
||||
@@ -84,6 +85,7 @@ impl RoomInfo {
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
encryption_state: room.encryption_state(),
|
||||
creator: room.creator().as_ref().map(ToString::to_string),
|
||||
display_name: room.cached_display_name().map(|name| name.to_string()),
|
||||
raw_name: room.name(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::ruma::{
|
||||
@@ -26,10 +27,9 @@ use crate::{
|
||||
room::{Membership, Room},
|
||||
room_info::RoomInfo,
|
||||
room_preview::RoomPreview,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
timeline_event_filter::TimelineEventTypeFilter,
|
||||
timeline::{configuration::TimelineEventTypeFilter, EventTimelineItem, Timeline},
|
||||
utils::AsyncRuntimeDropped,
|
||||
TaskHandle, RUNTIME,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -92,7 +92,7 @@ impl RoomListService {
|
||||
fn state(&self, listener: Box<dyn RoomListServiceStateListener>) -> Arc<TaskHandle> {
|
||||
let state_stream = self.inner.state();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(state_stream);
|
||||
|
||||
while let Some(state) = state_stream.next().await {
|
||||
@@ -128,7 +128,7 @@ impl RoomListService {
|
||||
Duration::from_millis(delay_before_hiding_in_ms.into()),
|
||||
);
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(sync_indicator_stream);
|
||||
|
||||
while let Some(sync_indicator) = sync_indicator_stream.next().await {
|
||||
@@ -167,7 +167,7 @@ impl RoomList {
|
||||
|
||||
Ok(RoomListLoadingStateResult {
|
||||
state: loading_state.get().into(),
|
||||
state_stream: Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
state_stream: Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(loading_state);
|
||||
|
||||
while let Some(loading_state) = loading_state.next().await {
|
||||
@@ -237,7 +237,7 @@ impl RoomList {
|
||||
let dynamic_entries_controller =
|
||||
Arc::new(RoomListDynamicEntriesController::new(dynamic_entries_controller));
|
||||
|
||||
let entries_stream = Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
let entries_stream = Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(entries_stream);
|
||||
|
||||
while let Some(diffs) = entries_stream.next().await {
|
||||
@@ -557,8 +557,8 @@ impl RoomListItem {
|
||||
self.inner.avatar_url().map(|uri| uri.to_string())
|
||||
}
|
||||
|
||||
fn is_direct(&self) -> bool {
|
||||
RUNTIME.block_on(self.inner.inner_room().is_direct()).unwrap_or(false)
|
||||
async fn is_direct(&self) -> bool {
|
||||
self.inner.inner_room().is_direct().await.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn canonical_alias(&self) -> Option<String> {
|
||||
@@ -574,29 +574,8 @@ impl RoomListItem {
|
||||
self.inner.inner_room().state().into()
|
||||
}
|
||||
|
||||
/// Builds a `Room` FFI from an invited room without initializing its
|
||||
/// internal timeline.
|
||||
///
|
||||
/// An error will be returned if the room is a state different than invited.
|
||||
///
|
||||
/// ⚠️ Holding on to this room instance after it has been joined is not
|
||||
/// safe. Use `full_room` instead.
|
||||
#[deprecated(note = "Please use `preview_room` instead.")]
|
||||
fn invited_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if !matches!(self.membership(), Membership::Invited) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited],
|
||||
actual: self.membership(),
|
||||
});
|
||||
}
|
||||
Ok(Arc::new(Room::new(self.inner.inner_room().clone())))
|
||||
}
|
||||
|
||||
/// Builds a `RoomPreview` from a room list item. This is intended for
|
||||
/// invited or knocked rooms.
|
||||
///
|
||||
/// An error will be returned if the room is in a state other than invited
|
||||
/// or knocked.
|
||||
/// invited, knocked or banned rooms.
|
||||
async fn preview_room(&self, via: Vec<String>) -> Result<Arc<RoomPreview>, ClientError> {
|
||||
// Validate parameters first.
|
||||
let server_names: Vec<OwnedServerName> = via
|
||||
@@ -604,16 +583,6 @@ impl RoomListItem {
|
||||
.map(|server| ServerName::parse(server).map_err(ClientError::from))
|
||||
.collect::<Result<_, ClientError>>()?;
|
||||
|
||||
// Validate internal room state.
|
||||
let membership = self.membership();
|
||||
if !matches!(membership, Membership::Invited | Membership::Knocked) {
|
||||
return Err(RoomListError::IncorrectRoomMembership {
|
||||
expected: vec![Membership::Invited, Membership::Knocked],
|
||||
actual: membership,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// Do the thing.
|
||||
let client = self.inner.client();
|
||||
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
|
||||
@@ -711,7 +680,11 @@ impl RoomListItem {
|
||||
/// **Note**: this info may not be reliable if you don't set up
|
||||
/// `m.room.encryption` as required state.
|
||||
async fn is_encrypted(&self) -> bool {
|
||||
self.inner.is_encrypted().await.unwrap_or(false)
|
||||
self.inner
|
||||
.latest_encryption_state()
|
||||
.await
|
||||
.map(|state| state.is_encrypted())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn latest_event(&self) -> Option<EventTimelineItem> {
|
||||
|
||||
@@ -108,3 +108,25 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the current user's room member info and the optional room member
|
||||
/// info of the sender of the `m.room.member` event that this info represents.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomMemberWithSenderInfo {
|
||||
/// The room member.
|
||||
room_member: RoomMember,
|
||||
/// The info of the sender of the event `room_member` is based on, if
|
||||
/// available.
|
||||
sender_info: Option<RoomMember>,
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk::room::RoomMemberWithSenderInfo> for RoomMemberWithSenderInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk::room::RoomMemberWithSenderInfo) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
room_member: value.room_member.try_into()?,
|
||||
sender_info: value.sender_info.map(|member| member.try_into()).transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::RoomMember,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
@@ -51,11 +51,23 @@ impl RoomPreview {
|
||||
/// Leave the room if the room preview state is either joined, invited or
|
||||
/// knocked.
|
||||
///
|
||||
/// If rejecting an invite then also forget it as an extra layer of
|
||||
/// protection against spam attacks.
|
||||
///
|
||||
/// Will return an error otherwise.
|
||||
pub async fn leave(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.leave().await.map_err(Into::into)
|
||||
|
||||
let should_forget = matches!(room.state(), matrix_sdk::RoomState::Invited);
|
||||
|
||||
room.leave().await.map_err(ClientError::from)?;
|
||||
|
||||
if should_forget {
|
||||
_ = self.forget().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the user who created the invite, if any.
|
||||
@@ -74,32 +86,12 @@ impl RoomPreview {
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMembershipDetails> {
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMemberWithSenderInfo> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
|
||||
let (own_member, sender_member) = match room.own_membership_details().await {
|
||||
Ok(memberships) => memberships,
|
||||
Err(error) => {
|
||||
warn!("Couldn't get membership info: {error}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(RoomMembershipDetails {
|
||||
own_room_member: own_member.try_into().ok()?,
|
||||
sender_room_member: sender_member.and_then(|member| member.try_into().ok()),
|
||||
})
|
||||
room.member_with_sender_info(self.client.user_id()?).await.ok()?.try_into().ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the current user's room member info and the optional room member
|
||||
/// info of the sender of the `m.room.member` event that this info represents.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomMembershipDetails {
|
||||
pub own_room_member: RoomMember,
|
||||
pub sender_room_member: Option<RoomMember>,
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
pub(crate) fn new(client: AsyncRuntimeDropped<Client>, inner: SdkRoomPreview) -> Self {
|
||||
Self { client, inner }
|
||||
|
||||
@@ -34,7 +34,7 @@ use ruma::{
|
||||
MessageType as RumaMessageType,
|
||||
NoticeMessageEventContent as RumaNoticeMessageEventContent,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
TextMessageEventContent as RumaTextMessageEventContent,
|
||||
TextMessageEventContent as RumaTextMessageEventContent, UnstableAmplitude,
|
||||
UnstableAudioDetailsContentBlock as RumaUnstableAudioDetailsContentBlock,
|
||||
UnstableVoiceContentBlock as RumaUnstableVoiceContentBlock,
|
||||
VideoInfo as RumaVideoInfo,
|
||||
@@ -362,6 +362,8 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
event_content.audio = content.audio.map(Into::into);
|
||||
event_content.voice = content.voice.map(Into::into);
|
||||
Self::Audio(event_content)
|
||||
}
|
||||
MessageType::Video { content } => {
|
||||
@@ -658,6 +660,15 @@ impl From<RumaUnstableAudioDetailsContentBlock> for UnstableAudioDetailsContent
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UnstableAudioDetailsContent> for RumaUnstableAudioDetailsContentBlock {
|
||||
fn from(details: UnstableAudioDetailsContent) -> Self {
|
||||
Self::new(
|
||||
details.duration,
|
||||
details.waveform.iter().map(|x| UnstableAmplitude::new(x.to_owned())).collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct UnstableVoiceContent {}
|
||||
|
||||
@@ -667,6 +678,12 @@ impl From<RumaUnstableVoiceContentBlock> for UnstableVoiceContent {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UnstableVoiceContent> for RumaUnstableVoiceContentBlock {
|
||||
fn from(_details: UnstableVoiceContent) -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct VideoInfo {
|
||||
pub duration: Option<Duration>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption::{
|
||||
@@ -7,13 +8,13 @@ use matrix_sdk::{
|
||||
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
|
||||
Encryption,
|
||||
},
|
||||
ruma::events::{key::verification::VerificationMethod, AnyToDeviceEvent},
|
||||
ruma::events::key::verification::VerificationMethod,
|
||||
Account,
|
||||
};
|
||||
use ruma::UserId;
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, utils::Timestamp};
|
||||
use crate::{client::UserProfile, error::ClientError, utils::Timestamp};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SessionVerificationEmoji {
|
||||
@@ -39,12 +40,12 @@ pub enum SessionVerificationData {
|
||||
}
|
||||
|
||||
/// Details about the incoming verification request
|
||||
#[derive(Debug, uniffi::Record)]
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct SessionVerificationRequestDetails {
|
||||
sender_id: String,
|
||||
sender_profile: UserProfile,
|
||||
flow_id: String,
|
||||
device_id: String,
|
||||
display_name: Option<String>,
|
||||
device_display_name: Option<String>,
|
||||
/// First time this device was seen in milliseconds since epoch.
|
||||
first_seen_timestamp: Timestamp,
|
||||
}
|
||||
@@ -66,6 +67,7 @@ pub type Delegate = Arc<RwLock<Option<Box<dyn SessionVerificationControllerDeleg
|
||||
pub struct SessionVerificationController {
|
||||
encryption: Encryption,
|
||||
user_identity: UserIdentity,
|
||||
account: Account,
|
||||
delegate: Delegate,
|
||||
verification_request: Arc<RwLock<Option<VerificationRequest>>>,
|
||||
sas_verification: Arc<RwLock<Option<SasVerification>>>,
|
||||
@@ -94,15 +96,7 @@ impl SessionVerificationController {
|
||||
.await
|
||||
.ok_or(ClientError::new("Unknown session verification request"))?;
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
|
||||
/// Accept the previously acknowledged verification request
|
||||
@@ -118,7 +112,7 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
/// Request verification for the current device
|
||||
pub async fn request_verification(&self) -> Result<(), ClientError> {
|
||||
pub async fn request_device_verification(&self) -> Result<(), ClientError> {
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
let verification_request = self
|
||||
.user_identity
|
||||
@@ -126,15 +120,31 @@ impl SessionVerificationController {
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
|
||||
RUNTIME.spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
/// Request verification for the given user
|
||||
pub async fn request_user_verification(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
|
||||
Ok(())
|
||||
let user_identity = self
|
||||
.encryption
|
||||
.get_user_identity(&user_id)
|
||||
.await?
|
||||
.ok_or(ClientError::new("Unknown user identity"))?;
|
||||
|
||||
if user_identity.is_verified() {
|
||||
return Err(ClientError::new("User is already verified"));
|
||||
}
|
||||
|
||||
let methods = vec![VerificationMethod::SasV1];
|
||||
|
||||
let verification_request = user_identity
|
||||
.request_verification_with_methods(methods)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
self.set_ongoing_verification_request(verification_request)
|
||||
}
|
||||
|
||||
/// Transition the current verification request into a SAS verification
|
||||
@@ -155,7 +165,8 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
let delegate = self.delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
|
||||
get_runtime_handle()
|
||||
.spawn(Self::listen_to_sas_verification_changes(verification, delegate));
|
||||
}
|
||||
_ => {
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
@@ -202,50 +213,91 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
impl SessionVerificationController {
|
||||
pub(crate) fn new(encryption: Encryption, user_identity: UserIdentity) -> Self {
|
||||
pub(crate) fn new(
|
||||
encryption: Encryption,
|
||||
user_identity: UserIdentity,
|
||||
account: Account,
|
||||
) -> Self {
|
||||
SessionVerificationController {
|
||||
encryption,
|
||||
user_identity,
|
||||
account,
|
||||
delegate: Arc::new(RwLock::new(None)),
|
||||
verification_request: Arc::new(RwLock::new(None)),
|
||||
sas_verification: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn process_to_device_message(&self, event: AnyToDeviceEvent) {
|
||||
if let AnyToDeviceEvent::KeyVerificationRequest(event) = event {
|
||||
info!("Received verification request: {:}", event.sender);
|
||||
|
||||
let Some(request) = self
|
||||
.encryption
|
||||
.get_verification_request(&event.sender, &event.content.transaction_id)
|
||||
.await
|
||||
else {
|
||||
error!("Failed retrieving verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
if !request.is_self_verification() {
|
||||
info!("Received non-self verification request. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
let VerificationRequestState::Requested { other_device_data, .. } = request.state()
|
||||
else {
|
||||
error!("Received key verification event but the request is in the wrong state.");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_id: request.other_user_id().into(),
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
display_name: other_device_data.display_name().map(str::to_string),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
|
||||
});
|
||||
/// Ask the controller to process an incoming request based on the sender
|
||||
/// and flow identifier. It will fetch the request, verify that it's in the
|
||||
/// correct state and then and notify the delegate.
|
||||
pub(crate) async fn process_incoming_verification_request(
|
||||
&self,
|
||||
sender: &UserId,
|
||||
flow_id: impl AsRef<str>,
|
||||
) {
|
||||
if sender != self.user_identity.user_id() {
|
||||
if let Some(status) = self.encryption.cross_signing_status().await {
|
||||
if !status.is_complete() {
|
||||
warn!("Cannot verify other users until our own device's cross-signing status is complete: {:?}", status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
|
||||
error!("Failed retrieving verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
let VerificationRequestState::Requested { other_device_data, .. } = request.state() else {
|
||||
error!("Received verification request event but the request is in the wrong state.");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(sender_profile) = self.account.fetch_user_profile_of(sender).await else {
|
||||
error!("Failed fetching user profile for verification request");
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(delegate) = &*self.delegate.read().unwrap() {
|
||||
delegate.did_receive_verification_request(SessionVerificationRequestDetails {
|
||||
sender_profile: UserProfile {
|
||||
user_id: request.other_user_id().to_string(),
|
||||
display_name: sender_profile.displayname,
|
||||
avatar_url: sender_profile.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
},
|
||||
flow_id: request.flow_id().into(),
|
||||
device_id: other_device_data.device_id().into(),
|
||||
device_display_name: other_device_data.display_name().map(str::to_string),
|
||||
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ongoing_verification_request(
|
||||
&self,
|
||||
verification_request: VerificationRequest,
|
||||
) -> Result<(), ClientError> {
|
||||
if let Some(ongoing_verification_request) =
|
||||
self.verification_request.read().unwrap().clone()
|
||||
{
|
||||
if !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
return Err(ClientError::new("There is another verification flow ongoing."));
|
||||
}
|
||||
}
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
get_runtime_handle().spawn(Self::listen_to_verification_request_changes(
|
||||
verification_request,
|
||||
self.sas_verification.clone(),
|
||||
self.delegate.clone(),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen_to_verification_request_changes(
|
||||
@@ -271,7 +323,7 @@ impl SessionVerificationController {
|
||||
}
|
||||
|
||||
let delegate = delegate.clone();
|
||||
RUNTIME.spawn(Self::listen_to_sas_verification_changes(
|
||||
get_runtime_handle().spawn(Self::listen_to_sas_verification_changes(
|
||||
verification,
|
||||
delegate,
|
||||
));
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, Client};
|
||||
use matrix_sdk_ui::{
|
||||
@@ -29,7 +30,6 @@ use tracing::error;
|
||||
|
||||
use crate::{
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService, TaskHandle,
|
||||
RUNTIME,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -84,7 +84,7 @@ impl SyncService {
|
||||
pub fn state(&self, listener: Box<dyn SyncServiceStateObserver>) -> Arc<TaskHandle> {
|
||||
let state_stream = self.inner.state();
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(state_stream);
|
||||
|
||||
while let Some(state) = state_stream.next().await {
|
||||
|
||||
@@ -1,7 +1,65 @@
|
||||
use ruma::EventId;
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
|
||||
use ruma::{
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use super::FocusEventError;
|
||||
use crate::{error::ClientError, event::RoomMessageEventMessageType};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
#[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) })
|
||||
}
|
||||
|
||||
#[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) })
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineEventTypeFilter {
|
||||
/// Filters an [`event`] to decide whether it should be part of the timeline
|
||||
/// based on [`AnySyncTimelineEvent::event_type()`].
|
||||
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
|
||||
self.inner.filter(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum FilterTimelineEventType {
|
||||
MessageLike { event_type: MessageLikeEventType },
|
||||
State { event_type: StateEventType },
|
||||
}
|
||||
|
||||
impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
fn from(value: FilterTimelineEventType) -> TimelineEventType {
|
||||
match value {
|
||||
FilterTimelineEventType::MessageLike { event_type } => {
|
||||
ruma::events::MessageLikeEventType::from(event_type).into()
|
||||
}
|
||||
FilterTimelineEventType::State { event_type } => {
|
||||
ruma::events::StateEventType::from(event_type).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineFocus {
|
||||
@@ -52,28 +110,27 @@ impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AllowedMessageTypes {
|
||||
pub enum TimelineFilter {
|
||||
/// Show all the events in the timeline, independent of their type.
|
||||
All,
|
||||
Only { types: Vec<RoomMessageEventMessageType> },
|
||||
/// Show only `m.room.messages` of the given room message types.
|
||||
OnlyMessage {
|
||||
/// A list of [`RoomMessageEventMessageType`] that will be allowed to
|
||||
/// appear in the timeline.
|
||||
types: Vec<RoomMessageEventMessageType>,
|
||||
},
|
||||
/// Show only events which match this filter.
|
||||
EventTypeFilter { filter: Arc<TimelineEventTypeFilter> },
|
||||
}
|
||||
|
||||
/// Various options used to configure the timeline's behavior.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `internal_id_prefix` -
|
||||
///
|
||||
/// * `allowed_message_types` -
|
||||
///
|
||||
/// * `date_divider_mode` -
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TimelineConfiguration {
|
||||
/// What should the timeline focus on?
|
||||
pub focus: TimelineFocus,
|
||||
|
||||
/// A list of [`RoomMessageEventMessageType`] that will be allowed to appear
|
||||
/// in the timeline
|
||||
pub allowed_message_types: AllowedMessageTypes,
|
||||
/// How should we filter out events from the timeline?
|
||||
pub filter: TimelineFilter,
|
||||
|
||||
/// An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
@@ -82,4 +139,11 @@ pub struct TimelineConfiguration {
|
||||
|
||||
/// How often to insert date dividers
|
||||
pub date_divider_mode: DateDividerMode,
|
||||
|
||||
/// Should the read receipts and read markers be tracked for the timeline
|
||||
/// items in this instance?
|
||||
///
|
||||
/// As this has a non negligible performance impact, make sure to enable it
|
||||
/// only when you need it.
|
||||
pub track_read_receipts: bool,
|
||||
}
|
||||
|
||||
@@ -12,73 +12,31 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes};
|
||||
use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails};
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent, FullStateEventContent};
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
|
||||
use ruma::events::FullStateEventContent;
|
||||
|
||||
use super::ProfileDetails;
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
utils::Timestamp,
|
||||
};
|
||||
use crate::{timeline::msg_like::MsgLikeContent, utils::Timestamp};
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
|
||||
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
|
||||
|
||||
match value {
|
||||
Content::Message(message) => {
|
||||
let msgtype = message.msgtype().msgtype().to_owned();
|
||||
|
||||
match TryInto::<MessageContent>::try_into(message) {
|
||||
Ok(message) => TimelineItemContent::Message { content: message },
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: msgtype,
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
|
||||
let media_source = RumaMediaSource::from(content.source.clone());
|
||||
|
||||
if let Err(error) = media_source.verify() {
|
||||
return TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
match TryInto::<ImageInfo>::try_into(&content.info) {
|
||||
Ok(info) => TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info,
|
||||
source: Arc::new(MediaSource { media_source }),
|
||||
},
|
||||
Err(error) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type: sticker.content().event_type().to_string(),
|
||||
error: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()),
|
||||
Content::MsgLike(msg_like) => match msg_like.try_into() {
|
||||
Ok(content) => TimelineItemContent::MsgLike { content },
|
||||
Err((error, event_type)) => TimelineItemContent::FailedToParseMessageLike {
|
||||
event_type,
|
||||
error: error.to_string(),
|
||||
},
|
||||
},
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::CallNotify => TimelineItemContent::CallNotify,
|
||||
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }
|
||||
}
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
FullStateEventContent::Original { content, .. } => content.reason.clone(),
|
||||
@@ -137,65 +95,13 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct MessageContent {
|
||||
pub msg_type: MessageType,
|
||||
pub body: String,
|
||||
pub in_reply_to: Option<Arc<InReplyToDetails>>,
|
||||
pub thread_root: Option<String>,
|
||||
pub is_edited: bool,
|
||||
pub mentions: Option<Mentions>,
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: matrix_sdk_ui::timeline::Message) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
msg_type: value.msgtype().clone().try_into()?,
|
||||
body: value.body().to_owned(),
|
||||
in_reply_to: value.in_reply_to().map(|r| Arc::new(r.clone().into())),
|
||||
is_edited: value.is_edited(),
|
||||
thread_root: value.thread_root().map(|id| id.to_string()),
|
||||
mentions: value.mentions().cloned().map(|m| m.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruma::events::Mentions> for Mentions {
|
||||
fn from(value: ruma::events::Mentions) -> Self {
|
||||
Self {
|
||||
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
|
||||
room: value.room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum TimelineItemContent {
|
||||
Message {
|
||||
content: MessageContent,
|
||||
},
|
||||
RedactedMessage,
|
||||
Sticker {
|
||||
body: String,
|
||||
info: ImageInfo,
|
||||
source: Arc<MediaSource>,
|
||||
},
|
||||
Poll {
|
||||
question: String,
|
||||
kind: PollKind,
|
||||
max_selections: u64,
|
||||
answers: Vec<PollAnswer>,
|
||||
votes: HashMap<String, Vec<String>>,
|
||||
end_time: Option<Timestamp>,
|
||||
has_been_edited: bool,
|
||||
MsgLike {
|
||||
content: MsgLikeContent,
|
||||
},
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
UnableToDecrypt {
|
||||
msg: EncryptedMessage,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
@@ -223,94 +129,6 @@ pub enum TimelineItemContent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: RepliedToEventDetails,
|
||||
}
|
||||
|
||||
impl InReplyToDetails {
|
||||
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl InReplyToDetails {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.event_id.clone()
|
||||
}
|
||||
|
||||
pub fn event(&self) -> RepliedToEventDetails {
|
||||
self.event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
let event_id = inner.event_id.to_string();
|
||||
let event = match &inner.event {
|
||||
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
|
||||
TimelineDetails::Pending => RepliedToEventDetails::Pending,
|
||||
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
|
||||
content: event.content().clone().into(),
|
||||
sender: event.sender().to_string(),
|
||||
sender_profile: event.sender_profile().into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => {
|
||||
RepliedToEventDetails::Error { message: err.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RepliedToEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EncryptedMessage {
|
||||
OlmV1Curve25519AesSha2 {
|
||||
/// The Curve25519 key of the sender.
|
||||
sender_key: String,
|
||||
},
|
||||
// Other fields not included because UniFFI doesn't have the concept of
|
||||
// deprecated fields right now.
|
||||
MegolmV1AesSha2 {
|
||||
/// The ID of the session used to encrypt the message.
|
||||
session_id: String,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent
|
||||
/// when we were not a member of this room?
|
||||
cause: UtdCause,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EncryptedMessage {
|
||||
fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
|
||||
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
|
||||
|
||||
match msg {
|
||||
Message::OlmV1Curve25519AesSha2 { sender_key } => {
|
||||
let sender_key = sender_key.clone();
|
||||
Self::OlmV1Curve25519AesSha2 { sender_key }
|
||||
}
|
||||
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
|
||||
let session_id = session_id.clone();
|
||||
Self::MegolmV1AesSha2 { session_id, cause: *cause }
|
||||
}
|
||||
Message::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct Reaction {
|
||||
pub key: String,
|
||||
@@ -463,27 +281,3 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct PollAnswer {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl From<PollResult> for TimelineItemContent {
|
||||
fn from(value: PollResult) -> Self {
|
||||
TimelineItemContent::Poll {
|
||||
question: value.question,
|
||||
kind: PollKind::from(value.kind),
|
||||
max_selections: value.max_selections,
|
||||
answers: value
|
||||
.answers
|
||||
.into_iter()
|
||||
.map(|i| PollAnswer { id: i.id, text: i.text })
|
||||
.collect(),
|
||||
votes: value.votes,
|
||||
end_time: value.end_time.map(|t| t.into()),
|
||||
has_been_edited: value.has_been_edited,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::{collections::HashMap, fmt::Write as _, fs, panic, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use as_variant::as_variant;
|
||||
use content::{InReplyToDetails, RepliedToEventDetails};
|
||||
use async_compat::get_runtime_handle;
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use matrix_sdk::{
|
||||
@@ -25,14 +25,18 @@ use matrix_sdk::{
|
||||
BaseVideoInfo, Thumbnail,
|
||||
},
|
||||
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
|
||||
room::edit::EditedContent as SdkEditedContent,
|
||||
Error,
|
||||
event_cache::RoomPaginationStatus,
|
||||
room::{
|
||||
edit::EditedContent as SdkEditedContent,
|
||||
reply::{EnforceThread, Reply},
|
||||
},
|
||||
};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
self, EventItemOrigin, LiveBackPaginationStatus, Profile, RepliedToEvent, TimelineDetails,
|
||||
self, EventItemOrigin, Profile, RepliedToEvent, TimelineDetails,
|
||||
TimelineUniqueId as SdkTimelineUniqueId,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reply::{InReplyToDetails, RepliedToEventDetails};
|
||||
use ruma::{
|
||||
events::{
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
@@ -46,7 +50,7 @@ use ruma::{
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
room::message::{
|
||||
ForwardThread, LocationMessageEventContent, MessageType,
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
@@ -60,7 +64,8 @@ use tokio::{
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
use self::content::TimelineItemContent;
|
||||
pub use self::msg_like::MessageContent;
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
@@ -72,13 +77,13 @@ use crate::{
|
||||
},
|
||||
task_handle::TaskHandle,
|
||||
utils::Timestamp,
|
||||
RUNTIME,
|
||||
};
|
||||
|
||||
pub mod configuration;
|
||||
mod content;
|
||||
mod msg_like;
|
||||
mod reply;
|
||||
|
||||
pub use content::MessageContent;
|
||||
use matrix_sdk::utils::formatted_body_from;
|
||||
|
||||
use crate::error::QueueWedgeError;
|
||||
@@ -121,9 +126,10 @@ impl Timeline {
|
||||
.info(attachment_info)
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into));
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
|
||||
let handle = SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut request =
|
||||
self.inner.send_attachment(params.filename, mime_type, attachment_config);
|
||||
|
||||
@@ -133,7 +139,7 @@ impl Timeline {
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
@@ -201,32 +207,65 @@ pub struct UploadParameters {
|
||||
caption: Option<String>,
|
||||
/// Optional HTML-formatted caption, for clients that support it.
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
// Optional intentional mentions to be sent with the media.
|
||||
/// Optional intentional mentions to be sent with the media.
|
||||
mentions: Option<Mentions>,
|
||||
/// Optional parameters for sending the media as (threaded) reply.
|
||||
reply_params: Option<ReplyParameters>,
|
||||
/// Should the media be sent with the send queue, or synchronously?
|
||||
///
|
||||
/// Watching progress only works with the synchronous method, at the moment.
|
||||
use_send_queue: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ReplyParameters {
|
||||
/// The ID of the event to reply to.
|
||||
event_id: String,
|
||||
/// Whether to enforce a thread relation.
|
||||
enforce_thread: bool,
|
||||
/// If enforcing a threaded relation, whether the message is a reply on a
|
||||
/// thread.
|
||||
reply_within_thread: bool,
|
||||
}
|
||||
|
||||
impl TryInto<Reply> for ReplyParameters {
|
||||
type Error = RoomError;
|
||||
|
||||
fn try_into(self) -> Result<Reply, Self::Error> {
|
||||
let event_id =
|
||||
EventId::parse(&self.event_id).map_err(|_| RoomError::InvalidRepliedToEventId)?;
|
||||
let enforce_thread = if self.enforce_thread {
|
||||
EnforceThread::Threaded(if self.reply_within_thread {
|
||||
ReplyWithinThread::Yes
|
||||
} else {
|
||||
ReplyWithinThread::No
|
||||
})
|
||||
} else {
|
||||
EnforceThread::MaybeThreaded
|
||||
};
|
||||
|
||||
Ok(Reply { event_id, enforce_thread })
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Timeline {
|
||||
pub async fn add_listener(&self, listener: Box<dyn TimelineListener>) -> Arc<TaskHandle> {
|
||||
let (timeline_items, timeline_stream) = self.inner.subscribe().await;
|
||||
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// It's important that the initial items are passed *before* we forward the
|
||||
// stream updates, with a guaranteed ordering. Otherwise, it could
|
||||
// be that the listener be called before the initial items have been
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
|
||||
values: timeline_items,
|
||||
}))]);
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
|
||||
// It's important that the initial items are passed *before* we forward the
|
||||
// stream updates, with a guaranteed ordering. Otherwise, it could
|
||||
// be that the listener be called before the initial items have been
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![Arc::new(TimelineDiff::new(VectorDiff::Reset {
|
||||
values: timeline_items,
|
||||
}))]);
|
||||
|
||||
// Then forward new items.
|
||||
while let Some(diffs) = timeline_stream.next().await {
|
||||
listener
|
||||
@@ -236,7 +275,7 @@ impl Timeline {
|
||||
}
|
||||
|
||||
pub fn retry_decryption(self: Arc<Self>, session_ids: Vec<String>) {
|
||||
RUNTIME.spawn(async move {
|
||||
get_runtime_handle().spawn(async move {
|
||||
self.inner.retry_decryption(&session_ids).await;
|
||||
});
|
||||
}
|
||||
@@ -255,10 +294,14 @@ impl Timeline {
|
||||
.await
|
||||
.context("can't subscribe to the back-pagination status on a focused timeline")?;
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
// Send the current state even if it hasn't changed right away.
|
||||
listener.on_update(initial);
|
||||
// Send the current state even if it hasn't changed right away.
|
||||
//
|
||||
// Note: don't do it in the spawned function, so that the caller is immediately
|
||||
// aware of the current state, and this doesn't depend on the async runtime
|
||||
// having an available worker
|
||||
listener.on_update(initial);
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(status) = subscriber.next().await {
|
||||
listener.on_update(status);
|
||||
}
|
||||
@@ -441,7 +484,7 @@ impl Timeline {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn end_poll(
|
||||
pub async fn end_poll(
|
||||
self: Arc<Self>,
|
||||
poll_start_event_id: String,
|
||||
text: String,
|
||||
@@ -451,29 +494,25 @@ impl Timeline {
|
||||
let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id);
|
||||
let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content);
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
if let Err(err) = self.inner.send(event_content).await {
|
||||
error!("unable to end poll: {err}");
|
||||
}
|
||||
});
|
||||
if let Err(err) = self.inner.send(event_content).await {
|
||||
error!("unable to end poll: {err}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a reply.
|
||||
///
|
||||
/// If the replied to event has a thread relation, it is forwarded on the
|
||||
/// reply so that clients that support threads can render the reply
|
||||
/// inside the thread.
|
||||
pub async fn send_reply(
|
||||
&self,
|
||||
msg: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
event_id: String,
|
||||
reply_params: ReplyParameters,
|
||||
) -> Result<(), ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
let replied_to_info = self
|
||||
.inner
|
||||
.replied_to_info_from_event_id(&event_id)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
|
||||
self.inner
|
||||
.send_reply((*msg).clone(), replied_to_info, ForwardThread::Yes)
|
||||
.send_reply((*msg).clone(), reply_params.try_into()?)
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!(err))?;
|
||||
Ok(())
|
||||
@@ -621,19 +660,12 @@ impl Timeline {
|
||||
) -> Result<Arc<InReplyToDetails>, ClientError> {
|
||||
let event_id = EventId::parse(&event_id_str)?;
|
||||
|
||||
let replied_to: Result<RepliedToEvent, Error> =
|
||||
if let Some(event) = self.inner.item_by_event_id(&event_id).await {
|
||||
Ok(RepliedToEvent::from_timeline_item(&event))
|
||||
} else {
|
||||
match self.inner.room().event(&event_id, None).await {
|
||||
Ok(timeline_event) => Ok(RepliedToEvent::try_from_timeline_event_for_room(
|
||||
timeline_event,
|
||||
self.inner.room(),
|
||||
)
|
||||
.await?),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
};
|
||||
let replied_to = match self.inner.room().load_or_fetch_event(&event_id, None).await {
|
||||
Ok(event) => RepliedToEvent::try_from_timeline_event_for_room(event, self.inner.room())
|
||||
.await
|
||||
.map_err(ClientError::from),
|
||||
Err(e) => Err(ClientError::from(e)),
|
||||
};
|
||||
|
||||
match replied_to {
|
||||
Ok(replied_to) => Ok(Arc::new(InReplyToDetails::new(
|
||||
@@ -757,7 +789,7 @@ pub trait TimelineListener: Sync + Send {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait PaginationStatusListener: Sync + Send {
|
||||
fn on_update(&self, status: LiveBackPaginationStatus);
|
||||
fn on_update(&self, status: RoomPaginationStatus);
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
@@ -934,6 +966,7 @@ impl TimelineItem {
|
||||
match self.0.as_virtual()? {
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),
|
||||
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
|
||||
VItem::TimelineStart => Some(VirtualTimelineItem::TimelineStart),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,7 +1061,6 @@ pub struct EventTimelineItem {
|
||||
is_editable: bool,
|
||||
content: TimelineItemContent,
|
||||
timestamp: Timestamp,
|
||||
reactions: Vec<Reaction>,
|
||||
local_send_state: Option<EventSendState>,
|
||||
local_created_at: Option<u64>,
|
||||
read_receipts: HashMap<String, Receipt>,
|
||||
@@ -1039,20 +1071,6 @@ pub struct EventTimelineItem {
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
fn from(item: matrix_sdk_ui::timeline::EventTimelineItem) -> Self {
|
||||
let reactions = item
|
||||
.reactions()
|
||||
.iter()
|
||||
.map(|(k, v)| Reaction {
|
||||
key: k.to_owned(),
|
||||
senders: v
|
||||
.into_iter()
|
||||
.map(|(sender_id, info)| ReactionSenderData {
|
||||
sender_id: sender_id.to_string(),
|
||||
timestamp: info.timestamp.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
let item = Arc::new(item);
|
||||
let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone()));
|
||||
let read_receipts =
|
||||
@@ -1066,7 +1084,6 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
timestamp: item.timestamp().into(),
|
||||
reactions,
|
||||
local_send_state: item.send_state().map(|s| s.into()),
|
||||
local_created_at: item.local_created_at().map(|t| t.0.into()),
|
||||
read_receipts,
|
||||
@@ -1213,6 +1230,9 @@ pub enum VirtualTimelineItem {
|
||||
|
||||
/// The user's own read marker.
|
||||
ReadMarker,
|
||||
|
||||
/// The timeline start, that is, the *oldest* event in time for that room.
|
||||
TimelineStart,
|
||||
}
|
||||
|
||||
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2023 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, sync::Arc};
|
||||
|
||||
use matrix_sdk::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, EventContent};
|
||||
|
||||
use super::{content::Reaction, reply::InReplyToDetails};
|
||||
use crate::{
|
||||
error::ClientError,
|
||||
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
|
||||
timeline::content::ReactionSenderData,
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MsgLikeKind {
|
||||
/// An `m.room.message` event or extensible event, including edits.
|
||||
Message { content: MessageContent },
|
||||
/// An `m.sticker` event.
|
||||
Sticker { body: String, info: ImageInfo, source: Arc<MediaSource> },
|
||||
/// An `m.poll.start` event.
|
||||
Poll {
|
||||
question: String,
|
||||
kind: PollKind,
|
||||
max_selections: u64,
|
||||
answers: Vec<PollAnswer>,
|
||||
votes: HashMap<String, Vec<String>>,
|
||||
end_time: Option<Timestamp>,
|
||||
has_been_edited: bool,
|
||||
},
|
||||
|
||||
/// A redacted message.
|
||||
Redacted,
|
||||
|
||||
/// An `m.room.encrypted` event that could not be decrypted.
|
||||
UnableToDecrypt { msg: EncryptedMessage },
|
||||
}
|
||||
|
||||
/// A special kind of [`super::TimelineItemContent`] that groups together
|
||||
/// different room message types with their respective reactions and thread
|
||||
/// information.
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct MsgLikeContent {
|
||||
pub kind: MsgLikeKind,
|
||||
pub reactions: Vec<Reaction>,
|
||||
/// Event ID of the thread root, if this is a threaded message.
|
||||
pub thread_root: Option<String>,
|
||||
/// The event this message is replying to, if any.
|
||||
pub in_reply_to: Option<Arc<InReplyToDetails>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct MessageContent {
|
||||
pub msg_type: MessageType,
|
||||
pub body: String,
|
||||
pub is_edited: bool,
|
||||
pub mentions: Option<Mentions>,
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
|
||||
type Error = (ClientError, String);
|
||||
|
||||
fn try_from(value: matrix_sdk_ui::timeline::MsgLikeContent) -> Result<Self, Self::Error> {
|
||||
use matrix_sdk_ui::timeline::MsgLikeKind as Kind;
|
||||
|
||||
let reactions = value
|
||||
.reactions
|
||||
.iter()
|
||||
.map(|(k, v)| Reaction {
|
||||
key: k.to_owned(),
|
||||
senders: v
|
||||
.into_iter()
|
||||
.map(|(sender_id, info)| ReactionSenderData {
|
||||
sender_id: sender_id.to_string(),
|
||||
timestamp: info.timestamp.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let in_reply_to = value.in_reply_to.map(|r| Arc::new(r.into()));
|
||||
|
||||
let thread_root = value.thread_root.map(|id| id.to_string());
|
||||
|
||||
Ok(match value.kind {
|
||||
Kind::Message(message) => {
|
||||
let msg_type = TryInto::<MessageType>::try_into(message.msgtype().clone())
|
||||
.map_err(|e| (e, message.msgtype().msgtype().to_owned()))?;
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::Message {
|
||||
content: MessageContent {
|
||||
msg_type,
|
||||
body: message.body().to_owned(),
|
||||
is_edited: message.is_edited(),
|
||||
mentions: message.mentions().cloned().map(|m| m.into()),
|
||||
},
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
}
|
||||
}
|
||||
Kind::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
|
||||
let media_source = RumaMediaSource::from(content.source.clone());
|
||||
media_source
|
||||
.verify()
|
||||
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
|
||||
|
||||
let image_info = TryInto::<ImageInfo>::try_into(&content.info)
|
||||
.map_err(|e| (e, sticker.content().event_type().to_string()))?;
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: image_info,
|
||||
source: Arc::new(MediaSource { media_source }),
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
}
|
||||
}
|
||||
Kind::Poll(poll_state) => {
|
||||
let results = poll_state.results();
|
||||
|
||||
Self {
|
||||
kind: MsgLikeKind::Poll {
|
||||
question: results.question,
|
||||
kind: PollKind::from(results.kind),
|
||||
max_selections: results.max_selections,
|
||||
answers: results
|
||||
.answers
|
||||
.into_iter()
|
||||
.map(|i| PollAnswer { id: i.id, text: i.text })
|
||||
.collect(),
|
||||
votes: results.votes,
|
||||
end_time: results.end_time.map(|t| t.into()),
|
||||
has_been_edited: results.has_been_edited,
|
||||
},
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
}
|
||||
}
|
||||
Kind::Redacted => {
|
||||
Self { kind: MsgLikeKind::Redacted, reactions, in_reply_to, thread_root }
|
||||
}
|
||||
Kind::UnableToDecrypt(msg) => Self {
|
||||
kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) },
|
||||
reactions,
|
||||
in_reply_to,
|
||||
thread_root,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruma::events::Mentions> for Mentions {
|
||||
fn from(value: ruma::events::Mentions) -> Self {
|
||||
Self {
|
||||
user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(),
|
||||
room: value.room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EncryptedMessage {
|
||||
OlmV1Curve25519AesSha2 {
|
||||
/// The Curve25519 key of the sender.
|
||||
sender_key: String,
|
||||
},
|
||||
// Other fields not included because UniFFI doesn't have the concept of
|
||||
// deprecated fields right now.
|
||||
MegolmV1AesSha2 {
|
||||
/// The ID of the session used to encrypt the message.
|
||||
session_id: String,
|
||||
|
||||
/// What we know about what caused this UTD. E.g. was this event sent
|
||||
/// when we were not a member of this room?
|
||||
cause: UtdCause,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EncryptedMessage {
|
||||
pub(crate) fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
|
||||
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
|
||||
|
||||
match msg {
|
||||
Message::OlmV1Curve25519AesSha2 { sender_key } => {
|
||||
let sender_key = sender_key.clone();
|
||||
Self::OlmV1Curve25519AesSha2 { sender_key }
|
||||
}
|
||||
Message::MegolmV1AesSha2 { session_id, cause, .. } => {
|
||||
let session_id = session_id.clone();
|
||||
Self::MegolmV1AesSha2 { session_id, cause: *cause }
|
||||
}
|
||||
Message::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct PollAnswer {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2023 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_ui::timeline::TimelineDetails;
|
||||
|
||||
use super::{content::TimelineItemContent, ProfileDetails};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct InReplyToDetails {
|
||||
event_id: String,
|
||||
event: RepliedToEventDetails,
|
||||
}
|
||||
|
||||
impl InReplyToDetails {
|
||||
pub(crate) fn new(event_id: String, event: RepliedToEventDetails) -> Self {
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl InReplyToDetails {
|
||||
pub fn event_id(&self) -> String {
|
||||
self.event_id.clone()
|
||||
}
|
||||
|
||||
pub fn event(&self) -> RepliedToEventDetails {
|
||||
self.event.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
|
||||
fn from(inner: matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
|
||||
let event_id = inner.event_id.to_string();
|
||||
let event = match &inner.event {
|
||||
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
|
||||
TimelineDetails::Pending => RepliedToEventDetails::Pending,
|
||||
TimelineDetails::Ready(event) => RepliedToEventDetails::Ready {
|
||||
content: event.content().clone().into(),
|
||||
sender: event.sender().to_string(),
|
||||
sender_profile: event.sender_profile().into(),
|
||||
},
|
||||
TimelineDetails::Error(err) => {
|
||||
RepliedToEventDetails::Error { message: err.to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
Self { event_id, event }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum RepliedToEventDetails {
|
||||
Unavailable,
|
||||
Pending,
|
||||
Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails },
|
||||
Error { message: String },
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
|
||||
use ruma::events::{AnySyncTimelineEvent, TimelineEventType};
|
||||
|
||||
use crate::event::{MessageLikeEventType, StateEventType};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct TimelineEventTypeFilter {
|
||||
inner: InnerTimelineEventTypeFilter,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl TimelineEventTypeFilter {
|
||||
#[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) })
|
||||
}
|
||||
|
||||
#[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) })
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineEventTypeFilter {
|
||||
/// Filters an [`event`] to decide whether it should be part of the timeline
|
||||
/// based on [`AnySyncTimelineEvent::event_type()`].
|
||||
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
|
||||
self.inner.filter(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum FilterTimelineEventType {
|
||||
MessageLike { event_type: MessageLikeEventType },
|
||||
State { event_type: StateEventType },
|
||||
}
|
||||
|
||||
impl From<FilterTimelineEventType> for TimelineEventType {
|
||||
fn from(value: FilterTimelineEventType) -> TimelineEventType {
|
||||
match value {
|
||||
FilterTimelineEventType::MessageLike { event_type } => {
|
||||
ruma::events::MessageLikeEventType::from(event_type).into()
|
||||
}
|
||||
FilterTimelineEventType::State { event_type } => {
|
||||
ruma::events::StateEventType::from(event_type).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ impl Span {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)]
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, uniffi::Enum)]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
@@ -176,7 +176,7 @@ pub enum LogLevel {
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn to_tracing_level(&self) -> tracing::Level {
|
||||
fn to_tracing_level(self) -> tracing::Level {
|
||||
match self {
|
||||
LogLevel::Error => tracing::Level::ERROR,
|
||||
LogLevel::Warn => tracing::Level::WARN,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use async_compat::get_runtime_handle;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
use tracing::warn;
|
||||
|
||||
@@ -54,7 +54,7 @@ impl<T> AsyncRuntimeDropped<T> {
|
||||
|
||||
impl<T> Drop for AsyncRuntimeDropped<T> {
|
||||
fn drop(&mut self) {
|
||||
let _guard = RUNTIME.enter();
|
||||
let _guard = get_runtime_handle().enter();
|
||||
// SAFETY: self.inner is never used again, which is the only requirement
|
||||
// for ManuallyDrop::drop to be used safely.
|
||||
unsafe {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_compat::get_runtime_handle;
|
||||
use language_tags::LanguageTag;
|
||||
use matrix_sdk::{
|
||||
async_trait,
|
||||
@@ -8,7 +9,7 @@ use matrix_sdk::{
|
||||
use ruma::events::MessageLikeEventType;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{room::Room, RUNTIME};
|
||||
use crate::room::Room;
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct WidgetDriverAndHandle {
|
||||
@@ -140,12 +141,31 @@ impl From<EncryptionSystem> for matrix_sdk::widget::EncryptionSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the intent of showing the call.
|
||||
///
|
||||
/// This controls whether to show or skip the lobby.
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum Intent {
|
||||
/// The user wants to start a call.
|
||||
StartCall,
|
||||
/// The user wants to join an existing call.
|
||||
JoinExisting,
|
||||
}
|
||||
impl From<Intent> for matrix_sdk::widget::Intent {
|
||||
fn from(value: Intent) -> Self {
|
||||
match value {
|
||||
Intent::StartCall => Self::StartCall,
|
||||
Intent::JoinExisting => Self::JoinExisting,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties to create a new virtual Element Call widget.
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct VirtualElementCallWidgetOptions {
|
||||
/// The url to the app.
|
||||
/// The url to the Element Call app including any `/room` path if required.
|
||||
///
|
||||
/// E.g. <https://call.element.io>, <https://call.element.dev>
|
||||
/// E.g. <https://call.element.io>, <https://call.element.dev>, <https://call.element.dev/room>
|
||||
pub element_call_url: String,
|
||||
|
||||
/// The widget id.
|
||||
@@ -188,11 +208,6 @@ pub struct VirtualElementCallWidgetOptions {
|
||||
/// Default: `false`
|
||||
pub app_prompt: Option<bool>,
|
||||
|
||||
/// Don't show the lobby and join the call immediately.
|
||||
///
|
||||
/// Default: `false`
|
||||
pub skip_lobby: Option<bool>,
|
||||
|
||||
/// Make it not possible to get to the calls list in the webview.
|
||||
///
|
||||
/// Default: `true`
|
||||
@@ -201,13 +216,38 @@ pub struct VirtualElementCallWidgetOptions {
|
||||
/// The font to use, to adapt to the system font.
|
||||
pub font: Option<String>,
|
||||
|
||||
/// Can be used to pass a PostHog id to element call.
|
||||
pub analytics_id: Option<String>,
|
||||
|
||||
/// The encryption system to use.
|
||||
///
|
||||
/// Use `EncryptionSystem::Unencrypted` to disable encryption.
|
||||
pub encryption: EncryptionSystem,
|
||||
|
||||
/// The intent of showing the call.
|
||||
/// If the user wants to start a call or join an existing one.
|
||||
/// Controls if the lobby is skipped or not.
|
||||
pub intent: Option<Intent>,
|
||||
|
||||
/// Do not show the screenshare button.
|
||||
pub hide_screensharing: bool,
|
||||
|
||||
/// Can be used to pass a PostHog id to element call.
|
||||
pub posthog_user_id: Option<String>,
|
||||
/// The host of the posthog api.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub posthog_api_host: Option<String>,
|
||||
/// The key for the posthog api.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub posthog_api_key: Option<String>,
|
||||
|
||||
/// The url to use for submitting rageshakes.
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub rageshake_submit_url: Option<String>,
|
||||
|
||||
/// Sentry [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/)
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub sentry_dsn: Option<String>,
|
||||
/// Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/)
|
||||
/// Supported since Element Call v0.9.0. Only used by the embedded package.
|
||||
pub sentry_environment: Option<String>,
|
||||
}
|
||||
|
||||
impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElementCallWidgetOptions {
|
||||
@@ -220,11 +260,17 @@ impl From<VirtualElementCallWidgetOptions> for matrix_sdk::widget::VirtualElemen
|
||||
preload: value.preload,
|
||||
font_scale: value.font_scale,
|
||||
app_prompt: value.app_prompt,
|
||||
skip_lobby: value.skip_lobby,
|
||||
confine_to_room: value.confine_to_room,
|
||||
font: value.font,
|
||||
analytics_id: value.analytics_id,
|
||||
posthog_user_id: value.posthog_user_id,
|
||||
encryption: value.encryption.into(),
|
||||
intent: value.intent.map(Into::into),
|
||||
hide_screensharing: value.hide_screensharing,
|
||||
posthog_api_host: value.posthog_api_host,
|
||||
posthog_api_key: value.posthog_api_key,
|
||||
rageshake_submit_url: value.rageshake_submit_url,
|
||||
sentry_dsn: value.sentry_dsn,
|
||||
sentry_environment: value.sentry_environment,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -501,7 +547,7 @@ impl matrix_sdk::widget::CapabilitiesProvider for CapabilitiesProviderWrap {
|
||||
// This could require a prompt to the user. Ideally the callback
|
||||
// interface would just be async, but that's not supported yet so use
|
||||
// one of tokio's blocking task threads instead.
|
||||
RUNTIME
|
||||
get_runtime_handle()
|
||||
.spawn_blocking(move || this.acquire_capabilities(capabilities.into()).into())
|
||||
.await
|
||||
// propagate panics from the blocking task
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="250" viewBox="0 0 512 512">
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
rect.bg { fill: none; }
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
rect.bg { fill: #17191C; }
|
||||
}
|
||||
</style>
|
||||
<g clip-path="url(#clip0_7151_5134)">
|
||||
<rect class="bg" width="512" height="512" />
|
||||
<path d="M437.539 522.597L437.062 517.672L451.585 504.129C454.537 501.365 453.431 495.837 449.663 494.43L431.107 487.495L429.649 482.721L441.207 466.64C443.569 463.374 441.396 458.16 437.438 457.507L417.865 454.328L415.516 449.931L423.744 431.878C425.428 428.197 422.3 423.498 418.254 423.649L398.404 424.34L395.264 420.533L399.824 401.186C400.741 397.253 396.759 393.271 392.826 394.188L373.479 398.748L369.66 395.608L370.351 375.758C370.501 371.737 365.803 368.597 362.134 370.268L344.093 378.496L339.696 376.135L336.505 356.561C335.877 352.591 330.638 350.43 327.372 352.792L311.291 364.35L306.517 362.905L299.582 344.35C298.175 340.581 292.634 339.475 289.883 342.415L276.34 356.938L271.415 356.461L260.962 339.563C258.852 336.146 253.173 336.146 251.075 339.563L240.622 356.461L235.697 356.938L222.129 342.39C219.365 339.45 213.837 340.543 212.43 344.324L205.495 362.88L200.721 364.325L184.64 352.767C181.374 350.405 176.148 352.578 175.507 356.536L172.316 376.109L167.919 378.471L149.878 370.242C146.209 368.571 141.498 371.712 141.661 375.733L142.352 395.583L138.533 398.723L119.186 394.163C115.253 393.246 111.271 397.228 112.188 401.161L116.748 420.508L113.608 424.315L93.7577 423.624C89.7374 423.498 86.5966 428.172 88.2675 431.853L96.4965 449.906L94.1346 454.303L74.561 457.482C70.591 458.11 68.4301 463.349 70.792 466.615L82.3502 482.696L80.8929 487.47L62.3369 494.405C58.568 495.812 57.4624 501.353 60.4148 504.104L74.9379 517.647L74.4605 522.572L57.5629 533.025C54.1457 535.135 54.1457 540.814 57.5629 542.912L74.4605 553.365L74.9379 558.289L60.3771 571.883C57.4373 574.647 58.5303 580.175 62.2993 581.582L80.8552 588.517L82.3125 593.291L70.7543 609.372C68.405 612.638 70.5659 617.864 74.5233 618.505L94.0843 621.684L96.4462 626.081L88.2173 644.122C86.5464 647.79 89.6997 652.501 93.7074 652.351L113.557 651.66L116.698 655.479L112.138 674.826C111.221 678.746 115.203 682.741 119.135 681.811L138.483 677.251L142.302 680.392L141.611 700.242C141.46 704.262 146.159 707.403 149.828 705.732L167.868 697.503L172.266 699.865L175.457 719.426C176.085 723.408 181.324 725.557 184.59 723.22L200.671 711.637L205.445 713.094L212.38 731.65C213.787 735.419 219.328 736.524 222.079 733.572L235.622 719.049L240.547 719.551L251 736.449C253.11 739.841 258.764 739.866 260.887 736.449L271.339 719.551L276.264 719.049L289.807 733.572C292.571 736.524 298.099 735.419 299.506 731.65L306.441 713.094L311.215 711.637L327.296 723.22C330.563 725.569 335.789 723.408 336.43 719.426L339.621 699.865L344.018 697.503L362.059 705.732C365.727 707.403 370.426 704.275 370.275 700.242L369.584 680.392L373.391 677.251L392.738 681.811C396.671 682.729 400.653 678.746 399.736 674.826L395.176 655.479L398.316 651.66L418.166 652.351C422.187 652.514 425.327 647.79 423.657 644.122L415.428 626.081L417.777 621.684L437.351 618.505C441.333 617.877 443.506 612.651 441.119 609.372L429.561 593.291L431.019 588.517L449.575 581.582C453.344 580.162 454.449 574.634 451.497 571.883L436.974 558.34L437.451 553.415L454.349 542.962C457.766 540.852 457.778 535.198 454.349 533.075L437.539 522.597Z" fill="#F74C00"/>
|
||||
<ellipse cx="197.264" cy="427.256" rx="36.8364" ry="41.9173" fill="#17191C"/>
|
||||
<ellipse cx="314.125" cy="427.256" rx="36.8364" ry="41.9173" fill="#17191C"/>
|
||||
<path d="M230.597 485.777C236.497 497.197 246.672 504.739 258.235 504.739C271.41 504.739 282.782 494.948 288.083 480.787L230.597 485.777Z" fill="#17191C"/>
|
||||
<ellipse cx="210.602" cy="442.181" rx="15.8777" ry="20.6411" fill="white"/>
|
||||
<ellipse cx="326.827" cy="442.181" rx="15.8777" ry="20.6411" fill="white"/>
|
||||
<g clip-path="url(#clip1_7151_5134)">
|
||||
<path d="M186.081 188.249V206.978H186.604C191.603 199.815 197.647 194.293 204.66 190.413C211.674 186.459 219.807 184.519 228.91 184.519C237.64 184.519 245.624 186.235 252.862 189.592C260.1 192.95 265.547 198.994 269.352 207.5C273.456 201.456 279.052 196.084 286.066 191.458C293.08 186.832 301.437 184.519 311.062 184.519C318.374 184.519 325.164 185.414 331.432 187.205C337.7 188.995 342.997 191.831 347.474 195.785C351.951 199.74 355.384 204.814 357.92 211.156C360.383 217.499 361.651 225.109 361.651 234.063V326.661H323.672V248.24C323.672 243.614 323.523 239.212 323.15 235.108C322.777 231.004 321.807 227.422 320.24 224.438C318.598 221.379 316.285 218.991 313.151 217.2C310.017 215.409 305.764 214.514 300.467 214.514C295.094 214.514 290.767 215.559 287.484 217.573C284.2 219.662 281.589 222.274 279.724 225.632C277.858 228.915 276.59 232.645 275.993 236.899C275.396 241.077 275.023 245.33 275.023 249.583V326.661H237.044V249.061C237.044 244.957 236.969 240.928 236.745 236.899C236.596 232.869 235.775 229.213 234.432 225.781C233.089 222.423 230.85 219.662 227.717 217.648C224.583 215.633 220.031 214.589 213.913 214.589C212.122 214.589 209.734 214.962 206.824 215.782C203.914 216.603 201.004 218.095 198.244 220.334C195.483 222.572 193.095 225.781 191.155 229.959C189.215 234.138 188.245 239.659 188.245 246.449V326.735H150.266V188.249H186.081Z" fill="white"/>
|
||||
<path d="M72.2223 70.8792V441.121H98.86V450H62V62H98.86V70.8792H72.2223Z" fill="white"/>
|
||||
<path d="M439.785 441.121V70.8792H413.147V62H450.007V450H413.147V441.121H439.785Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7151_5134">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_7151_5134">
|
||||
<rect width="388" height="388" fill="white" transform="translate(62 62)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -6,6 +6,41 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] The `Client::subscribe_to_ignore_user_list_changes()`
|
||||
method will now only trigger whenever the ignored user list has
|
||||
changed from what was previously known, instead of triggering
|
||||
every time an ignore-user-list event has been received from sync.
|
||||
([#4779](https://github.com/matrix-org/matrix-rust-sdk/pull/4779))
|
||||
- [**breaking**] The `MediaRetentionPolicy` can now trigger regular cleanups
|
||||
with its new `cleanup_frequency` setting.
|
||||
([#4603](https://github.com/matrix-org/matrix-rust-sdk/pull/4603))
|
||||
- `Clone` is a supertrait of `EventCacheStoreMedia`.
|
||||
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
|
||||
- There are new `'static` bounds in `MediaService` for the media cache stores
|
||||
- `event_cache::store::MemoryStore` implements `Clone`.
|
||||
- `BaseClient` now has a `handle_verification_events` field which is `true` by
|
||||
default and can be negated so the `NotificationClient` won't handle received
|
||||
verification events too, causing errors in the `VerificationMachine`.
|
||||
- [**breaking**] `Room::is_encryption_state_synced` has been removed
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
|
||||
- [**breaking**] `Room::is_encrypted` is replaced by `Room::encryption_state`
|
||||
which returns a value of the new `EncryptionState` enum
|
||||
([#4777](https://github.com/matrix-org/matrix-rust-sdk/pull/4777))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `BaseClient::store` is renamed `state_store`
|
||||
([#4851](https://github.com/matrix-org/matrix-rust-sdk/pull/4851))
|
||||
- [**breaking**] `BaseClient::with_store_config` is renamed `new`
|
||||
([#4847](https://github.com/matrix-org/matrix-rust-sdk/pull/4847))
|
||||
- [**breaking**] `BaseClient::set_session_metadata` is renamed
|
||||
`activate`, and `BaseClient::logged_in` is renamed `is_activated`
|
||||
([#4850](https://github.com/matrix-org/matrix-rust-sdk/pull/4850))
|
||||
|
||||
## [0.10.0] - 2025-02-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.10.0"
|
||||
version = "0.11.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -65,7 +65,6 @@ ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc3575",
|
||||
"unstable-msc4186",
|
||||
"rand",
|
||||
] }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -277,8 +277,10 @@ impl RawAnySyncOrStrippedState {
|
||||
/// Try to deserialize the inner JSON as the expected type.
|
||||
pub fn deserialize(&self) -> serde_json::Result<AnySyncOrStrippedState> {
|
||||
match self {
|
||||
Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(raw.deserialize()?)),
|
||||
Self::Stripped(raw) => Ok(AnySyncOrStrippedState::Stripped(raw.deserialize()?)),
|
||||
Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(Box::new(raw.deserialize()?))),
|
||||
Self::Stripped(raw) => {
|
||||
Ok(AnySyncOrStrippedState::Stripped(Box::new(raw.deserialize()?)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,9 +302,15 @@ impl RawAnySyncOrStrippedState {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AnySyncOrStrippedState {
|
||||
/// An event from a room in joined or left state.
|
||||
Sync(AnySyncStateEvent),
|
||||
///
|
||||
/// The value is `Box`ed because it is quite large. Let's keep the size of
|
||||
/// `Self` as small as possible.
|
||||
Sync(Box<AnySyncStateEvent>),
|
||||
/// An event from a room in invited state.
|
||||
Stripped(AnyStrippedStateEvent),
|
||||
///
|
||||
/// The value is `Box`ed because it is quite large. Let's keep the size of
|
||||
/// `Self` as small as possible.
|
||||
Stripped(Box<AnyStrippedStateEvent>),
|
||||
}
|
||||
|
||||
impl AnySyncOrStrippedState {
|
||||
|
||||
@@ -21,20 +21,24 @@ use matrix_sdk_common::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{
|
||||
ChunkContent, ChunkIdentifier as CId, LinkedChunk, LinkedChunkBuilder, Position, RawChunk,
|
||||
Update,
|
||||
},
|
||||
linked_chunk::{lazy_loader, ChunkContent, ChunkIdentifier as CId, Position, Update},
|
||||
};
|
||||
use matrix_sdk_test::{event_factory::EventFactory, ALICE, DEFAULT_TEST_ROOM_ID};
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, mxc_uri,
|
||||
push::Action, room_id, uint, RoomId,
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
event_id,
|
||||
events::{
|
||||
relation::RelationType,
|
||||
room::{message::RoomMessageEventContentWithoutRelation, MediaSource},
|
||||
},
|
||||
mxc_uri,
|
||||
push::Action,
|
||||
room_id, uint, EventId, RoomId,
|
||||
};
|
||||
|
||||
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
event_cache::{store::DEFAULT_CHUNK_CAPACITY, Gap},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
|
||||
@@ -43,6 +47,15 @@ use crate::{
|
||||
///
|
||||
/// Keep in sync with [`check_test_event`].
|
||||
pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
|
||||
make_test_event_with_event_id(room_id, content, None)
|
||||
}
|
||||
|
||||
/// Same as [`make_test_event`], with an extra event id.
|
||||
pub fn make_test_event_with_event_id(
|
||||
room_id: &RoomId,
|
||||
content: &str,
|
||||
event_id: Option<&EventId>,
|
||||
) -> TimelineEvent {
|
||||
let encryption_info = EncryptionInfo {
|
||||
sender: (*ALICE).into(),
|
||||
sender_device: None,
|
||||
@@ -51,14 +64,14 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent {
|
||||
sender_claimed_keys: Default::default(),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
session_id: Some("mysessionid9".to_owned()),
|
||||
};
|
||||
|
||||
let event = EventFactory::new()
|
||||
.text_msg(content)
|
||||
.room(room_id)
|
||||
.sender(*ALICE)
|
||||
.into_raw_timeline()
|
||||
.cast();
|
||||
let mut builder = EventFactory::new().text_msg(content).room(room_id).sender(*ALICE);
|
||||
if let Some(event_id) = event_id {
|
||||
builder = builder.event_id(event_id);
|
||||
}
|
||||
let event = builder.into_raw_timeline().cast();
|
||||
|
||||
TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
@@ -100,7 +113,7 @@ pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
/// `EventCacheStore` integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`event_cache_store_integration_tests!`] macro.
|
||||
/// `event_cache_store_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStoreIntegrationTests {
|
||||
@@ -114,6 +127,10 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
/// the store.
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self);
|
||||
|
||||
/// Test loading a linked chunk incrementally (chunk by chunk) from the
|
||||
/// store.
|
||||
async fn test_linked_chunk_incremental_loading(&self);
|
||||
|
||||
/// Test that rebuilding a linked chunk from an empty store doesn't return
|
||||
/// anything.
|
||||
async fn test_rebuild_empty_linked_chunk(&self);
|
||||
@@ -123,10 +140,18 @@ pub trait EventCacheStoreIntegrationTests {
|
||||
|
||||
/// Test that removing a room from storage empties all associated data.
|
||||
async fn test_remove_room(&self);
|
||||
}
|
||||
|
||||
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
|
||||
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
|
||||
/// Test that filtering duplicated events works as expected.
|
||||
async fn test_filter_duplicated_events(&self);
|
||||
|
||||
/// Test that an event can be found or not.
|
||||
async fn test_find_event(&self);
|
||||
|
||||
/// Test that finding event relations works as expected.
|
||||
async fn test_find_event_relations(&self);
|
||||
|
||||
/// Test that saving an event works as expected.
|
||||
async fn test_save_event(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -341,7 +366,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
},
|
||||
// another items chunk
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 0
|
||||
// new items on 2
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![make_test_event(room_id, "sup")],
|
||||
@@ -352,8 +377,10 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
let raws = self.reload_linked_chunk(room_id).await.unwrap();
|
||||
let lc = rebuild_linked_chunk(raws).expect("linked chunk not empty");
|
||||
let lc =
|
||||
lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(room_id).await.unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let mut chunks = lc.chunks();
|
||||
|
||||
@@ -392,10 +419,228 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
|
||||
async fn test_linked_chunk_incremental_loading(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
// Load the last chunk, but none exists yet.
|
||||
{
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(room_id).await.unwrap();
|
||||
|
||||
assert!(last_chunk.is_none());
|
||||
assert_eq!(chunk_identifier_generator.current(), 0);
|
||||
}
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
// new items on 0
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event("a"), event("b")],
|
||||
},
|
||||
// new chunk for a gap
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "morbier".to_owned() },
|
||||
},
|
||||
// new chunk for items
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
// new items on 2
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![event("c"), event("d"), event("e")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Load the last chunk.
|
||||
let mut linked_chunk = {
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
self.load_last_chunk(room_id).await.unwrap();
|
||||
|
||||
assert_eq!(chunk_identifier_generator.current(), 2);
|
||||
|
||||
let linked_chunk = lazy_loader::from_last_chunk::<DEFAULT_CHUNK_CAPACITY, _, _>(
|
||||
last_chunk,
|
||||
chunk_identifier_generator,
|
||||
)
|
||||
.unwrap() // unwrap the `Result`
|
||||
.unwrap(); // unwrap the `Option`
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
// A unique chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert_eq!(chunk.lazy_previous(), Some(CId::new(1)));
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(rchunks.next().is_none());
|
||||
|
||||
linked_chunk
|
||||
};
|
||||
|
||||
// Load the previous chunk: this is a gap.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
// The last chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
// The new chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 1);
|
||||
assert_eq!(chunk.lazy_previous(), Some(CId::new(0)));
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(rchunks.next().is_none());
|
||||
}
|
||||
|
||||
// Load the previous chunk: these are items.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk =
|
||||
self.load_previous_chunk(room_id, first_chunk).await.unwrap().unwrap();
|
||||
|
||||
let _ = lazy_loader::insert_new_first_chunk(&mut linked_chunk, previous_chunk).unwrap();
|
||||
|
||||
let mut rchunks = linked_chunk.rchunks();
|
||||
|
||||
// The last chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
// Its previous chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 1);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
// Already asserted, but let's be sure nothing breaks.
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
// The new chunk.
|
||||
assert_matches!(rchunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 0);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "a");
|
||||
check_test_event(&events[1], "b");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(rchunks.next().is_none());
|
||||
}
|
||||
|
||||
// Load the previous chunk: there is none.
|
||||
{
|
||||
let first_chunk = linked_chunk.chunks().next().unwrap().identifier();
|
||||
let previous_chunk = self.load_previous_chunk(room_id, first_chunk).await.unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
|
||||
// One last check: a round of assert by using the forwards chunk iterator
|
||||
// instead of the backwards chunk iterator.
|
||||
{
|
||||
let mut chunks = linked_chunk.chunks();
|
||||
|
||||
// The first chunk.
|
||||
assert_matches!(chunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 0);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 2);
|
||||
check_test_event(&events[0], "a");
|
||||
check_test_event(&events[1], "b");
|
||||
});
|
||||
});
|
||||
|
||||
// The second chunk.
|
||||
assert_matches!(chunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 1);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
|
||||
assert_eq!(gap.prev_token, "morbier");
|
||||
});
|
||||
});
|
||||
|
||||
// The third and last chunk.
|
||||
assert_matches!(chunks.next(), Some(chunk) => {
|
||||
assert_eq!(chunk.identifier(), 2);
|
||||
assert!(chunk.lazy_previous().is_none());
|
||||
|
||||
assert_matches!(chunk.content(), ChunkContent::Items(events) => {
|
||||
assert_eq!(events.len(), 3);
|
||||
check_test_event(&events[0], "c");
|
||||
check_test_event(&events[1], "d");
|
||||
check_test_event(&events[2], "e");
|
||||
});
|
||||
});
|
||||
|
||||
assert!(chunks.next().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_rebuild_empty_linked_chunk(&self) {
|
||||
// When I rebuild a linked chunk from an empty store, it's empty.
|
||||
let raw_parts = self.reload_linked_chunk(&DEFAULT_TEST_ROOM_ID).await.unwrap();
|
||||
assert!(rebuild_linked_chunk(raw_parts).is_none());
|
||||
let linked_chunk = lazy_loader::from_all_chunks::<3, _, _>(
|
||||
self.load_all_chunks(&DEFAULT_TEST_ROOM_ID).await.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(linked_chunk.is_none());
|
||||
}
|
||||
|
||||
async fn test_clear_all_rooms_chunks(&self) {
|
||||
@@ -444,15 +689,23 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: both linked chunks can be reloaded.
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_some());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
|
||||
.unwrap()
|
||||
.is_some());
|
||||
|
||||
// Clear the chunks.
|
||||
self.clear_all_rooms_chunks().await.unwrap();
|
||||
|
||||
// Both rooms now have no linked chunk.
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r0).await.unwrap()).is_none());
|
||||
assert!(rebuild_linked_chunk(self.reload_linked_chunk(r1).await.unwrap()).is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r0).await.unwrap())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(lazy_loader::from_all_chunks::<3, _, _>(self.load_all_chunks(r1).await.unwrap())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
async fn test_remove_room(&self) {
|
||||
@@ -495,13 +748,272 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
self.remove_room(r0).await.unwrap();
|
||||
|
||||
// Check that r0 doesn't have a linked chunk anymore.
|
||||
let r0_linked_chunk = self.reload_linked_chunk(r0).await.unwrap();
|
||||
let r0_linked_chunk = self.load_all_chunks(r0).await.unwrap();
|
||||
assert!(r0_linked_chunk.is_empty());
|
||||
|
||||
// Check that r1 is unaffected.
|
||||
let r1_linked_chunk = self.reload_linked_chunk(r1).await.unwrap();
|
||||
let r1_linked_chunk = self.load_all_chunks(r1).await.unwrap();
|
||||
assert!(!r1_linked_chunk.is_empty());
|
||||
}
|
||||
|
||||
async fn test_filter_duplicated_events(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let event_comte = event("comté");
|
||||
let event_brigand = event("brigand du jorat");
|
||||
let event_raclette = event("raclette");
|
||||
let event_morbier = event("morbier");
|
||||
let event_gruyere = event("gruyère");
|
||||
let event_tome = event("tome");
|
||||
let event_mont_dor = event("mont d'or");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_comte.clone(), event_brigand.clone()],
|
||||
},
|
||||
Update::NewGapChunk {
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: None,
|
||||
gap: Gap { prev_token: "brillat-savarin".to_owned() },
|
||||
},
|
||||
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(2), 0),
|
||||
items: vec![event_morbier.clone(), event_mont_dor.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add other events in another room, to ensure filtering take the `room_id` into
|
||||
// account.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_tome.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let duplicated_events = self
|
||||
.filter_duplicated_events(
|
||||
room_id,
|
||||
vec![
|
||||
event_comte.event_id().unwrap().to_owned(),
|
||||
event_raclette.event_id().unwrap().to_owned(),
|
||||
event_morbier.event_id().unwrap().to_owned(),
|
||||
event_gruyere.event_id().unwrap().to_owned(),
|
||||
event_tome.event_id().unwrap().to_owned(),
|
||||
event_mont_dor.event_id().unwrap().to_owned(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(duplicated_events.len(), 3);
|
||||
assert_eq!(
|
||||
duplicated_events[0],
|
||||
(event_comte.event_id().unwrap(), Position::new(CId::new(0), 0))
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[1],
|
||||
(event_morbier.event_id().unwrap(), Position::new(CId::new(2), 0))
|
||||
);
|
||||
assert_eq!(
|
||||
duplicated_events[2],
|
||||
(event_mont_dor.event_id().unwrap(), Position::new(CId::new(2), 1))
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_find_event(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
|
||||
let event_comte = event("comté");
|
||||
let event_gruyere = event("gruyère");
|
||||
|
||||
// Add one event in one room.
|
||||
self.handle_linked_chunk_updates(
|
||||
room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_comte.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add another event in another room.
|
||||
self.handle_linked_chunk_updates(
|
||||
another_room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(0), 0),
|
||||
items: vec![event_gruyere.clone()],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now let's find the event.
|
||||
let event = self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find an event");
|
||||
|
||||
assert_eq!(event.event_id(), event_comte.event_id());
|
||||
|
||||
// Now let's try to find an event that exists, but not in the expected room.
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
|
||||
// Clearing the rooms also clears the event's storage.
|
||||
self.clear_all_rooms_chunks().await.expect("failed to clear all rooms chunks");
|
||||
assert!(self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
async fn test_find_event_relations(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*ALICE);
|
||||
|
||||
// Create event and related events for the first room.
|
||||
let eid1 = event_id!("$event1:matrix.org");
|
||||
let e1 = f.text_msg("comter").event_id(eid1).into_event();
|
||||
|
||||
let edit_eid1 = event_id!("$edit_event1:matrix.org");
|
||||
let edit_e1 = f
|
||||
.text_msg("* comté")
|
||||
.event_id(edit_eid1)
|
||||
.edit(eid1, RoomMessageEventContentWithoutRelation::text_plain("comté"))
|
||||
.into_event();
|
||||
|
||||
let reaction_eid1 = event_id!("$reaction_event1:matrix.org");
|
||||
let reaction_e1 = f.reaction(eid1, "👍").event_id(reaction_eid1).into_event();
|
||||
|
||||
let eid2 = event_id!("$event2:matrix.org");
|
||||
let e2 = f.text_msg("galette saucisse").event_id(eid2).into_event();
|
||||
|
||||
// Create events for the second room.
|
||||
let f = f.room(another_room_id);
|
||||
|
||||
let eid3 = event_id!("$event3:matrix.org");
|
||||
let e3 = f.text_msg("gruyère").event_id(eid3).into_event();
|
||||
|
||||
let reaction_eid3 = event_id!("$reaction_event3:matrix.org");
|
||||
let reaction_e3 = f.reaction(eid3, "👍").event_id(reaction_eid3).into_event();
|
||||
|
||||
// Save All The Things!
|
||||
self.save_event(room_id, e1).await.unwrap();
|
||||
self.save_event(room_id, edit_e1).await.unwrap();
|
||||
self.save_event(room_id, reaction_e1).await.unwrap();
|
||||
self.save_event(room_id, e2).await.unwrap();
|
||||
self.save_event(another_room_id, e3).await.unwrap();
|
||||
self.save_event(another_room_id, reaction_e3).await.unwrap();
|
||||
|
||||
// Finding relations without a filter returns all of them.
|
||||
let relations = self.find_event_relations(room_id, eid1, None).await.unwrap();
|
||||
assert_eq!(relations.len(), 2);
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
|
||||
|
||||
// Finding relations with a filter only returns a subset.
|
||||
let relations = self
|
||||
.find_event_relations(room_id, eid1, Some(&[RelationType::Replacement]))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relations.len(), 1);
|
||||
assert_eq!(relations[0].event_id().as_deref(), Some(edit_eid1));
|
||||
|
||||
let relations = self
|
||||
.find_event_relations(
|
||||
room_id,
|
||||
eid1,
|
||||
Some(&[RelationType::Replacement, RelationType::Annotation]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(relations.len(), 2);
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(edit_eid1)));
|
||||
assert!(relations.iter().any(|r| r.event_id().as_deref() == Some(reaction_eid1)));
|
||||
|
||||
// We can't find relations using the wrong room.
|
||||
let relations = self
|
||||
.find_event_relations(another_room_id, eid1, Some(&[RelationType::Replacement]))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(relations.is_empty());
|
||||
}
|
||||
|
||||
async fn test_save_event(&self) {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let another_room_id = room_id!("!r1:matrix.org");
|
||||
|
||||
let event = |msg: &str| make_test_event(room_id, msg);
|
||||
let event_comte = event("comté");
|
||||
let event_gruyere = event("gruyère");
|
||||
|
||||
// Add one event in one room.
|
||||
self.save_event(room_id, event_comte.clone()).await.unwrap();
|
||||
|
||||
// Add another event in another room.
|
||||
self.save_event(another_room_id, event_gruyere.clone()).await.unwrap();
|
||||
|
||||
// Events can be found, when searched in their own rooms.
|
||||
let event = self
|
||||
.find_event(room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find an event");
|
||||
assert_eq!(event.event_id(), event_comte.event_id());
|
||||
|
||||
let event = self
|
||||
.find_event(another_room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.expect("failed to find an event");
|
||||
assert_eq!(event.event_id(), event_gruyere.event_id());
|
||||
|
||||
// But they won't be returned when searching in the wrong room.
|
||||
assert!(self
|
||||
.find_event(another_room_id, event_comte.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
assert!(self
|
||||
.find_event(room_id, event_gruyere.event_id().unwrap().as_ref())
|
||||
.await
|
||||
.expect("failed to query for finding an event")
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your `EventCacheStore` implementation to run the
|
||||
@@ -564,6 +1076,13 @@ macro_rules! event_cache_store_integration_tests {
|
||||
event_cache_store.test_handle_updates_and_rebuild_linked_chunk().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_linked_chunk_incremental_loading() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_linked_chunk_incremental_loading().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_rebuild_empty_linked_chunk() {
|
||||
let event_cache_store =
|
||||
@@ -584,6 +1103,34 @@ macro_rules! event_cache_store_integration_tests {
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_remove_room().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_filter_duplicated_events() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_filter_duplicated_events().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_find_event().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_find_event_relations() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_find_event_relations().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_save_event() {
|
||||
let event_cache_store =
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_save_event().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ use crate::media::{MediaFormat, MediaRequestParameters};
|
||||
/// [`EventCacheStoreMedia`] integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`event_cache_store_media_integration_tests!`] macro.
|
||||
/// `event_cache_store_media_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStoreMediaIntegrationTests {
|
||||
@@ -53,6 +53,9 @@ pub trait EventCacheStoreMediaIntegrationTests {
|
||||
/// Test [`IgnoreMediaRetentionPolicy`] with the media content's retention
|
||||
/// policy expiry.
|
||||
async fn test_media_ignore_expiry(&self);
|
||||
|
||||
/// Test last media cleanup time storage.
|
||||
async fn test_store_last_media_cleanup_time(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -941,6 +944,25 @@ where
|
||||
let stored = self.get_media_content_inner(&request_5, time).await.unwrap();
|
||||
assert!(stored.is_none());
|
||||
}
|
||||
|
||||
async fn test_store_last_media_cleanup_time(&self) {
|
||||
let initial = self.last_media_cleanup_time_inner().await.unwrap();
|
||||
let new_time = initial.unwrap_or_else(SystemTime::now) + Duration::from_secs(60);
|
||||
|
||||
// With an empty policy.
|
||||
let policy = MediaRetentionPolicy::empty();
|
||||
self.clean_up_media_cache_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();
|
||||
|
||||
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
|
||||
@@ -1031,5 +1053,11 @@ macro_rules! event_cache_store_media_integration_tests {
|
||||
let event_cache_store_media = get_event_cache_store().await.unwrap();
|
||||
event_cache_store_media.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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ use serde::{Deserialize, Serialize};
|
||||
///
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
|
||||
#[non_exhaustive]
|
||||
pub struct MediaRetentionPolicy {
|
||||
/// The maximum authorized size of the overall media cache, in bytes.
|
||||
@@ -50,7 +51,7 @@ pub struct MediaRetentionPolicy {
|
||||
///
|
||||
/// Defaults to 400 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_cache_size: Option<usize>,
|
||||
pub max_cache_size: Option<u64>,
|
||||
|
||||
/// The maximum authorized size of a single media content, in bytes.
|
||||
///
|
||||
@@ -68,7 +69,7 @@ pub struct MediaRetentionPolicy {
|
||||
///
|
||||
/// Defaults to 20 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<usize>,
|
||||
pub max_file_size: Option<u64>,
|
||||
|
||||
/// The duration after which unaccessed media content is considered
|
||||
/// expired.
|
||||
@@ -79,6 +80,17 @@ pub struct MediaRetentionPolicy {
|
||||
/// Defaults to 60 days.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_access_expiry: Option<Duration>,
|
||||
|
||||
/// The duration between two automatic media cache cleanups.
|
||||
///
|
||||
/// If this is set, a cleanup will be triggered after the given duration
|
||||
/// is elapsed, at the next call to the media cache API. If this is set to
|
||||
/// zero, each call to the media cache API will trigger a cleanup. If this
|
||||
/// is `None`, cleanups will only occur if they are triggered manually.
|
||||
///
|
||||
/// Defaults to running cleanups daily.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cleanup_frequency: Option<Duration>,
|
||||
}
|
||||
|
||||
impl MediaRetentionPolicy {
|
||||
@@ -91,17 +103,22 @@ impl MediaRetentionPolicy {
|
||||
///
|
||||
/// This means that all media will be cached and cleanups have no effect.
|
||||
pub fn empty() -> Self {
|
||||
Self { max_cache_size: None, max_file_size: None, last_access_expiry: None }
|
||||
Self {
|
||||
max_cache_size: None,
|
||||
max_file_size: None,
|
||||
last_access_expiry: None,
|
||||
cleanup_frequency: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of the overall media cache, in bytes.
|
||||
pub fn with_max_cache_size(mut self, size: Option<usize>) -> Self {
|
||||
pub fn with_max_cache_size(mut self, size: Option<u64>) -> Self {
|
||||
self.max_cache_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum authorized size of a single media content, in bytes.
|
||||
pub fn with_max_file_size(mut self, size: Option<usize>) -> Self {
|
||||
pub fn with_max_file_size(mut self, size: Option<u64>) -> Self {
|
||||
self.max_file_size = size;
|
||||
self
|
||||
}
|
||||
@@ -113,6 +130,12 @@ impl MediaRetentionPolicy {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the duration between two automatic media cache cleanups.
|
||||
pub fn with_cleanup_frequency(mut self, duration: Option<Duration>) -> Self {
|
||||
self.cleanup_frequency = duration;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether this policy has limitations.
|
||||
///
|
||||
/// If this policy has no limitations, a cleanup job would have no effect.
|
||||
@@ -130,7 +153,7 @@ impl MediaRetentionPolicy {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The overall size of the media cache to check, in bytes.
|
||||
pub fn exceeds_max_cache_size(&self, size: usize) -> bool {
|
||||
pub fn exceeds_max_cache_size(&self, size: u64) -> bool {
|
||||
self.max_cache_size.is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
@@ -138,7 +161,7 @@ impl MediaRetentionPolicy {
|
||||
/// bytes.
|
||||
///
|
||||
/// This is the lowest value between `max_cache_size` and `max_file_size`.
|
||||
pub fn computed_max_file_size(&self) -> Option<usize> {
|
||||
pub fn computed_max_file_size(&self) -> Option<u64> {
|
||||
match (self.max_cache_size, self.max_file_size) {
|
||||
(None, None) => None,
|
||||
(None, Some(size)) => Some(size),
|
||||
@@ -153,7 +176,7 @@ impl MediaRetentionPolicy {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The size of the media content to check, in bytes.
|
||||
pub fn exceeds_max_file_size(&self, size: usize) -> bool {
|
||||
pub fn exceeds_max_file_size(&self, size: u64) -> bool {
|
||||
self.computed_max_file_size().is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
@@ -178,6 +201,24 @@ impl MediaRetentionPolicy {
|
||||
.is_ok_and(|elapsed| elapsed >= max_duration)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether an automatic media cache cleanup should be triggered given the
|
||||
/// time of the last cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `current_time` - The current time.
|
||||
///
|
||||
/// * `last_cleanup_time` - The time of the last media cache cleanup.
|
||||
pub fn should_clean_up(&self, current_time: SystemTime, last_cleanup_time: SystemTime) -> bool {
|
||||
self.cleanup_frequency.is_some_and(|max_duration| {
|
||||
current_time
|
||||
.duration_since(last_cleanup_time)
|
||||
// If this returns an error, the last cleanup time is newer than the current time.
|
||||
// This shouldn't happen but in this case no cleanup job is needed.
|
||||
.is_ok_and(|elapsed| elapsed >= max_duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaRetentionPolicy {
|
||||
@@ -189,6 +230,8 @@ impl Default for MediaRetentionPolicy {
|
||||
max_file_size: Some(20 * 1024 * 1024),
|
||||
// 60 days.
|
||||
last_access_expiry: Some(Duration::from_secs(60 * 24 * 60 * 60)),
|
||||
// 1 day.
|
||||
cleanup_frequency: Some(Duration::from_secs(24 * 60 * 60)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,4 +370,36 @@ mod tests {
|
||||
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_cleanup_frequency() {
|
||||
let epoch = SystemTime::UNIX_EPOCH;
|
||||
let epoch_plus_60 = epoch + Duration::from_secs(60);
|
||||
let epoch_plus_120 = epoch + Duration::from_secs(120);
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(0)));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(30)));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(60)));
|
||||
assert!(policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
|
||||
policy = policy.with_cleanup_frequency(Some(Duration::from_secs(90)));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_60));
|
||||
assert!(!policy.should_clean_up(epoch_plus_60, epoch_plus_120));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fmt;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{locks::Mutex, AsyncTraitDeps};
|
||||
use matrix_sdk_common::{
|
||||
executor::{spawn, JoinHandle},
|
||||
locks::Mutex,
|
||||
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
|
||||
};
|
||||
use ruma::{time::SystemTime, MxcUri};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::error;
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
|
||||
@@ -28,6 +33,11 @@ use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParamet
|
||||
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
|
||||
#[derive(Debug)]
|
||||
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
inner: Arc<MediaServiceInner<Time>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MediaServiceInner<Time: TimeProvider = DefaultTimeProvider> {
|
||||
/// The time provider.
|
||||
time_provider: Time,
|
||||
|
||||
@@ -36,6 +46,15 @@ pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
|
||||
|
||||
/// A mutex to ensure a single cleanup is running at a time.
|
||||
cleanup_guard: AsyncMutex<()>,
|
||||
|
||||
/// The time of the last media cache cleanup.
|
||||
last_media_cleanup_time: Mutex<Option<SystemTime>>,
|
||||
|
||||
/// The [`JoinHandle`] for an automatic media cleanup task.
|
||||
///
|
||||
/// Used to ensure that only one automatic cleanup is running at a time, and
|
||||
/// to stop the cleanup when the [`MediaServiceInner`] is dropped.
|
||||
automatic_media_cleanup_join_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl MediaService {
|
||||
@@ -56,16 +75,20 @@ impl Default for MediaService {
|
||||
|
||||
impl<Time> MediaService<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
Time: TimeProvider + 'static,
|
||||
{
|
||||
/// Construct a new `MediaService` with the given `TimeProvider` and an
|
||||
/// empty `MediaRetentionPolicy`.
|
||||
fn with_time_provider(time_provider: Time) -> Self {
|
||||
Self {
|
||||
let inner = MediaServiceInner {
|
||||
time_provider,
|
||||
policy: Mutex::new(MediaRetentionPolicy::empty()),
|
||||
cleanup_guard: AsyncMutex::new(()),
|
||||
}
|
||||
last_media_cleanup_time: Mutex::new(None),
|
||||
automatic_media_cleanup_join_handle: Mutex::new(None),
|
||||
};
|
||||
|
||||
Self { inner: Arc::new(inner) }
|
||||
}
|
||||
|
||||
/// Restore the previous state of the [`MediaRetentionPolicy`] from data
|
||||
@@ -76,10 +99,23 @@ where
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` that was persisted in the store.
|
||||
pub fn restore(&self, policy: Option<MediaRetentionPolicy>) {
|
||||
pub fn restore(
|
||||
&self,
|
||||
policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: Option<SystemTime>,
|
||||
) {
|
||||
if let Some(policy) = policy {
|
||||
*self.policy.lock() = policy;
|
||||
*self.inner.policy.lock() = policy;
|
||||
}
|
||||
|
||||
if let Some(time) = last_media_cleanup_time {
|
||||
*self.inner.last_media_cleanup_time.lock() = Some(time);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current time from the inner [`TimeProvider`].
|
||||
fn now(&self) -> SystemTime {
|
||||
self.inner.time_provider.now()
|
||||
}
|
||||
|
||||
/// Set the `MediaRetentionPolicy` of this service.
|
||||
@@ -89,21 +125,23 @@ where
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia>(
|
||||
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Store::Error> {
|
||||
store.set_media_retention_policy_inner(policy).await?;
|
||||
|
||||
*self.policy.lock() = policy;
|
||||
*self.inner.policy.lock() = policy;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, self.now());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the `MediaRetentionPolicy` of this service.
|
||||
pub fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
*self.policy.lock()
|
||||
*self.inner.policy.lock()
|
||||
}
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
@@ -118,7 +156,7 @@ where
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia>(
|
||||
pub async fn add_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -128,21 +166,20 @@ where
|
||||
let policy = self.media_retention_policy();
|
||||
|
||||
if ignore_policy == IgnoreMediaRetentionPolicy::No
|
||||
&& policy.exceeds_max_file_size(content.len())
|
||||
&& policy.exceeds_max_file_size(content.len() as u64)
|
||||
{
|
||||
// We do not cache the content.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current_time = self.now();
|
||||
store
|
||||
.add_media_content_inner(
|
||||
request,
|
||||
content,
|
||||
self.time_provider.now(),
|
||||
policy,
|
||||
ignore_policy,
|
||||
)
|
||||
.await
|
||||
.add_media_content_inner(request, content, current_time, policy, ignore_policy)
|
||||
.await?;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
@@ -174,12 +211,17 @@ where
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia>(
|
||||
pub async fn get_media_content<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_inner(request, self.time_provider.now()).await
|
||||
let current_time = self.now();
|
||||
let content = store.get_media_content_inner(request, current_time).await?;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
@@ -190,12 +232,17 @@ where
|
||||
/// * `store` - The `EventCacheStoreMedia`.
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia>(
|
||||
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia + 'static>(
|
||||
&self,
|
||||
store: &Store,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Store::Error> {
|
||||
store.get_media_content_for_uri_inner(uri, self.time_provider.now()).await
|
||||
let current_time = self.now();
|
||||
let content = store.get_media_content_for_uri_inner(uri, current_time).await?;
|
||||
|
||||
self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
@@ -209,7 +256,15 @@ where
|
||||
&self,
|
||||
store: &Store,
|
||||
) -> Result<(), Store::Error> {
|
||||
let Ok(_guard) = self.cleanup_guard.try_lock() else {
|
||||
self.clean_up_media_cache_inner(store, self.now()).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner<Store: EventCacheStoreMedia>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Store::Error> {
|
||||
let Ok(_guard) = self.inner.cleanup_guard.try_lock() else {
|
||||
// There is another ongoing cleanup.
|
||||
return Ok(());
|
||||
};
|
||||
@@ -221,7 +276,76 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
store.clean_up_media_cache_inner(policy, self.time_provider.now()).await
|
||||
store.clean_up_media_cache_inner(policy, current_time).await?;
|
||||
|
||||
*self.inner.last_media_cleanup_time.lock() = Some(current_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn an automatic media cache cleanup, according to the media retention
|
||||
/// policy.
|
||||
///
|
||||
/// A cleanup will be spawned if:
|
||||
/// * 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>(
|
||||
&self,
|
||||
store: &Store,
|
||||
current_time: SystemTime,
|
||||
) {
|
||||
let mut join_handle = self.inner.automatic_media_cleanup_join_handle.lock();
|
||||
|
||||
if join_handle.as_ref().is_some_and(|join_handle| !join_handle.is_finished()) {
|
||||
// There is an ongoing automatic media cache cleanup.
|
||||
return;
|
||||
}
|
||||
|
||||
let policy = self.media_retention_policy();
|
||||
if policy.cleanup_frequency.is_none() || !policy.has_limitations() {
|
||||
// Automatic cleanups are disabled or have no effect.
|
||||
return;
|
||||
}
|
||||
|
||||
let last_media_cleanup_time = *self.inner.last_media_cleanup_time.lock();
|
||||
if last_media_cleanup_time.is_some_and(|last_cleanup_time| {
|
||||
!policy.should_clean_up(current_time, last_cleanup_time)
|
||||
}) {
|
||||
// It is not time to clean up.
|
||||
return;
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
let store = store.clone();
|
||||
|
||||
let handle = spawn(async move {
|
||||
if let Err(error) = this.clean_up_media_cache_inner(&store, current_time).await {
|
||||
error!("Failed to run automatic media cache cleanup: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
*join_handle = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Time> Clone for MediaService<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self { inner: self.inner.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Time> Drop for MediaServiceInner<Time>
|
||||
where
|
||||
Time: TimeProvider,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
if let Some(join_handle) = self.automatic_media_cleanup_join_handle.lock().take() {
|
||||
join_handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,9 +358,9 @@ where
|
||||
/// over the `SystemTime`s provided to the store.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps {
|
||||
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
|
||||
/// The error type used by this media cache store.
|
||||
type Error: fmt::Debug + Into<EventCacheStoreError>;
|
||||
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
|
||||
|
||||
/// The persisted media retention policy in the media cache.
|
||||
async fn media_retention_policy_inner(
|
||||
@@ -340,12 +464,15 @@ pub trait EventCacheStoreMedia: AsyncTraitDeps {
|
||||
/// `cleanup_frequency` will be ignored.
|
||||
///
|
||||
/// * `current_time` - The current time, to be used to check for expired
|
||||
/// content.
|
||||
/// 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
|
||||
@@ -385,7 +512,7 @@ impl IgnoreMediaRetentionPolicy {
|
||||
|
||||
/// An abstract trait to provide the current `SystemTime` for the
|
||||
/// [`MediaService`].
|
||||
pub trait TimeProvider {
|
||||
pub trait TimeProvider: SendOutsideWasm + SyncOutsideWasm {
|
||||
/// The current time.
|
||||
fn now(&self) -> SystemTime;
|
||||
}
|
||||
@@ -402,7 +529,10 @@ impl TimeProvider for DefaultTimeProvider {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fmt, sync::MutexGuard};
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{Arc, MutexGuard},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
@@ -420,9 +550,9 @@ mod tests {
|
||||
media::{MediaFormat, MediaRequestParameters, UniqueKey},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct MockEventCacheStoreMedia {
|
||||
inner: Mutex<MockEventCacheStoreMediaInner>,
|
||||
inner: Arc<Mutex<MockEventCacheStoreMediaInner>>,
|
||||
}
|
||||
|
||||
impl MockEventCacheStoreMedia {
|
||||
@@ -529,7 +659,7 @@ mod tests {
|
||||
) -> Result<(), Self::Error> {
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(content.len()) {
|
||||
if !ignore_policy && policy.exceeds_max_file_size(content.len() as u64) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -617,6 +747,10 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(self.inner().cleanup_time)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -658,7 +792,7 @@ mod tests {
|
||||
|
||||
// By default an empty policy is used.
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
service.restore(None);
|
||||
service.restore(None, None);
|
||||
assert!(!service.media_retention_policy().has_limitations());
|
||||
assert!(!store.accessed());
|
||||
|
||||
@@ -676,7 +810,7 @@ mod tests {
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
@@ -689,7 +823,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
@@ -712,12 +846,12 @@ mod tests {
|
||||
assert!(media_content.ignore_policy);
|
||||
|
||||
// Try a cleanup. With the empty policy the store should not be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(!store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -747,7 +881,7 @@ mod tests {
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Check that restoring the policy works.
|
||||
service.restore(Some(MediaRetentionPolicy::default()));
|
||||
service.restore(Some(MediaRetentionPolicy::default()), None);
|
||||
assert_eq!(service.media_retention_policy(), MediaRetentionPolicy::default());
|
||||
assert!(!store.accessed());
|
||||
|
||||
@@ -779,7 +913,7 @@ mod tests {
|
||||
assert_eq!(media_content.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from request.
|
||||
@@ -792,7 +926,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
@@ -805,7 +939,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Add big media, it will not work because it is bigger than the max file size.
|
||||
@@ -858,7 +992,7 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
// Get media from URI.
|
||||
@@ -871,14 +1005,101 @@ mod tests {
|
||||
assert_eq!(media.last_access, now);
|
||||
|
||||
// Try a cleanup, the store should be accessed.
|
||||
assert_eq!(store.inner().cleanup_time, None);
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
|
||||
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.time_provider.set_now(now);
|
||||
service.inner.time_provider.set_now(now);
|
||||
store.reset_accessed();
|
||||
|
||||
service.clean_up_media_cache(&store).await.unwrap();
|
||||
assert!(store.accessed());
|
||||
assert_eq!(store.inner().cleanup_time, Some(now));
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_service_automatic_cleanup() {
|
||||
// 64 bytes content.
|
||||
let content = vec![0; 64];
|
||||
|
||||
let uri_1 = mxc_uri!("mxc://localhost/media-1");
|
||||
let request_1 = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri_1.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
let uri_2 = mxc_uri!("mxc://localhost/media-2");
|
||||
let request_2 = MediaRequestParameters {
|
||||
source: MediaSource::Plain(uri_2.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let now = SystemTime::UNIX_EPOCH;
|
||||
|
||||
let store = MockEventCacheStoreMedia::default();
|
||||
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
|
||||
|
||||
// Set an empty policy.
|
||||
let policy = MediaRetentionPolicy::empty();
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
|
||||
// Add the contents.
|
||||
service
|
||||
.add_media_content(&store, &request_1, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.unwrap();
|
||||
service
|
||||
.add_media_content(&store, &request_2, content, IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Try to launch an automatic cleanup.
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.maybe_spawn_automatic_media_cache_cleanup(&store, now);
|
||||
|
||||
// No cleanup was spawned since automatic cleanups are disabled.
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Set a policy with automatic cleanup every hour.
|
||||
let policy = MediaRetentionPolicy::empty()
|
||||
.with_cleanup_frequency(Some(Duration::from_secs(60 * 60)));
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
|
||||
// No cleanup was spawned since the policy has no limitations.
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Set a policy with automatic cleanup every hour and a max file size.
|
||||
let policy = MediaRetentionPolicy::empty()
|
||||
.with_cleanup_frequency(Some(Duration::from_secs(60 * 60)))
|
||||
.with_max_file_size(Some(512));
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.set_media_retention_policy(&store, policy).await.unwrap();
|
||||
|
||||
// A cleanup was spawned since there was no last_media_cleanup_time.
|
||||
let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
|
||||
join_handle.await.unwrap();
|
||||
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
|
||||
// Try again one minute in the future, nothing is spawned because we need to
|
||||
// wait for one hour.
|
||||
let now = now + Duration::from_secs(60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.get_media_content(&store, &request_1).await.unwrap();
|
||||
|
||||
assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
|
||||
|
||||
// Try again 2 hours in the future, another cleanup is spawned.
|
||||
let now = now + Duration::from_secs(2 * 60 * 60);
|
||||
service.inner.time_provider.set_now(now);
|
||||
service.get_media_content_for_uri(&store, uri_1).await.unwrap();
|
||||
|
||||
let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
|
||||
join_handle.await.unwrap();
|
||||
|
||||
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,20 +12,30 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
num::NonZeroUsize,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{relational::RelationalLinkedChunk, RawChunk, Update},
|
||||
linked_chunk::{
|
||||
relational::RelationalLinkedChunk, ChunkIdentifier, ChunkIdentifierGenerator, Position,
|
||||
RawChunk, Update,
|
||||
},
|
||||
ring_buffer::RingBuffer,
|
||||
store_locks::memory_store_helper::try_take_leased_lock,
|
||||
};
|
||||
use ruma::{
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
MxcUri, OwnedMxcUri, RoomId,
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
compute_filters_string, extract_event_relation,
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
EventCacheStore, EventCacheStoreError, Result,
|
||||
};
|
||||
@@ -37,10 +47,9 @@ use crate::{
|
||||
/// In-memory, non-persistent implementation of the `EventCacheStore`.
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStore {
|
||||
inner: StdRwLock<MemoryStoreInner>,
|
||||
inner: Arc<StdRwLock<MemoryStoreInner>>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
@@ -48,8 +57,9 @@ pub struct MemoryStore {
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
events: RelationalLinkedChunk<Event, Gap>,
|
||||
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: SystemTime,
|
||||
}
|
||||
|
||||
/// A media content in the `MemoryStore`.
|
||||
@@ -71,20 +81,24 @@ struct MediaContent {
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
|
||||
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: StdRwLock::new(MemoryStoreInner {
|
||||
inner: Arc::new(StdRwLock::new(MemoryStoreInner {
|
||||
media: RingBuffer::new(NUMBER_OF_MEDIAS),
|
||||
leases: Default::default(),
|
||||
events: RelationalLinkedChunk::new(),
|
||||
media_retention_policy: None,
|
||||
}),
|
||||
// No need to call `restore()` since nothing is persisted.
|
||||
media_service: MediaService::new(),
|
||||
last_media_cleanup_time,
|
||||
})),
|
||||
media_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,14 +137,37 @@ impl EventCacheStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reload_linked_chunk(
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.reload_chunks(room_id)
|
||||
.load_all_chunks(room_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_last_chunk(room_id)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.load_previous_chunk(room_id, before_chunk_identifier)
|
||||
.map_err(|err| EventCacheStoreError::InvalidData { details: err })
|
||||
}
|
||||
|
||||
@@ -139,6 +176,97 @@ impl EventCacheStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
mut events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
// Collect all duplicated events.
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let mut duplicated_events = Vec::new();
|
||||
|
||||
for (event, position) in inner.events.unordered_room_items(room_id) {
|
||||
// If `events` is empty, we can short-circuit.
|
||||
if events.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(known_event_id) = event.event_id() {
|
||||
// This event is a duplicate!
|
||||
if let Some(index) =
|
||||
events.iter().position(|new_event_id| &known_event_id == new_event_id)
|
||||
{
|
||||
duplicated_events.push((events.remove(index), position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(duplicated_events)
|
||||
}
|
||||
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let event = inner.events.items().find_map(|(event, this_room_id)| {
|
||||
(room_id == this_room_id && event.event_id()? == event_id).then_some(event.clone())
|
||||
});
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn find_event_relations(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filters: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let filters = compute_filters_string(filters);
|
||||
|
||||
let related_events = inner
|
||||
.events
|
||||
.items()
|
||||
.filter_map(|(event, this_room_id)| {
|
||||
// Must be in the same room.
|
||||
if room_id != this_room_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must have a relation.
|
||||
let (related_to, rel_type) = extract_event_relation(event.raw())?;
|
||||
|
||||
// Must relate to the target item.
|
||||
if related_to != event_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must not be filtered out.
|
||||
if let Some(filters) = &filters {
|
||||
filters.contains(&rel_type).then_some(event.clone())
|
||||
} else {
|
||||
Some(event.clone())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(related_events)
|
||||
}
|
||||
|
||||
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");
|
||||
return Ok(());
|
||||
}
|
||||
self.inner.write().unwrap().events.save_item(room_id.to_owned(), event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
@@ -268,7 +396,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len()) {
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
};
|
||||
@@ -374,7 +502,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
// 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())
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -392,7 +520,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
// 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(
|
||||
(0usize, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
(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.
|
||||
@@ -401,7 +529,7 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
|
||||
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()) {
|
||||
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
|
||||
@@ -431,8 +559,14 @@ impl EventCacheStoreMedia for MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
|
||||
@@ -32,6 +32,12 @@ use matrix_sdk_common::store_locks::{
|
||||
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
|
||||
};
|
||||
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
|
||||
use ruma::{
|
||||
events::{relation::RelationType, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
OwnedEventId,
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreIntegrationTests;
|
||||
@@ -193,3 +199,51 @@ impl BackingStore for LockableEventCacheStore {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to extract the relation information from an event.
|
||||
///
|
||||
/// If the event isn't in relation to another event, then this will return
|
||||
/// `None`. Otherwise, returns both the event id this event relates to, and the
|
||||
/// kind of relation as a string (e.g. `m.replace`).
|
||||
pub fn extract_event_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(OwnedEventId, String)> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct RelatesTo {
|
||||
event_id: OwnedEventId,
|
||||
rel_type: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct EventContent {
|
||||
#[serde(rename = "m.relates_to")]
|
||||
rel: Option<RelatesTo>,
|
||||
}
|
||||
|
||||
match event.get_field::<EventContent>("content") {
|
||||
Ok(event_content) => {
|
||||
event_content.and_then(|c| c.rel).map(|rel| (rel.event_id, rel.rel_type))
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("when extracting relation data from an event: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the list of string filters to be applied when looking for an event's
|
||||
/// relations.
|
||||
// TODO: get Ruma fix from https://github.com/ruma/ruma/pull/2052, and get rid of this function
|
||||
// then.
|
||||
pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<String>> {
|
||||
filters.map(|filter| {
|
||||
filter
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if *f == RelationType::Replacement {
|
||||
"m.replace".to_owned()
|
||||
} else {
|
||||
f.to_string()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{RawChunk, Update},
|
||||
linked_chunk::{ChunkIdentifier, ChunkIdentifierGenerator, Position, RawChunk, Update},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{MxcUri, RoomId};
|
||||
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
|
||||
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
@@ -69,17 +69,80 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
|
||||
/// Return all the raw components of a linked chunk, so the caller may
|
||||
/// reconstruct the linked chunk later.
|
||||
async fn reload_linked_chunk(
|
||||
#[doc(hidden)]
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Load the last chunk of the `LinkedChunk` holding all events of the room
|
||||
/// identified by `room_id`.
|
||||
///
|
||||
/// This is used to iteratively load events for the `EventCache`.
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error>;
|
||||
|
||||
/// Load the chunk before the chunk identified by `before_chunk_identifier`
|
||||
/// of the `LinkedChunk` holding all events of the room identified by
|
||||
/// `room_id`
|
||||
///
|
||||
/// This is used to iteratively load events for the `EventCache`.
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error>;
|
||||
|
||||
/// Clear persisted events for all the rooms.
|
||||
///
|
||||
/// This will empty and remove all the linked chunks stored previously,
|
||||
/// using the above [`Self::handle_linked_chunk_updates`] methods.
|
||||
/// using the above [`Self::handle_linked_chunk_updates`] methods. It
|
||||
/// must *also* delete all the events' content, if they were stored in a
|
||||
/// separate table.
|
||||
///
|
||||
/// ⚠ This is meant only for super specific use cases, where there shouldn't
|
||||
/// be any live in-memory linked chunks. In general, prefer using
|
||||
/// `EventCache::clear_all_rooms()` from the common SDK crate.
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Given a set of event IDs, return the duplicated events along with their
|
||||
/// position if there are any.
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error>;
|
||||
|
||||
/// Find an event by its ID.
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error>;
|
||||
|
||||
/// Find all the events that relate to a given event.
|
||||
///
|
||||
/// An additional filter can be provided to only retrieve related events for
|
||||
/// a certain relationship.
|
||||
async fn find_event_relations(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error>;
|
||||
|
||||
/// Save an event, that might or might not be part of an existing linked
|
||||
/// chunk.
|
||||
///
|
||||
/// If the event has no event id, it will not be saved, and the function
|
||||
/// must return an Ok result early.
|
||||
///
|
||||
/// If the event was already stored with the same id, it must be replaced,
|
||||
/// 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.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -236,17 +299,61 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn reload_linked_chunk(
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.reload_linked_chunk(room_id).await.map_err(Into::into)
|
||||
self.0.load_all_chunks(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
|
||||
self.0.load_last_chunk(room_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
|
||||
self.0.load_previous_chunk(room_id, before_chunk_identifier).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.0.clear_all_rooms_chunks().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn filter_duplicated_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
events: Vec<OwnedEventId>,
|
||||
) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
|
||||
self.0.filter_duplicated_events(room_id, events).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn find_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
) -> Result<Option<Event>, Self::Error> {
|
||||
self.0.find_event(room_id, event_id).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn find_event_relations(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_id: &EventId,
|
||||
filter: Option<&[RelationType]>,
|
||||
) -> Result<Vec<Event>, Self::Error> {
|
||||
self.0.find_event_relations(room_id, event_id, filter).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,
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruma::{
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
message::SyncRoomMessageEvent,
|
||||
message::{MessageType, SyncRoomMessageEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
@@ -67,8 +67,13 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
match event {
|
||||
// Suitable - we have an m.room.message that was not redacted or edited
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
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() {
|
||||
@@ -589,6 +594,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[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)]
|
||||
|
||||
@@ -25,6 +25,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub use crate::error::{Error, Result};
|
||||
|
||||
mod client;
|
||||
pub use client::RequestedRequiredStates;
|
||||
pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
mod error;
|
||||
@@ -55,9 +56,9 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
apply_redaction, Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate,
|
||||
RoomMemberships, RoomState, RoomStateFilter,
|
||||
apply_redaction, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
|
||||
RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember,
|
||||
RoomMembersUpdate, RoomMemberships, RoomState, RoomStateFilter,
|
||||
};
|
||||
pub use store::{
|
||||
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
|
||||
|
||||
@@ -110,8 +110,8 @@
|
||||
//! events ids in both sets. As a matter of fact, we have to manually handle
|
||||
//! this edge case here. I hope that having an event database will help avoid
|
||||
//! this kind of workaround here later.
|
||||
//! - In addition to that, and as noted in the timeline code, it seems that the
|
||||
//! sliding-sync proxy could return the same event multiple times in a sync
|
||||
//! - In addition to that, and as noted in the timeline code, it seems that
|
||||
//! sliding sync could return the same event multiple times in a sync
|
||||
//! timeline, leading to incorrect results. We have to take that into account
|
||||
//! by resetting the read counts *every* time we see an event that was the
|
||||
//! target of the latest active read receipt.
|
||||
@@ -245,10 +245,9 @@ impl RoomReadReceipts {
|
||||
let mut counting_receipts = false;
|
||||
|
||||
for event in events {
|
||||
// The sliding sync proxy sometimes sends the same event multiple times, so it
|
||||
// can be at the beginning and end of a batch, for instance. In that
|
||||
// case, just reset every time we see the event matching the
|
||||
// receipt. NOTE: SS proxy workaround.
|
||||
// Sliding sync sometimes sends the same event multiple times, so it can be at
|
||||
// the beginning and end of a batch, for instance. In that case, just reset
|
||||
// every time we see the event matching the receipt.
|
||||
if let Some(event_id) = event.event_id() {
|
||||
if event_id == receipt_event_id {
|
||||
// Bingo! Switch over to the counting state, after resetting the
|
||||
@@ -925,7 +924,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = Default::default();
|
||||
compute_unread_counts(
|
||||
@@ -1007,7 +1006,7 @@ mod tests {
|
||||
receipt_type_1.clone(),
|
||||
receipt_thread_2.clone(),
|
||||
)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
// When I compute the notifications for this room (with no new events),
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
@@ -1070,7 +1069,7 @@ mod tests {
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
assert!(read_receipts.pending.is_empty());
|
||||
@@ -1104,7 +1103,7 @@ mod tests {
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
// Sync with a read receipt *and* a single event that was already known: in that
|
||||
// case, only consider the new events in isolation, and compute the
|
||||
@@ -1372,7 +1371,7 @@ mod tests {
|
||||
ReceiptType::Read,
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1391,7 +1390,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
@@ -1409,7 +1408,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1426,7 +1425,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1443,7 +1442,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
@@ -1464,7 +1463,7 @@ mod tests {
|
||||
.add(event_id!("$4"), myself, ReceiptType::ReadPrivate, ReceiptThread::Unthreaded)
|
||||
.add(event_id!("$6"), myself, ReceiptType::ReadPrivate, ReceiptThread::Main)
|
||||
.add(event_id!("$3"), myself, ReceiptType::Read, ReceiptThread::Main)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending.len(), 1);
|
||||
@@ -1542,7 +1541,7 @@ mod tests {
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.build();
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
|
||||
|
||||
+44
-33
@@ -26,36 +26,23 @@ use ruma::{
|
||||
};
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
|
||||
use crate::{store::Store, RoomInfo, StateChanges};
|
||||
use super::super::Context;
|
||||
use crate::{store::BaseStateStore, RoomInfo, StateChanges};
|
||||
|
||||
/// Applies a function to an existing `RoomInfo` if present in changes, or one
|
||||
/// loaded from the database.
|
||||
fn map_info<F: FnOnce(&mut RoomInfo)>(
|
||||
room_id: &RoomId,
|
||||
changes: &mut StateChanges,
|
||||
store: &Store,
|
||||
f: F,
|
||||
) {
|
||||
if let Some(info) = changes.room_infos.get_mut(room_id) {
|
||||
f(info);
|
||||
} else if let Some(room) = store.room(room_id) {
|
||||
let mut info = room.clone_info();
|
||||
f(&mut info);
|
||||
changes.add_room(info);
|
||||
} else {
|
||||
debug!(room = %room_id, "couldn't find room in state changes or store");
|
||||
}
|
||||
/// Create the [`Global`] account data processor.
|
||||
pub fn global(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Global {
|
||||
Global::process(events)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) struct AccountDataProcessor {
|
||||
pub struct Global {
|
||||
parsed_events: Vec<AnyGlobalAccountDataEvent>,
|
||||
raw_by_type: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
|
||||
}
|
||||
|
||||
impl AccountDataProcessor {
|
||||
impl Global {
|
||||
/// Creates a new processor for global account data.
|
||||
pub fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
|
||||
fn process(events: &[Raw<AnyGlobalAccountDataEvent>]) -> Self {
|
||||
let mut raw_by_type = BTreeMap::new();
|
||||
let mut parsed_events = Vec::new();
|
||||
|
||||
@@ -87,23 +74,24 @@ impl AccountDataProcessor {
|
||||
/// from the global account data and adds it to the room infos to
|
||||
/// save.
|
||||
#[instrument(skip_all)]
|
||||
pub(crate) fn process_direct_rooms(
|
||||
fn process_direct_rooms(
|
||||
&self,
|
||||
events: &[AnyGlobalAccountDataEvent],
|
||||
store: &Store,
|
||||
changes: &mut StateChanges,
|
||||
state_store: &BaseStateStore,
|
||||
state_changes: &mut StateChanges,
|
||||
) {
|
||||
for event in events {
|
||||
let AnyGlobalAccountDataEvent::Direct(direct_event) = event else { continue };
|
||||
|
||||
let mut new_dms = HashMap::<&RoomId, HashSet<OwnedDirectUserIdentifier>>::new();
|
||||
|
||||
for (user_identifier, rooms) in direct_event.content.iter() {
|
||||
for room_id in rooms {
|
||||
new_dms.entry(room_id).or_default().insert(user_identifier.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let rooms = store.rooms();
|
||||
let rooms = state_store.rooms();
|
||||
let mut old_dms = rooms
|
||||
.iter()
|
||||
.filter_map(|r| {
|
||||
@@ -120,7 +108,7 @@ impl AccountDataProcessor {
|
||||
}
|
||||
}
|
||||
trace!(?room_id, targets = ?new_direct_targets, "Marking room as direct room");
|
||||
map_info(room_id, changes, store, |info| {
|
||||
map_info(room_id, state_changes, state_store, |info| {
|
||||
info.base_info.dm_targets = new_direct_targets;
|
||||
});
|
||||
}
|
||||
@@ -128,17 +116,17 @@ impl AccountDataProcessor {
|
||||
// Remove the targets of old direct chats.
|
||||
for room_id in old_dms.keys() {
|
||||
trace!(?room_id, "Unmarking room as direct room");
|
||||
map_info(room_id, changes, store, |info| {
|
||||
map_info(room_id, state_changes, state_store, |info| {
|
||||
info.base_info.dm_targets.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the processed data to the state changes.
|
||||
pub async fn apply(mut self, changes: &mut StateChanges, store: &Store) {
|
||||
/// Applies the processed data to the state changes and the state store.
|
||||
pub async fn apply(mut self, context: &mut Context, state_store: &BaseStateStore) {
|
||||
// Fill in the content of `changes.account_data`.
|
||||
mem::swap(&mut changes.account_data, &mut self.raw_by_type);
|
||||
mem::swap(&mut context.state_changes.account_data, &mut self.raw_by_type);
|
||||
|
||||
// Process direct rooms.
|
||||
let has_new_direct_room_data = self
|
||||
@@ -147,16 +135,39 @@ impl AccountDataProcessor {
|
||||
.any(|event| event.event_type() == GlobalAccountDataEventType::Direct);
|
||||
|
||||
if has_new_direct_room_data {
|
||||
self.process_direct_rooms(&self.parsed_events, store, changes);
|
||||
self.process_direct_rooms(&self.parsed_events, state_store, &mut context.state_changes);
|
||||
} else if let Ok(Some(direct_account_data)) =
|
||||
store.get_account_data_event(GlobalAccountDataEventType::Direct).await
|
||||
state_store.get_account_data_event(GlobalAccountDataEventType::Direct).await
|
||||
{
|
||||
debug!("Found direct room data in the Store, applying it");
|
||||
if let Ok(direct_account_data) = direct_account_data.deserialize() {
|
||||
self.process_direct_rooms(&[direct_account_data], store, changes);
|
||||
self.process_direct_rooms(
|
||||
&[direct_account_data],
|
||||
state_store,
|
||||
&mut context.state_changes,
|
||||
);
|
||||
} else {
|
||||
warn!("Failed to deserialize direct room account data");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a function to an existing `RoomInfo` if present in changes, or one
|
||||
/// loaded from the database.
|
||||
fn map_info<F: FnOnce(&mut RoomInfo)>(
|
||||
room_id: &RoomId,
|
||||
changes: &mut StateChanges,
|
||||
store: &BaseStateStore,
|
||||
f: F,
|
||||
) {
|
||||
if let Some(info) = changes.room_infos.get_mut(room_id) {
|
||||
f(info);
|
||||
} else if let Some(room) = store.room(room_id) {
|
||||
let mut info = room.clone_info();
|
||||
f(&mut info);
|
||||
changes.add_room(info);
|
||||
} else {
|
||||
debug!(room = %room_id, "couldn't find room in state changes or store");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod global;
|
||||
mod room;
|
||||
|
||||
pub use global::{global, Global};
|
||||
pub use room::for_room;
|
||||
@@ -0,0 +1,143 @@
|
||||
// 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 ruma::{
|
||||
events::{marked_unread::MarkedUnreadEventContent, AnyRoomAccountDataEvent},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
};
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::super::{Context, RoomInfoNotableUpdates};
|
||||
use crate::{store::BaseStateStore, RoomInfo, RoomInfoNotableUpdateReasons, StateChanges};
|
||||
|
||||
#[instrument(skip_all, fields(?room_id))]
|
||||
pub async fn for_room(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
events: &[Raw<AnyRoomAccountDataEvent>],
|
||||
state_store: &BaseStateStore,
|
||||
) {
|
||||
// Handle new events.
|
||||
for raw_event in events {
|
||||
match raw_event.deserialize() {
|
||||
Ok(event) => {
|
||||
context.state_changes.add_room_account_data(
|
||||
room_id,
|
||||
event.clone(),
|
||||
raw_event.clone(),
|
||||
);
|
||||
|
||||
match event {
|
||||
AnyRoomAccountDataEvent::MarkedUnread(event) => {
|
||||
on_room_info(
|
||||
room_id,
|
||||
&mut context.state_changes,
|
||||
state_store,
|
||||
|room_info| {
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
AnyRoomAccountDataEvent::UnstableMarkedUnread(event) => {
|
||||
on_room_info(
|
||||
room_id,
|
||||
&mut context.state_changes,
|
||||
state_store,
|
||||
|room_info| {
|
||||
on_unread_marker(
|
||||
room_id,
|
||||
&event.content.0,
|
||||
room_info,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
AnyRoomAccountDataEvent::Tag(event) => {
|
||||
on_room_info(
|
||||
room_id,
|
||||
&mut context.state_changes,
|
||||
state_store,
|
||||
|room_info| {
|
||||
room_info.base_info.handle_notable_tags(&event.content.tags);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Nothing.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
warn!("unable to deserialize account data event: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small helper to make the code easier to read.
|
||||
//
|
||||
// It finds the appropriate `RoomInfo`, allowing the caller to modify it, and
|
||||
// save it in the correct place.
|
||||
fn on_room_info<F>(
|
||||
room_id: &RoomId,
|
||||
state_changes: &mut StateChanges,
|
||||
state_store: &BaseStateStore,
|
||||
mut on_room_info: F,
|
||||
) where
|
||||
F: FnMut(&mut RoomInfo),
|
||||
{
|
||||
// `StateChanges` has the `RoomInfo`.
|
||||
if let Some(room_info) = state_changes.room_infos.get_mut(room_id) {
|
||||
// Show time.
|
||||
on_room_info(room_info);
|
||||
}
|
||||
// The `BaseStateStore` has the `Room`, which has the `RoomInfo`.
|
||||
else if let Some(room) = state_store.room(room_id) {
|
||||
// Clone the `RoomInfo`.
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// Show time.
|
||||
on_room_info(&mut room_info);
|
||||
|
||||
// Update the `RoomInfo` via `StateChanges`.
|
||||
state_changes.add_room(room_info);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to update the unread marker for stable and unstable prefixes.
|
||||
fn on_unread_marker(
|
||||
room_id: &RoomId,
|
||||
content: &MarkedUnreadEventContent,
|
||||
room_info: &mut RoomInfo,
|
||||
room_info_notable_updates: &mut RoomInfoNotableUpdates,
|
||||
) {
|
||||
if room_info.base_info.is_marked_unread != content.unread {
|
||||
// Notify the room list about a manual read marker change if the
|
||||
// value's changed.
|
||||
room_info_notable_updates
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(RoomInfoNotableUpdateReasons::UNREAD_MARKER);
|
||||
}
|
||||
|
||||
room_info.base_info.is_marked_unread = content.unread;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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 eyeball::SharedObservable;
|
||||
use ruma::{
|
||||
events::{ignored_user_list::IgnoredUserListEvent, GlobalAccountDataEventType},
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
use super::Context;
|
||||
use crate::{
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
Result,
|
||||
};
|
||||
|
||||
/// Save the [`StateChanges`] from the [`Context`] inside the
|
||||
/// [`BaseStateStore`], and apply them on the in-memory rooms.
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save_and_apply(
|
||||
context: Context,
|
||||
state_store: &BaseStateStore,
|
||||
ignore_user_list_changes: &SharedObservable<Vec<String>>,
|
||||
sync_token: Option<String>,
|
||||
) -> Result<()> {
|
||||
trace!("ready to submit changes to store");
|
||||
|
||||
let previous_ignored_user_list =
|
||||
state_store.get_account_data_event_static().await.ok().flatten();
|
||||
|
||||
state_store.save_changes(&context.state_changes).await?;
|
||||
|
||||
if let Some(sync_token) = sync_token {
|
||||
*state_store.sync_token.write().await = Some(sync_token);
|
||||
}
|
||||
apply_changes(context, state_store, ignore_user_list_changes, previous_ignored_user_list);
|
||||
|
||||
trace!("applied changes");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_changes(
|
||||
context: Context,
|
||||
state_store: &BaseStateStore,
|
||||
ignore_user_list_changes: &SharedObservable<Vec<String>>,
|
||||
previous_ignored_user_list: Option<Raw<IgnoredUserListEvent>>,
|
||||
) {
|
||||
let (state_changes, room_info_notable_updates) = context.into_parts();
|
||||
|
||||
if let Some(event) =
|
||||
state_changes.account_data.get(&GlobalAccountDataEventType::IgnoredUserList)
|
||||
{
|
||||
match event.deserialize_as::<IgnoredUserListEvent>() {
|
||||
Ok(event) => {
|
||||
let user_ids: Vec<String> =
|
||||
event.content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
// Try to only trigger the observable if the ignored user list has changed,
|
||||
// from the previous time we've seen it. If we couldn't load the previous event
|
||||
// for any reason, always trigger.
|
||||
if let Some(prev_user_ids) =
|
||||
previous_ignored_user_list.and_then(|raw| raw.deserialize().ok()).map(|event| {
|
||||
event
|
||||
.content
|
||||
.ignored_users
|
||||
.keys()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
{
|
||||
if user_ids != prev_user_ids {
|
||||
ignore_user_list_changes.set(user_ids);
|
||||
}
|
||||
} else {
|
||||
ignore_user_list_changes.set(user_ids);
|
||||
}
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
error!("Failed to deserialize ignored user list event: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &state_changes.room_infos {
|
||||
if let Some(room) = state_store.room(room_id) {
|
||||
let room_info_notable_update_reasons =
|
||||
room_info_notable_updates.get(room_id).copied().unwrap_or_default();
|
||||
|
||||
room.set_room_info(room_info.clone(), room_info_notable_update_reasons)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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::{
|
||||
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
|
||||
};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::super::{verification, Context};
|
||||
use crate::Result;
|
||||
|
||||
/// 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`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
pub async fn sync_timeline_event(
|
||||
context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
room_id: &RoomId,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Result<Option<TimelineEvent>> {
|
||||
let Some(olm) = olm_machine else { return Ok(None) };
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: decryption_trust_requirement };
|
||||
|
||||
Ok(Some(
|
||||
match olm.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings).await? {
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let timeline_event = TimelineEvent::from(decrypted);
|
||||
|
||||
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
|
||||
verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
verification_is_allowed,
|
||||
olm_machine,
|
||||
room_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
timeline_event
|
||||
}
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod decrypt;
|
||||
pub mod to_device;
|
||||
pub mod tracked_users;
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use matrix_sdk_crypto::{store::RoomKeyInfo, EncryptionSyncChanges, OlmMachine};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5, DeviceLists},
|
||||
events::AnyToDeviceEvent,
|
||||
serde::Raw,
|
||||
OneTimeKeyAlgorithm, UInt,
|
||||
};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::Result;
|
||||
|
||||
/// Process the to-device events and other related e2ee data based on a response
|
||||
/// from a [MSC4186 request][`v5`].
|
||||
///
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
pub async fn from_msc4186(
|
||||
context: &mut Context,
|
||||
to_device: Option<&v5::response::ToDevice>,
|
||||
e2ee: &v5::response::E2EE,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
context,
|
||||
olm_machine,
|
||||
to_device.as_ref().map(|to_device| to_device.events.clone()).unwrap_or_default(),
|
||||
&e2ee.device_lists,
|
||||
&e2ee.device_one_time_keys_count,
|
||||
e2ee.device_unused_fallback_key_types.as_deref(),
|
||||
to_device.as_ref().map(|to_device| to_device.next_batch.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Process the to-device events and other related e2ee data based on a response
|
||||
/// from a [`/v3/sync` request][`v3`].
|
||||
///
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
pub async fn from_sync_v2(
|
||||
context: &mut Context,
|
||||
response: &v3::Response,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
) -> Result<Output> {
|
||||
process(
|
||||
context,
|
||||
olm_machine,
|
||||
response.to_device.events.clone(),
|
||||
&response.device_lists,
|
||||
&response.device_one_time_keys_count,
|
||||
response.device_unused_fallback_key_types.as_deref(),
|
||||
Some(response.next_batch.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Process the to-device events and other related e2ee data.
|
||||
///
|
||||
/// This returns a list of all the to-device events that were passed in but
|
||||
/// encrypted ones were replaced with their decrypted version.
|
||||
async fn process(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
device_lists: &DeviceLists,
|
||||
one_time_keys_counts: &BTreeMap<OneTimeKeyAlgorithm, UInt>,
|
||||
unused_fallback_keys: Option<&[OneTimeKeyAlgorithm]>,
|
||||
next_batch_token: Option<String>,
|
||||
) -> Result<Output> {
|
||||
let encryption_sync_changes = EncryptionSyncChanges {
|
||||
to_device_events,
|
||||
changed_devices: device_lists,
|
||||
one_time_keys_counts,
|
||||
unused_fallback_keys,
|
||||
next_batch_token,
|
||||
};
|
||||
|
||||
Ok(if let Some(olm_machine) = olm_machine {
|
||||
// Let the crypto machine handle the sync response, this
|
||||
// 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) =
|
||||
olm_machine.receive_sync_changes(encryption_sync_changes).await?;
|
||||
|
||||
Output { decrypted_to_device_events: events, room_key_updates: Some(room_key_updates) }
|
||||
} else {
|
||||
// If we have no `OlmMachine`, just return the events that were passed in.
|
||||
// This should not happen unless we forget to set things up by calling
|
||||
// `Self::activate()`.
|
||||
Output {
|
||||
decrypted_to_device_events: encryption_sync_changes.to_device_events,
|
||||
room_key_updates: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Output {
|
||||
pub decrypted_to_device_events: Vec<Raw<AnyToDeviceEvent>>,
|
||||
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use matrix_sdk_crypto::OlmMachine;
|
||||
use ruma::{OwnedUserId, RoomId};
|
||||
|
||||
use super::super::Context;
|
||||
use crate::{store::BaseStateStore, EncryptionState, Result, RoomMemberships};
|
||||
|
||||
/// Update tracked users, if the room is encrypted.
|
||||
pub async fn update(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
room_encryption_state: EncryptionState,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
) -> Result<()> {
|
||||
if room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update tracked users, if the room is encrypted, or if the room has become
|
||||
/// encrypted.
|
||||
pub async fn update_or_set_if_room_is_newly_encrypted(
|
||||
_context: &mut Context,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
user_ids_to_track: &BTreeSet<OwnedUserId>,
|
||||
new_room_encryption_state: EncryptionState,
|
||||
previous_room_encryption_state: EncryptionState,
|
||||
room_id: &RoomId,
|
||||
state_store: &BaseStateStore,
|
||||
) -> Result<()> {
|
||||
if new_room_encryption_state.is_encrypted() {
|
||||
if let Some(olm) = olm_machine {
|
||||
if !previous_room_encryption_state.is_encrypted() {
|
||||
// The room turned on encryption in this sync, we need
|
||||
// to also get all the existing users and mark them for
|
||||
// tracking.
|
||||
let user_ids = state_store.get_user_ids(room_id, RoomMemberships::ACTIVE).await?;
|
||||
olm.update_tracked_users(user_ids.iter().map(AsRef::as_ref)).await?
|
||||
}
|
||||
|
||||
if !user_ids_to_track.is_empty() {
|
||||
olm.update_tracked_users(user_ids_to_track.iter().map(AsRef::as_ref)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// 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::{
|
||||
DecryptionSettings, OlmMachine, RoomEventDecryptionResult, TrustRequirement,
|
||||
};
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw, RoomId};
|
||||
|
||||
use super::{verification, Context};
|
||||
use crate::{
|
||||
latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent},
|
||||
Result, Room,
|
||||
};
|
||||
|
||||
/// 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>,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Result<()> {
|
||||
let Some(olm_machine) = olm_machine else {
|
||||
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(
|
||||
context,
|
||||
olm_machine,
|
||||
&room,
|
||||
&decryption_trust_requirement,
|
||||
verification_is_allowed,
|
||||
)
|
||||
.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(
|
||||
context: &mut Context,
|
||||
olm_machine: &OlmMachine,
|
||||
room: &Room,
|
||||
decryption_trust_requirement: &TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> 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(
|
||||
context,
|
||||
olm_machine,
|
||||
event,
|
||||
room.room_id(),
|
||||
decryption_trust_requirement,
|
||||
verification_is_allowed,
|
||||
));
|
||||
|
||||
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`.
|
||||
///
|
||||
/// Returns `Ok(None)` if encryption is not configured.
|
||||
async fn decrypt_sync_room_event(
|
||||
context: &mut Context,
|
||||
olm_machine: &OlmMachine,
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
room_id: &RoomId,
|
||||
decryption_trust_requirement: &TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Result<TimelineEvent> {
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: *decryption_trust_requirement };
|
||||
|
||||
let event = match olm_machine
|
||||
.try_decrypt_room_event(event.cast_ref(), room_id, &decryption_settings)
|
||||
.await?
|
||||
{
|
||||
RoomEventDecryptionResult::Decrypted(decrypted) => {
|
||||
let event: TimelineEvent = decrypted.into();
|
||||
|
||||
if let Ok(sync_timeline_event) = event.raw().deserialize() {
|
||||
verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
verification_is_allowed,
|
||||
Some(olm_machine),
|
||||
room_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
|
||||
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
|
||||
TimelineEvent::new_utd_event(event.clone(), utd_info)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::{
|
||||
async_test, event_factory::EventFactory, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
};
|
||||
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
|
||||
|
||||
use super::{decrypt_from_rooms, Context};
|
||||
use crate::{
|
||||
rooms::normal::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client,
|
||||
StateChanges,
|
||||
};
|
||||
|
||||
#[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::new(StateChanges::default(), Default::default());
|
||||
|
||||
decrypt_from_rooms(
|
||||
&mut context,
|
||||
vec![room.clone()],
|
||||
client.olm_machine().await.as_ref(),
|
||||
client.decryption_trust_requirement,
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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.
|
||||
|
||||
pub mod account_data;
|
||||
pub mod changes;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod e2ee;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod latest_event;
|
||||
pub mod profiles;
|
||||
pub mod state_events;
|
||||
pub mod timeline;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub mod verification;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::OwnedRoomId;
|
||||
|
||||
use crate::{RoomInfoNotableUpdateReasons, StateChanges};
|
||||
|
||||
type RoomInfoNotableUpdates = BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>;
|
||||
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub(crate) struct Context {
|
||||
pub(super) state_changes: StateChanges,
|
||||
pub(super) room_info_notable_updates: RoomInfoNotableUpdates,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(
|
||||
state_changes: StateChanges,
|
||||
room_info_notable_updates: RoomInfoNotableUpdates,
|
||||
) -> Self {
|
||||
Self { state_changes, room_info_notable_updates }
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (StateChanges, RoomInfoNotableUpdates) {
|
||||
let Self { state_changes, room_info_notable_updates } = self;
|
||||
|
||||
(state_changes, room_info_notable_updates)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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 ruma::{
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
SyncStateEvent,
|
||||
},
|
||||
RoomId,
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
|
||||
/// Decide whether the profile must be created, updated or deleted based on the
|
||||
/// [`RoomMemberEventContent`].
|
||||
pub fn upsert_or_delete(
|
||||
context: &mut Context,
|
||||
room_id: &RoomId,
|
||||
event: &SyncStateEvent<RoomMemberEventContent>,
|
||||
) {
|
||||
// Senders can fake the profile easily so we keep track of profiles that the
|
||||
// member set themselves to avoid having confusing profile changes when a
|
||||
// member gets kicked/banned.
|
||||
if event.state_key() == event.sender() {
|
||||
context
|
||||
.state_changes
|
||||
.profiles
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(event.sender().to_owned(), event.into());
|
||||
}
|
||||
|
||||
if *event.membership() == MembershipState::Invite {
|
||||
// Remove any profile previously stored for the invited user.
|
||||
//
|
||||
// A room member could have joined the room and left it later; in that case, the
|
||||
// server may return a dummy, empty profile along the `leave` event. We
|
||||
// don't want to reuse that empty profile when the member has been
|
||||
// re-invited, so we remove it from the database.
|
||||
context
|
||||
.state_changes
|
||||
.profiles_to_delete
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(event.state_key().clone());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
iter,
|
||||
};
|
||||
|
||||
use ruma::{
|
||||
events::{room::member::MembershipState, AnySyncStateEvent},
|
||||
serde::Raw,
|
||||
OwnedUserId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::{instrument, warn};
|
||||
|
||||
use super::{profiles, Context};
|
||||
use crate::{
|
||||
store::{ambiguity_map::AmbiguityCache, Result as StoreResult},
|
||||
RoomInfo,
|
||||
};
|
||||
|
||||
/// Collect [`AnySyncStateEvent`].
|
||||
pub mod sync {
|
||||
use ruma::events::AnySyncTimelineEvent;
|
||||
|
||||
use super::{AnySyncStateEvent, Context, Raw};
|
||||
|
||||
/// Collect [`AnySyncStateEvent`] to [`AnySyncStateEvent`].
|
||||
pub fn collect(
|
||||
_context: &mut Context,
|
||||
raw_events: &[Raw<AnySyncStateEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
super::collect(raw_events)
|
||||
}
|
||||
|
||||
/// Collect [`AnySyncTimelineEvent`] to [`AnySyncStateEvent`].
|
||||
///
|
||||
/// A [`AnySyncTimelineEvent`] can represent either message-like events or
|
||||
/// state events. The message-like events are filtered out.
|
||||
pub fn collect_from_timeline(
|
||||
_context: &mut Context,
|
||||
raw_events: &[Raw<AnySyncTimelineEvent>],
|
||||
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
|
||||
super::collect(raw_events.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()),
|
||||
_ => None,
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect [`AnyStrippedStateEvent`].
|
||||
pub mod stripped {
|
||||
use ruma::events::AnyStrippedStateEvent;
|
||||
|
||||
use super::{Context, Raw};
|
||||
|
||||
/// Collect [`AnyStrippedStateEvent`] to [`AnyStrippedStateEvent`].
|
||||
pub fn collect(
|
||||
_context: &mut Context,
|
||||
raw_events: &[Raw<AnyStrippedStateEvent>],
|
||||
) -> (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>) {
|
||||
super::collect(raw_events)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
|
||||
where
|
||||
I: IntoIterator<Item = &'a Raw<T>>,
|
||||
T: Deserialize<'a> + 'a,
|
||||
{
|
||||
raw_events
|
||||
.into_iter()
|
||||
.filter_map(|raw_event| match raw_event.deserialize() {
|
||||
Ok(event) => Some((raw_event.clone(), event)),
|
||||
Err(e) => {
|
||||
warn!("Couldn't deserialize stripped state event: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.unzip()
|
||||
}
|
||||
|
||||
/// Dispatch the state events and return the new users for this room.
|
||||
///
|
||||
/// `raw_events` and `events` must be generated from [`collect_sync`]. Events
|
||||
/// must be exactly the same list of events that are in raw_events, but
|
||||
/// deserialised. We demand them here to avoid deserialising multiple times.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub async fn dispatch_and_get_new_users(
|
||||
context: &mut Context,
|
||||
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
|
||||
room_info: &mut RoomInfo,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
) -> StoreResult<BTreeSet<OwnedUserId>> {
|
||||
let mut user_ids = BTreeSet::new();
|
||||
|
||||
if raw_events.is_empty() {
|
||||
return Ok(user_ids);
|
||||
}
|
||||
|
||||
let mut state_events = BTreeMap::new();
|
||||
|
||||
for (raw_event, event) in iter::zip(raw_events, events) {
|
||||
room_info.handle_state_event(event);
|
||||
|
||||
if let AnySyncStateEvent::RoomMember(member) = event {
|
||||
ambiguity_cache
|
||||
.handle_event(&context.state_changes, &room_info.room_id, member)
|
||||
.await?;
|
||||
|
||||
match member.membership() {
|
||||
MembershipState::Join | MembershipState::Invite => {
|
||||
user_ids.insert(member.state_key().to_owned());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
profiles::upsert_or_delete(context, &room_info.room_id, member);
|
||||
}
|
||||
|
||||
state_events
|
||||
.entry(event.event_type())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_owned(), raw_event.clone());
|
||||
}
|
||||
|
||||
context.state_changes.state.insert(room_info.room_id.clone(), state_events);
|
||||
|
||||
Ok(user_ids)
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
// 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;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::SyncMessageLikeEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::power_levels::{
|
||||
RoomPowerLevelsEvent, RoomPowerLevelsEventContent, StrippedRoomPowerLevelsEvent,
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
StateEventType,
|
||||
},
|
||||
push::{Action, PushConditionRoomCtx},
|
||||
RoomVersionId, UInt, UserId,
|
||||
};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
use super::Context;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{e2ee, verification};
|
||||
use crate::{
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
store::{BaseStateStore, StateStoreExt as _},
|
||||
sync::{Notification, Timeline},
|
||||
Result, Room, RoomInfo,
|
||||
};
|
||||
|
||||
/// Process a set of sync timeline event, and create a [`Timeline`].
|
||||
///
|
||||
/// For each event:
|
||||
/// - will try to decrypt it,
|
||||
/// - will process verification,
|
||||
/// - will process redaction,
|
||||
/// - will process notification.
|
||||
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
|
||||
pub async fn build<'notification, 'e2ee>(
|
||||
context: &mut Context,
|
||||
room: &Room,
|
||||
room_info: &mut RoomInfo,
|
||||
timeline_inputs: builder::Timeline,
|
||||
notification_inputs: builder::Notification<'notification>,
|
||||
#[cfg(feature = "e2e-encryption")] e2ee: builder::E2EE<'e2ee>,
|
||||
) -> Result<Timeline> {
|
||||
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
|
||||
let mut push_context =
|
||||
get_push_room_context(context, room, room_info, notification_inputs.state_store).await?;
|
||||
let room_id = room.room_id();
|
||||
|
||||
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::new(raw_event);
|
||||
|
||||
// Do some special stuff on the `timeline_event` before collecting it.
|
||||
match timeline_event.raw().deserialize() {
|
||||
Ok(sync_timeline_event) => {
|
||||
match &sync_timeline_event {
|
||||
// State events are ignored. They must be processed separately.
|
||||
AnySyncTimelineEvent::State(_) => {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// A room redaction.
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomRedaction(
|
||||
redaction_event,
|
||||
)) => {
|
||||
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1);
|
||||
|
||||
if let Some(redacts) = redaction_event.redacts(room_version) {
|
||||
room_info
|
||||
.handle_redaction(redaction_event, timeline_event.raw().cast_ref());
|
||||
|
||||
context.state_changes.add_redaction(
|
||||
room_id,
|
||||
redacts,
|
||||
timeline_event.raw().clone().cast(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt encrypted event, or process verification event.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
AnySyncTimelineEvent::MessageLike(sync_message_like_event) => {
|
||||
match sync_message_like_event {
|
||||
AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
SyncMessageLikeEvent::Original(_),
|
||||
) => {
|
||||
if let Some(decrypted_timeline_event) =
|
||||
Box::pin(e2ee::decrypt::sync_timeline_event(
|
||||
context,
|
||||
e2ee.olm_machine,
|
||||
timeline_event.raw(),
|
||||
room_id,
|
||||
e2ee.decryption_trust_requirement,
|
||||
e2ee.verification_is_allowed,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
timeline_event = decrypted_timeline_event;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
Box::pin(verification::process_if_relevant(
|
||||
context,
|
||||
&sync_timeline_event,
|
||||
e2ee.verification_is_allowed,
|
||||
e2ee.olm_machine,
|
||||
room_id,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing particular to do.
|
||||
#[cfg(not(feature = "e2e-encryption"))]
|
||||
AnySyncTimelineEvent::MessageLike(_) => (),
|
||||
}
|
||||
|
||||
if let Some(push_context) = &mut push_context {
|
||||
update_push_room_context(context, push_context, room.own_user_id(), room_info)
|
||||
} else {
|
||||
push_context = get_push_room_context(
|
||||
context,
|
||||
room,
|
||||
room_info,
|
||||
notification_inputs.state_store,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(context) = &push_context {
|
||||
let actions =
|
||||
notification_inputs.push_rules.get_actions(timeline_event.raw(), context);
|
||||
|
||||
if actions.iter().any(Action::should_notify) {
|
||||
notification_inputs
|
||||
.notifications
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(Notification {
|
||||
actions: actions.to_owned(),
|
||||
event: RawAnySyncOrStrippedTimelineEvent::Sync(
|
||||
timeline_event.raw().clone(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
timeline_event.push_actions = Some(actions.to_owned());
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Error deserializing event: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, we have process the timeline event. We can collect it.
|
||||
timeline.events.push(timeline_event);
|
||||
}
|
||||
|
||||
Ok(timeline)
|
||||
}
|
||||
|
||||
/// Set of types used by [`build`] to reduce the number of arguments by grouping
|
||||
/// them by thematics.
|
||||
pub mod builder {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{OlmMachine, TrustRequirement};
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v3, v5},
|
||||
events::AnySyncTimelineEvent,
|
||||
push::Ruleset,
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
};
|
||||
|
||||
use crate::{store::BaseStateStore, sync};
|
||||
|
||||
pub struct Timeline {
|
||||
pub limited: bool,
|
||||
pub raw_events: Vec<Raw<AnySyncTimelineEvent>>,
|
||||
pub prev_batch: Option<String>,
|
||||
}
|
||||
|
||||
impl From<v3::Timeline> for Timeline {
|
||||
fn from(value: v3::Timeline) -> Self {
|
||||
Self { limited: value.limited, raw_events: value.events, prev_batch: value.prev_batch }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&v5::response::Room> for Timeline {
|
||||
fn from(value: &v5::response::Room) -> Self {
|
||||
Self {
|
||||
limited: value.limited,
|
||||
raw_events: value.timeline.clone(),
|
||||
prev_batch: value.prev_batch.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Notification<'a> {
|
||||
pub push_rules: &'a Ruleset,
|
||||
pub notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
pub state_store: &'a BaseStateStore,
|
||||
}
|
||||
|
||||
impl<'a> Notification<'a> {
|
||||
pub fn new(
|
||||
push_rules: &'a Ruleset,
|
||||
notifications: &'a mut BTreeMap<OwnedRoomId, Vec<sync::Notification>>,
|
||||
state_store: &'a BaseStateStore,
|
||||
) -> Self {
|
||||
Self { push_rules, notifications, state_store }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub struct E2EE<'a> {
|
||||
pub olm_machine: Option<&'a OlmMachine>,
|
||||
pub decryption_trust_requirement: TrustRequirement,
|
||||
pub verification_is_allowed: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
impl<'a> E2EE<'a> {
|
||||
pub fn new(
|
||||
olm_machine: Option<&'a OlmMachine>,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
verification_is_allowed: bool,
|
||||
) -> Self {
|
||||
Self { olm_machine, decryption_trust_requirement, verification_is_allowed }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the push context for the given room.
|
||||
///
|
||||
/// Updates the context data from `context.state_changes` or `room_info`.
|
||||
fn update_push_room_context(
|
||||
context: &mut Context,
|
||||
push_rules: &mut PushConditionRoomCtx,
|
||||
user_id: &UserId,
|
||||
room_info: &RoomInfo,
|
||||
) {
|
||||
let room_id = &*room_info.room_id;
|
||||
|
||||
push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
|
||||
|
||||
// TODO: Use if let chain once stable
|
||||
if let Some(AnySyncStateEvent::RoomMember(member)) =
|
||||
context.state_changes.state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
|
||||
})
|
||||
{
|
||||
push_rules.user_display_name = member
|
||||
.as_original()
|
||||
.and_then(|ev| ev.content.displayname.clone())
|
||||
.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
}
|
||||
|
||||
if let Some(AnySyncStateEvent::RoomPowerLevels(event)) =
|
||||
context.state_changes.state.get(room_id).and_then(|types| {
|
||||
types.get(&StateEventType::RoomPowerLevels)?.get("")?.deserialize().ok()
|
||||
})
|
||||
{
|
||||
push_rules.power_levels = Some(event.power_levels().into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the push context for the given room.
|
||||
///
|
||||
/// Tries to get the data from `changes` or the up to date `room_info`.
|
||||
/// Loads the data from the store otherwise.
|
||||
///
|
||||
/// Returns `None` if some data couldn't be found. This should only happen
|
||||
/// in brand new rooms, while we process its state.
|
||||
pub async fn get_push_room_context(
|
||||
context: &mut Context,
|
||||
room: &Room,
|
||||
room_info: &RoomInfo,
|
||||
state_store: &BaseStateStore,
|
||||
) -> Result<Option<PushConditionRoomCtx>> {
|
||||
let room_id = room.room_id();
|
||||
let user_id = room.own_user_id();
|
||||
|
||||
let member_count = room_info.active_members_count();
|
||||
|
||||
// TODO: Use if let chain once stable
|
||||
let user_display_name = if let Some(AnySyncStateEvent::RoomMember(member)) =
|
||||
context.state_changes.state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
|
||||
}) {
|
||||
member
|
||||
.as_original()
|
||||
.and_then(|ev| ev.content.displayname.clone())
|
||||
.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
} else if let Some(AnyStrippedStateEvent::RoomMember(member)) =
|
||||
context.state_changes.stripped_state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(user_id.as_str())?.deserialize().ok()
|
||||
})
|
||||
{
|
||||
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
} else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
|
||||
member.name().to_owned()
|
||||
} else {
|
||||
trace!("Couldn't get push context because of missing own member information");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let power_levels = if let Some(event) =
|
||||
context.state_changes.state.get(room_id).and_then(|types| {
|
||||
types
|
||||
.get(&StateEventType::RoomPowerLevels)?
|
||||
.get("")?
|
||||
.deserialize_as::<RoomPowerLevelsEvent>()
|
||||
.ok()
|
||||
}) {
|
||||
Some(event.power_levels().into())
|
||||
} else if let Some(event) =
|
||||
context.state_changes.stripped_state.get(room_id).and_then(|types| {
|
||||
types
|
||||
.get(&StateEventType::RoomPowerLevels)?
|
||||
.get("")?
|
||||
.deserialize_as::<StrippedRoomPowerLevelsEvent>()
|
||||
.ok()
|
||||
})
|
||||
{
|
||||
Some(event.power_levels().into())
|
||||
} else {
|
||||
state_store
|
||||
.get_state_event_static::<RoomPowerLevelsEventContent>(room_id)
|
||||
.await?
|
||||
.and_then(|e| e.deserialize().ok())
|
||||
.map(|event| event.power_levels().into())
|
||||
};
|
||||
|
||||
Ok(Some(PushConditionRoomCtx {
|
||||
user_id: user_id.to_owned(),
|
||||
room_id: room_id.to_owned(),
|
||||
member_count: UInt::new(member_count).unwrap_or(UInt::MAX),
|
||||
user_display_name,
|
||||
power_levels,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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_crypto::OlmMachine;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::message::MessageType, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
SyncMessageLikeEvent,
|
||||
},
|
||||
RoomId,
|
||||
};
|
||||
|
||||
use super::Context;
|
||||
use crate::Result;
|
||||
|
||||
/// Process the given event as a verification event if it is a candidate. The
|
||||
/// event must be decrypted.
|
||||
pub async fn process_if_relevant(
|
||||
context: &mut Context,
|
||||
event: &AnySyncTimelineEvent,
|
||||
verification_is_allowed: bool,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if let AnySyncTimelineEvent::MessageLike(event) = event {
|
||||
// That's it, we are good, the event has been decrypted successfully.
|
||||
|
||||
// However, let's run an additional action. Check if this is a verification
|
||||
// event (`m.key.verification.*`), and call `verification` accordingly.
|
||||
if match &event {
|
||||
// This is an original (i.e. non-redacted) `m.room.message` event and its
|
||||
// content is a verification request…
|
||||
AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
matches!(&original_event.content.msgtype, MessageType::VerificationRequest(_))
|
||||
}
|
||||
|
||||
// … or this is verification request event
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => true,
|
||||
|
||||
_ => false,
|
||||
} {
|
||||
verification(context, verification_is_allowed, olm_machine, event, room_id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn verification(
|
||||
_context: &mut Context,
|
||||
verification_is_allowed: bool,
|
||||
olm_machine: Option<&OlmMachine>,
|
||||
event: &AnySyncMessageLikeEvent,
|
||||
room_id: &RoomId,
|
||||
) -> Result<()> {
|
||||
if !verification_is_allowed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(olm) = olm_machine {
|
||||
olm.receive_verification_event(&event.clone().into_full_event(room_id.to_owned())).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,8 +12,8 @@ use std::{
|
||||
use bitflags::bitflags;
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{
|
||||
apply_redaction, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
|
||||
RoomMembersUpdate, RoomState, RoomStateFilter,
|
||||
apply_redaction, EncryptionState, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState, RoomStateFilter,
|
||||
};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
|
||||
@@ -272,10 +272,8 @@ pub enum RoomMembersUpdate {
|
||||
|
||||
impl Room {
|
||||
/// The size of the latest_encrypted_events RingBuffer
|
||||
// SAFETY: `new_unchecked` is safe because 10 is not zero.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize =
|
||||
unsafe { std::num::NonZeroUsize::new_unchecked(10) };
|
||||
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize = std::num::NonZeroUsize::new(10).unwrap();
|
||||
|
||||
pub(crate) fn new(
|
||||
own_user_id: &UserId,
|
||||
@@ -428,16 +426,6 @@ impl Room {
|
||||
self.inner.read().sync_info != SyncInfo::NoState
|
||||
}
|
||||
|
||||
/// Check if the room has its encryption event synced.
|
||||
///
|
||||
/// The encryption event can be missing when the room hasn't appeared in
|
||||
/// sync yet.
|
||||
///
|
||||
/// Returns true if the encryption state is synced, false otherwise.
|
||||
pub fn is_encryption_state_synced(&self) -> bool {
|
||||
self.inner.read().encryption_state_synced
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -533,9 +521,9 @@ impl Room {
|
||||
self.inner.read().base_info.dm_targets.len()
|
||||
}
|
||||
|
||||
/// Is the room encrypted.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.inner.read().is_encrypted()
|
||||
/// Get the encryption state of this room.
|
||||
pub fn encryption_state(&self) -> EncryptionState {
|
||||
self.inner.read().encryption_state()
|
||||
}
|
||||
|
||||
/// Get the `m.room.encryption` content that enabled end to end encryption
|
||||
@@ -1578,9 +1566,15 @@ impl RoomInfo {
|
||||
self.room_state
|
||||
}
|
||||
|
||||
/// Returns whether this is an encrypted room.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.base_info.encryption.is_some()
|
||||
/// Returns the encryption state of this room.
|
||||
pub fn encryption_state(&self) -> EncryptionState {
|
||||
if !self.encryption_state_synced {
|
||||
EncryptionState::Unknown
|
||||
} else if self.base_info.encryption.is_some() {
|
||||
EncryptionState::Encrypted
|
||||
} else {
|
||||
EncryptionState::NotEncrypted
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the encryption event content in this room.
|
||||
@@ -1588,22 +1582,41 @@ impl RoomInfo {
|
||||
self.base_info.encryption = event;
|
||||
}
|
||||
|
||||
/// Handle the encryption state.
|
||||
pub fn handle_encryption_state(
|
||||
&mut self,
|
||||
requested_required_states: &[(StateEventType, String)],
|
||||
) {
|
||||
if requested_required_states
|
||||
.iter()
|
||||
.any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
|
||||
{
|
||||
// The `m.room.encryption` event was requested during the sync. Whether we have
|
||||
// received a `m.room.encryption` event in return doesn't matter: we must mark
|
||||
// the encryption state as synced; if the event is present, it means the room
|
||||
// _is_ encrypted, otherwise it means the room _is not_ encrypted.
|
||||
|
||||
self.mark_encryption_state_synced();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the given state event.
|
||||
///
|
||||
/// Returns true if the event modified the info, false otherwise.
|
||||
pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
|
||||
let ret = self.base_info.handle_state_event(event);
|
||||
// Store the state event in the `BaseRoomInfo` first.
|
||||
let base_info_has_been_modified = self.base_info.handle_state_event(event);
|
||||
|
||||
// If we received an `m.room.encryption` event here, and encryption got enabled,
|
||||
// then we can be certain that we have synced the encryption state event, so
|
||||
// mark it here as synced.
|
||||
if let AnySyncStateEvent::RoomEncryption(_) = event {
|
||||
if self.is_encrypted() {
|
||||
self.mark_encryption_state_synced();
|
||||
}
|
||||
// The `m.room.encryption` event was or wasn't explicitly requested, we don't
|
||||
// know here (see `Self::handle_encryption_state`) but we got one in
|
||||
// return! In this case, we can deduce the room _is_ encrypted, but we cannot
|
||||
// know if it _is not_ encrypted.
|
||||
|
||||
self.mark_encryption_state_synced();
|
||||
}
|
||||
|
||||
ret
|
||||
base_info_has_been_modified
|
||||
}
|
||||
|
||||
/// Handle the given stripped state event.
|
||||
@@ -2153,6 +2166,33 @@ fn compute_display_name_from_heroes(
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of a room encryption.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
|
||||
pub enum EncryptionState {
|
||||
/// The room is encrypted.
|
||||
Encrypted,
|
||||
|
||||
/// The room is not encrypted.
|
||||
NotEncrypted,
|
||||
|
||||
/// The state of the room encryption is unknown, probably because the
|
||||
/// `/sync` did not provide all data needed to decide.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl EncryptionState {
|
||||
/// Check whether `EncryptionState` is [`Encrypted`][Self::Encrypted].
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
matches!(self, Self::Encrypted)
|
||||
}
|
||||
|
||||
/// Check whether `EncryptionState` is [`Unknown`][Self::Unknown].
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
matches!(self, Self::Unknown)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
@@ -2163,6 +2203,7 @@ mod tests {
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assign::assign;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_test::{
|
||||
@@ -2199,11 +2240,17 @@ mod tests {
|
||||
use similar_asserts::assert_eq;
|
||||
use stream_assert::{assert_pending, assert_ready};
|
||||
|
||||
use super::{compute_display_name_from_heroes, Room, RoomHero, RoomInfo, RoomState, SyncInfo};
|
||||
use super::{
|
||||
compute_display_name_from_heroes, EncryptionState, Room, RoomHero, RoomInfo, RoomState,
|
||||
SyncInfo,
|
||||
};
|
||||
use crate::{
|
||||
latest_event::LatestEvent,
|
||||
response_processors as processors,
|
||||
rooms::RoomNotableTags,
|
||||
store::{IntoStateStore, MemoryStore, StateChanges, StateStore, StoreConfig},
|
||||
store::{
|
||||
IntoStateStore, MemoryStore, RoomLoadSettings, StateChanges, StateStore, StoreConfig,
|
||||
},
|
||||
test_utils::logged_in_base_client,
|
||||
BaseClient, MinimalStateEvent, OriginalMinimalStateEvent, RoomDisplayName,
|
||||
RoomInfoNotableUpdateReasons, RoomStateFilter, SessionMeta,
|
||||
@@ -2490,16 +2537,16 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_is_favourite() {
|
||||
// Given a room,
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
|
||||
client
|
||||
.set_session_meta(
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
None,
|
||||
)
|
||||
@@ -2532,11 +2579,19 @@ mod tests {
|
||||
.cast();
|
||||
|
||||
// When the new tag is handled and applied.
|
||||
let mut changes = StateChanges::default();
|
||||
client
|
||||
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
|
||||
let mut context = processors::Context::new(StateChanges::default(), Default::default());
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
client.apply_changes(&changes, Default::default());
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context.clone(),
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
@@ -2554,10 +2609,18 @@ mod tests {
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
client
|
||||
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
client.apply_changes(&changes, Default::default());
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
@@ -2570,16 +2633,16 @@ mod tests {
|
||||
#[async_test]
|
||||
async fn test_is_low_priority() {
|
||||
// Given a room,
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
|
||||
client
|
||||
.set_session_meta(
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
None,
|
||||
)
|
||||
@@ -2612,11 +2675,19 @@ mod tests {
|
||||
.cast();
|
||||
|
||||
// When the new tag is handled and applied.
|
||||
let mut changes = StateChanges::default();
|
||||
client
|
||||
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
|
||||
let mut context = processors::Context::new(StateChanges::default(), Default::default());
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
client.apply_changes(&changes, Default::default());
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context.clone(),
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
@@ -2634,10 +2705,18 @@ mod tests {
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
client
|
||||
.handle_room_account_data(room_id, &[tag_raw], &mut changes, &mut Default::default())
|
||||
|
||||
processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
|
||||
.await;
|
||||
client.apply_changes(&changes, Default::default());
|
||||
|
||||
processors::changes::save_and_apply(
|
||||
context,
|
||||
&client.state_store,
|
||||
&client.ignore_user_list_changes,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
@@ -3148,23 +3227,21 @@ mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::{RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons};
|
||||
|
||||
// Given a room,
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
|
||||
client
|
||||
.set_session_meta(
|
||||
.activate(
|
||||
SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
},
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -3184,22 +3261,29 @@ mod tests {
|
||||
// And 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();
|
||||
let mut context = processors::Context::new(StateChanges::default(), Default::default());
|
||||
room.on_latest_event_decrypted(
|
||||
event.clone(),
|
||||
0,
|
||||
&mut changes,
|
||||
&mut room_info_notable_updates,
|
||||
&mut context.state_changes,
|
||||
&mut context.room_info_notable_updates,
|
||||
);
|
||||
|
||||
assert!(room_info_notable_updates.contains_key(room_id));
|
||||
assert!(context.room_info_notable_updates.contains_key(room_id));
|
||||
|
||||
// The subscriber isn't notified at this point.
|
||||
assert!(room_info_notable_update.try_recv().is_err());
|
||||
|
||||
// Then updating the room info will store the event,
|
||||
client.apply_changes(&changes, room_info_notable_updates);
|
||||
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.
|
||||
@@ -3569,11 +3653,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_is_set_when_encryption_event_is_received() {
|
||||
fn test_encryption_is_set_when_encryption_event_is_received_encrypted() {
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
assert!(room.is_encryption_state_synced().not());
|
||||
assert!(room.is_encrypted().not());
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
|
||||
|
||||
let encryption_content =
|
||||
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
@@ -3591,8 +3674,32 @@ mod tests {
|
||||
));
|
||||
receive_state_events(&room, vec![&encryption_event]);
|
||||
|
||||
assert!(room.is_encryption_state_synced());
|
||||
assert!(room.is_encrypted());
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_is_set_when_encryption_event_is_received_not_encrypted() {
|
||||
let (_store, room) = make_room_test_helper(RoomState::Joined);
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
|
||||
room.inner.update_if(|info| {
|
||||
info.mark_encryption_state_synced();
|
||||
|
||||
false
|
||||
});
|
||||
|
||||
assert_matches!(room.encryption_state(), EncryptionState::NotEncrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encryption_state() {
|
||||
assert!(EncryptionState::Unknown.is_unknown());
|
||||
assert!(EncryptionState::Encrypted.is_unknown().not());
|
||||
assert!(EncryptionState::NotEncrypted.is_unknown().not());
|
||||
|
||||
assert!(EncryptionState::Unknown.is_encrypted().not());
|
||||
assert!(EncryptionState::Encrypted.is_encrypted());
|
||||
assert!(EncryptionState::NotEncrypted.is_encrypted().not());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
|
||||
+496
-353
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
// 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.
|
||||
|
||||
//! HTTP types for MSC4186 or MSC3585.
|
||||
//!
|
||||
//! This module provides unified namings for types from MSC3575 and
|
||||
//! MSC4186.
|
||||
|
||||
/// HTTP types from MSC3575, renamed to match the MSC4186 namings.
|
||||
pub mod msc3575 {
|
||||
use ruma::api::client::sync::sync_events::v4;
|
||||
pub use v4::{Request, Response};
|
||||
|
||||
/// HTTP types related to a `Request`.
|
||||
pub mod request {
|
||||
pub use super::v4::{
|
||||
AccountDataConfig as AccountData, ExtensionsConfig as Extensions,
|
||||
ReceiptsConfig as Receipts, RoomDetailsConfig as RoomDetails, RoomSubscription,
|
||||
SyncRequestList as List, SyncRequestListFilters as ListFilters,
|
||||
ToDeviceConfig as ToDevice, TypingConfig as Typing,
|
||||
};
|
||||
}
|
||||
|
||||
/// HTTP types related to a `Response`.
|
||||
pub mod response {
|
||||
pub use super::v4::{
|
||||
AccountData, Extensions, Receipts, SlidingSyncRoom as Room,
|
||||
SlidingSyncRoomHero as RoomHero, SyncList as List, ToDevice, Typing,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP types from MSC4186.
|
||||
pub mod msc4186 {
|
||||
pub use ruma::api::client::sync::sync_events::v5::*;
|
||||
}
|
||||
|
||||
pub use msc4186::*;
|
||||
@@ -86,7 +86,7 @@ pub(crate) struct AmbiguityCache {
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
#[instrument(ret)]
|
||||
#[instrument(ret(level = "trace"))]
|
||||
pub(crate) fn is_display_name_ambiguous(
|
||||
display_name: &DisplayName,
|
||||
users_with_display_name: &BTreeSet<OwnedUserId>,
|
||||
|
||||
@@ -6,7 +6,7 @@ use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloomBuilder;
|
||||
use matrix_sdk_test::test_json;
|
||||
use matrix_sdk_test::{event_factory::EventFactory, test_json};
|
||||
use ruma::{
|
||||
api::MatrixVersion,
|
||||
event_id,
|
||||
@@ -22,10 +22,9 @@ use ruma::{
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
AnyEphemeralRoomEventContent, AnyGlobalAccountDataEvent, AnyMessageLikeEventContent,
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent,
|
||||
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
|
||||
SyncStateEvent,
|
||||
AnyGlobalAccountDataEvent, AnyMessageLikeEventContent, AnyRoomAccountDataEvent,
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType,
|
||||
RoomAccountDataEventType, StateEventType, SyncStateEvent,
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, room_id,
|
||||
serde::Raw,
|
||||
@@ -36,7 +35,7 @@ use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
use super::{
|
||||
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
|
||||
ServerCapabilities,
|
||||
RoomLoadSettings, ServerCapabilities,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
@@ -47,7 +46,7 @@ use crate::{
|
||||
/// `StateStore` integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`statestore_integration_tests!`] macro.
|
||||
/// `statestore_integration_tests!` macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait StateStoreIntegrationTests {
|
||||
@@ -95,6 +94,8 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_update_send_queue_dependent(&self);
|
||||
/// Test saving/restoring server capabilities.
|
||||
async fn test_server_capabilities_saving(&self);
|
||||
/// Test fetching room infos based on [`RoomLoadSettings`].
|
||||
async fn test_get_room_infos(&self);
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -174,13 +175,11 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let invited_member_state_event = invited_member_state_raw.deserialize().unwrap();
|
||||
changes.add_state_event(room_id, invited_member_state_event, invited_member_state_raw);
|
||||
|
||||
let receipt_json: &JsonValue = &test_json::READ_RECEIPT;
|
||||
let receipt_event =
|
||||
serde_json::from_value::<AnySyncEphemeralRoomEvent>(receipt_json.clone()).unwrap();
|
||||
let receipt_content = match receipt_event.content() {
|
||||
AnyEphemeralRoomEventContent::Receipt(content) => content,
|
||||
_ => panic!(),
|
||||
};
|
||||
let f = EventFactory::new().room(room_id);
|
||||
let receipt_content = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$example"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.into_content();
|
||||
changes.add_receipts(room_id, receipt_content);
|
||||
|
||||
changes.ambiguity_maps.insert(room_id.to_owned(), room_ambiguity_map);
|
||||
@@ -268,7 +267,11 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
assert!(self.get_kv_data(StateStoreDataKey::SyncToken).await?.is_some());
|
||||
assert!(self.get_presence_event(user_id).await?.is_some());
|
||||
assert_eq!(self.get_room_infos().await?.len(), 2, "Expected to find 2 room infos");
|
||||
assert_eq!(
|
||||
self.get_room_infos(&RoomLoadSettings::default()).await?.len(),
|
||||
2,
|
||||
"Expected to find 2 room infos"
|
||||
);
|
||||
assert!(self
|
||||
.get_account_data_event(GlobalAccountDataEventType::PushRules)
|
||||
.await?
|
||||
@@ -930,7 +933,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let user_id = user_id();
|
||||
|
||||
assert!(self.get_member_event(room_id, user_id).await.unwrap().is_none());
|
||||
assert_eq!(self.get_room_infos().await.unwrap().len(), 0);
|
||||
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 0);
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes
|
||||
@@ -946,7 +949,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let member_event =
|
||||
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
|
||||
assert!(matches!(member_event, MemberEvent::Sync(_)));
|
||||
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
|
||||
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 1);
|
||||
|
||||
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
|
||||
assert_eq!(members, vec![user_id.to_owned()]);
|
||||
@@ -959,7 +962,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let member_event =
|
||||
self.get_member_event(room_id, user_id).await.unwrap().unwrap().deserialize().unwrap();
|
||||
assert!(matches!(member_event, MemberEvent::Stripped(_)));
|
||||
assert_eq!(self.get_room_infos().await.unwrap().len(), 1);
|
||||
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 1);
|
||||
|
||||
let members = self.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
|
||||
assert_eq!(members, vec![user_id.to_owned()]);
|
||||
@@ -1003,7 +1006,11 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
self.remove_room(room_id).await?;
|
||||
|
||||
assert_eq!(self.get_room_infos().await?.len(), 1, "room is still there");
|
||||
assert_eq!(
|
||||
self.get_room_infos(&RoomLoadSettings::default()).await?.len(),
|
||||
1,
|
||||
"room is still there"
|
||||
);
|
||||
|
||||
assert!(self.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none());
|
||||
assert!(
|
||||
@@ -1057,7 +1064,10 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
self.remove_room(stripped_room_id).await?;
|
||||
|
||||
assert!(self.get_room_infos().await?.is_empty(), "still room info found");
|
||||
assert!(
|
||||
self.get_room_infos(&RoomLoadSettings::default()).await?.is_empty(),
|
||||
"still room info found"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1683,6 +1693,54 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_get_room_infos(&self) {
|
||||
let room_id_0 = room_id!("!r0");
|
||||
let room_id_1 = room_id!("!r1");
|
||||
let room_id_2 = room_id!("!r2");
|
||||
|
||||
// There is no room for the moment.
|
||||
{
|
||||
assert_eq!(self.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 0);
|
||||
}
|
||||
|
||||
// Save rooms.
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
|
||||
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
|
||||
self.save_changes(&changes).await.unwrap();
|
||||
|
||||
// We can find all the rooms with `RoomLoadSettings::All`.
|
||||
{
|
||||
let mut all_rooms = self.get_room_infos(&RoomLoadSettings::All).await.unwrap();
|
||||
|
||||
// (We need to sort by `room_id` so that the test is stable across all
|
||||
// `StateStore` implementations).
|
||||
all_rooms.sort_by(|a, b| a.room_id.cmp(&b.room_id));
|
||||
|
||||
assert_eq!(all_rooms.len(), 2);
|
||||
assert_eq!(all_rooms[0].room_id, room_id_0);
|
||||
assert_eq!(all_rooms[1].room_id, room_id_1);
|
||||
}
|
||||
|
||||
// We can find a single room with `RoomLoadSettings::One`.
|
||||
{
|
||||
let all_rooms =
|
||||
self.get_room_infos(&RoomLoadSettings::One(room_id_1.to_owned())).await.unwrap();
|
||||
|
||||
assert_eq!(all_rooms.len(), 1);
|
||||
assert_eq!(all_rooms[0].room_id, room_id_1);
|
||||
}
|
||||
|
||||
// `RoomLoadSetting::One` can result in loading zero room if the room is
|
||||
// unknown.
|
||||
{
|
||||
let all_rooms =
|
||||
self.get_room_infos(&RoomLoadSettings::One(room_id_2.to_owned())).await.unwrap();
|
||||
|
||||
assert_eq!(all_rooms.len(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro building to allow your StateStore implementation to run the entire
|
||||
@@ -1847,6 +1905,12 @@ macro_rules! statestore_integration_tests {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_update_send_queue_dependent().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room_infos() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
store.test_get_room_infos().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ use super::{
|
||||
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
|
||||
traits::{ComposerDraft, ServerCapabilities},
|
||||
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
|
||||
StateChanges, StateStore, StoreError,
|
||||
RoomLoadSettings, StateChanges, StateStore, StoreError,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
|
||||
@@ -635,8 +635,18 @@ impl StateStore for MemoryStore {
|
||||
Ok(get_user_ids_inner(&inner.members, room_id, memberships))
|
||||
}
|
||||
|
||||
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>> {
|
||||
Ok(self.inner.read().unwrap().room_info.values().cloned().collect())
|
||||
async fn get_room_infos(&self, room_load_settings: &RoomLoadSettings) -> Result<Vec<RoomInfo>> {
|
||||
let memory_store_inner = self.inner.read().unwrap();
|
||||
let room_infos = &memory_store_inner.room_info;
|
||||
|
||||
Ok(match room_load_settings {
|
||||
RoomLoadSettings::All => room_infos.values().cloned().collect(),
|
||||
|
||||
RoomLoadSettings::One(room_id) => match room_infos.get(room_id) {
|
||||
Some(room_info) => vec![room_info.clone()],
|
||||
None => vec![],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_users_with_display_name(
|
||||
|
||||
@@ -146,9 +146,10 @@ pub type Result<T, E = StoreError> = std::result::Result<T, E>;
|
||||
/// This adds additional higher level store functionality on top of a
|
||||
/// `StateStore` implementation.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Store {
|
||||
pub(crate) struct BaseStateStore {
|
||||
pub(super) inner: Arc<DynStateStore>,
|
||||
session_meta: Arc<OnceCell<SessionMeta>>,
|
||||
room_load_settings: Arc<RwLock<RoomLoadSettings>>,
|
||||
/// The current sync token that should be used for the next sync call.
|
||||
pub(super) sync_token: Arc<RwLock<Option<String>>>,
|
||||
/// All rooms the store knows about.
|
||||
@@ -158,12 +159,13 @@ pub(crate) struct Store {
|
||||
sync_lock: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
impl BaseStateStore {
|
||||
/// Create a new store, wrapping the given `StateStore`
|
||||
pub fn new(inner: Arc<DynStateStore>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
session_meta: Default::default(),
|
||||
room_load_settings: Default::default(),
|
||||
sync_token: Default::default(),
|
||||
rooms: Arc::new(StdRwLock::new(ObservableMap::new())),
|
||||
sync_lock: Default::default(),
|
||||
@@ -175,11 +177,51 @@ impl Store {
|
||||
&self.sync_lock
|
||||
}
|
||||
|
||||
/// Load the room infos from the inner `StateStore`.
|
||||
/// Set the [`SessionMeta`] into [`BaseStateStore::session_meta`].
|
||||
///
|
||||
/// Applies migrations to the room infos if needed.
|
||||
async fn load_room_infos(&self) -> Result<Vec<RoomInfo>> {
|
||||
let mut room_infos = self.inner.get_room_infos().await?;
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if called twice.
|
||||
pub(crate) fn set_session_meta(&self, session_meta: SessionMeta) {
|
||||
self.session_meta.set(session_meta).expect("`SessionMeta` was already set");
|
||||
}
|
||||
|
||||
/// Loads rooms from the given [`DynStateStore`] (in
|
||||
/// [`BaseStateStore::new`]) into [`BaseStateStore::rooms`].
|
||||
pub(crate) async fn load_rooms(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
room_load_settings: RoomLoadSettings,
|
||||
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Result<()> {
|
||||
*self.room_load_settings.write().await = room_load_settings.clone();
|
||||
|
||||
let room_infos = self.load_and_migrate_room_infos(room_load_settings).await?;
|
||||
|
||||
let mut rooms = self.rooms.write().unwrap();
|
||||
|
||||
for room_info in room_infos {
|
||||
let new_room = Room::restore(
|
||||
user_id,
|
||||
self.inner.clone(),
|
||||
room_info,
|
||||
room_info_notable_update_sender.clone(),
|
||||
);
|
||||
let new_room_id = new_room.room_id().to_owned();
|
||||
|
||||
rooms.insert(new_room_id, new_room);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load room infos from the [`StateStore`] and applies migrations onto
|
||||
/// them.
|
||||
async fn load_and_migrate_room_infos(
|
||||
&self,
|
||||
room_load_settings: RoomLoadSettings,
|
||||
) -> Result<Vec<RoomInfo>> {
|
||||
let mut room_infos = self.inner.get_room_infos(&room_load_settings).await?;
|
||||
let mut migrated_room_infos = Vec::with_capacity(room_infos.len());
|
||||
|
||||
for room_info in room_infos.iter_mut() {
|
||||
@@ -205,40 +247,34 @@ impl Store {
|
||||
Ok(room_infos)
|
||||
}
|
||||
|
||||
/// Set the meta of the session.
|
||||
///
|
||||
/// Restores the state of this `Store` from the given `SessionMeta` and the
|
||||
/// inner `StateStore`.
|
||||
///
|
||||
/// This method panics if it is called twice.
|
||||
pub async fn set_session_meta(
|
||||
&self,
|
||||
session_meta: SessionMeta,
|
||||
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Result<()> {
|
||||
{
|
||||
let room_infos = self.load_room_infos().await?;
|
||||
|
||||
let mut rooms = self.rooms.write().unwrap();
|
||||
|
||||
for room_info in room_infos {
|
||||
let new_room = Room::restore(
|
||||
&session_meta.user_id,
|
||||
self.inner.clone(),
|
||||
room_info,
|
||||
room_info_notable_update_sender.clone(),
|
||||
);
|
||||
let new_room_id = new_room.room_id().to_owned();
|
||||
|
||||
rooms.insert(new_room_id, new_room);
|
||||
}
|
||||
}
|
||||
|
||||
/// Load sync token from the [`StateStore`], and put it in
|
||||
/// [`BaseStateStore::sync_token`].
|
||||
pub(crate) async fn load_sync_token(&self) -> Result<()> {
|
||||
let token =
|
||||
self.get_kv_data(StateStoreDataKey::SyncToken).await?.and_then(|s| s.into_sync_token());
|
||||
*self.sync_token.write().await = token;
|
||||
|
||||
self.session_meta.set(session_meta).expect("Session Meta was already set");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restore the session meta, sync token and rooms from an existing
|
||||
/// [`BaseStateStore`].
|
||||
#[cfg(any(feature = "e2e-encryption", test))]
|
||||
pub(crate) async fn derive_from_other(
|
||||
&self,
|
||||
other: &Self,
|
||||
room_info_notable_update_sender: &broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Result<()> {
|
||||
let Some(session_meta) = other.session_meta.get() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let room_load_settings = other.room_load_settings.read().await.clone();
|
||||
|
||||
self.load_rooms(&session_meta.user_id, room_load_settings, room_info_notable_update_sender)
|
||||
.await?;
|
||||
self.load_sync_token().await?;
|
||||
self.set_session_meta(session_meta.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -319,7 +355,7 @@ impl Store {
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for Store {
|
||||
impl fmt::Debug for BaseStateStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Store")
|
||||
.field("inner", &self.inner)
|
||||
@@ -330,7 +366,7 @@ impl fmt::Debug for Store {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Store {
|
||||
impl Deref for BaseStateStore {
|
||||
type Target = DynStateStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -338,6 +374,57 @@ impl Deref for Store {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure how many rooms will be restored when restoring the session with
|
||||
/// `BaseStateStore::load_rooms`.
|
||||
///
|
||||
/// <div class="warning">
|
||||
///
|
||||
/// # ⚠️ Be careful!
|
||||
///
|
||||
/// When loading a single room with [`RoomLoadSettings::One`], the in-memory
|
||||
/// state may not reflect the store state (in the databases). Thus, when one
|
||||
/// will get a room that exists in the store state but _not_ in the in-memory
|
||||
/// state, it will be created from scratch and, when saved, will override the
|
||||
/// data in the store state (in the databases). This can lead to weird
|
||||
/// behaviours.
|
||||
///
|
||||
/// This option is expected to be used as follows:
|
||||
///
|
||||
/// 1. Create a `BaseStateStore` with a [`StateStore`] based on SQLite for
|
||||
/// example,
|
||||
/// 2. Restore a session and load one room from the [`StateStore`] (in the case
|
||||
/// of dealing with a notification for example),
|
||||
/// 3. Derive the `BaseStateStore`, with `BaseStateStore::derive_from_other`,
|
||||
/// into another one with an in-memory [`StateStore`], such as
|
||||
/// [`MemoryStore`],
|
||||
/// 4. Work on this derived `BaseStateStore`.
|
||||
///
|
||||
/// Now, all operations happen in the [`MemoryStore`], not on the original store
|
||||
/// (SQLite in this example), thus protecting original data.
|
||||
///
|
||||
/// From a higher-level point of view, this is what
|
||||
/// [`BaseClient::clone_with_in_memory_state_store`] does.
|
||||
///
|
||||
/// </div>
|
||||
///
|
||||
/// [`BaseClient::clone_with_in_memory_state_store`]: crate::BaseClient::clone_with_in_memory_state_store
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum RoomLoadSettings {
|
||||
/// Load all rooms from the [`StateStore`] into the in-memory state store
|
||||
/// `BaseStateStore`.
|
||||
///
|
||||
/// This is the default variant.
|
||||
#[default]
|
||||
All,
|
||||
|
||||
/// Load a single room from the [`StateStore`] into the in-memory state
|
||||
/// store `BaseStateStore`.
|
||||
///
|
||||
/// Please, be careful with this option. Read the documentation of
|
||||
/// [`RoomLoadSettings`].
|
||||
One(OwnedRoomId),
|
||||
}
|
||||
|
||||
/// Store state changes and pass them to the StateStore.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct StateChanges {
|
||||
@@ -479,7 +566,7 @@ impl StateChanges {
|
||||
///
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::store::StoreConfig;
|
||||
///
|
||||
/// #
|
||||
/// let store_config =
|
||||
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
|
||||
/// ```
|
||||
@@ -545,3 +632,188 @@ impl StoreConfig {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{owned_device_id, owned_user_id, room_id, user_id};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::{BaseStateStore, MemoryStore, RoomLoadSettings};
|
||||
use crate::{RoomInfo, RoomState, SessionMeta, StateChanges};
|
||||
|
||||
#[async_test]
|
||||
async fn test_set_session_meta() {
|
||||
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
|
||||
|
||||
let session_meta = SessionMeta {
|
||||
user_id: owned_user_id!("@mnt_io:matrix.org"),
|
||||
device_id: owned_device_id!("HELLOYOU"),
|
||||
};
|
||||
|
||||
assert!(store.session_meta.get().is_none());
|
||||
|
||||
store.set_session_meta(session_meta.clone());
|
||||
|
||||
assert_eq!(store.session_meta.get(), Some(&session_meta));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[should_panic]
|
||||
async fn test_set_session_meta_twice() {
|
||||
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
|
||||
|
||||
let session_meta = SessionMeta {
|
||||
user_id: owned_user_id!("@mnt_io:matrix.org"),
|
||||
device_id: owned_device_id!("HELLOYOU"),
|
||||
};
|
||||
|
||||
store.set_session_meta(session_meta.clone());
|
||||
// Kaboom.
|
||||
store.set_session_meta(session_meta);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_derive_from_other() {
|
||||
// The first store.
|
||||
let other = BaseStateStore::new(Arc::new(MemoryStore::new()));
|
||||
|
||||
let session_meta = SessionMeta {
|
||||
user_id: owned_user_id!("@mnt_io:matrix.org"),
|
||||
device_id: owned_device_id!("HELLOYOU"),
|
||||
};
|
||||
let (room_info_notable_update_sender, _) = broadcast::channel(1);
|
||||
let room_id_0 = room_id!("!r0");
|
||||
|
||||
other
|
||||
.load_rooms(
|
||||
&session_meta.user_id,
|
||||
RoomLoadSettings::One(room_id_0.to_owned()),
|
||||
&room_info_notable_update_sender,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
other.set_session_meta(session_meta.clone());
|
||||
|
||||
// Derive another store.
|
||||
let store = BaseStateStore::new(Arc::new(MemoryStore::new()));
|
||||
store.derive_from_other(&other, &room_info_notable_update_sender).await.unwrap();
|
||||
|
||||
// `SessionMeta` is derived.
|
||||
assert_eq!(store.session_meta.get(), Some(&session_meta));
|
||||
// `RoomLoadSettings` is derived.
|
||||
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::One(ref room_id) => {
|
||||
assert_eq!(room_id, room_id_0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_room_load_settings_default() {
|
||||
assert_matches!(RoomLoadSettings::default(), RoomLoadSettings::All);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_all_rooms() {
|
||||
let room_id_0 = room_id!("!r0");
|
||||
let room_id_1 = room_id!("!r1");
|
||||
let user_id = user_id!("@mnt_io:matrix.org");
|
||||
|
||||
let memory_state_store = Arc::new(MemoryStore::new());
|
||||
|
||||
// Initial state.
|
||||
{
|
||||
let store = BaseStateStore::new(memory_state_store.clone());
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
|
||||
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
|
||||
|
||||
store.inner.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
// Check a `BaseStateStore` is able to load all rooms.
|
||||
{
|
||||
let store = BaseStateStore::new(memory_state_store.clone());
|
||||
let (room_info_notable_update_sender, _) = broadcast::channel(2);
|
||||
|
||||
// Default value.
|
||||
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
|
||||
|
||||
// Load rooms.
|
||||
store
|
||||
.load_rooms(user_id, RoomLoadSettings::All, &room_info_notable_update_sender)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check the last room load settings.
|
||||
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
|
||||
|
||||
// Check the loaded rooms.
|
||||
let mut rooms = store.rooms();
|
||||
rooms.sort_by(|a, b| a.room_id().cmp(b.room_id()));
|
||||
|
||||
assert_eq!(rooms.len(), 2);
|
||||
|
||||
assert_eq!(rooms[0].room_id(), room_id_0);
|
||||
assert_eq!(rooms[0].own_user_id(), user_id);
|
||||
|
||||
assert_eq!(rooms[1].room_id(), room_id_1);
|
||||
assert_eq!(rooms[1].own_user_id(), user_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_load_one_room() {
|
||||
let room_id_0 = room_id!("!r0");
|
||||
let room_id_1 = room_id!("!r1");
|
||||
let user_id = user_id!("@mnt_io:matrix.org");
|
||||
|
||||
let memory_state_store = Arc::new(MemoryStore::new());
|
||||
|
||||
// Initial state.
|
||||
{
|
||||
let store = BaseStateStore::new(memory_state_store.clone());
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(room_id_0, RoomState::Joined));
|
||||
changes.add_room(RoomInfo::new(room_id_1, RoomState::Joined));
|
||||
|
||||
store.inner.save_changes(&changes).await.unwrap();
|
||||
}
|
||||
|
||||
// Check a `BaseStateStore` is able to load one room.
|
||||
{
|
||||
let store = BaseStateStore::new(memory_state_store.clone());
|
||||
let (room_info_notable_update_sender, _) = broadcast::channel(2);
|
||||
|
||||
// Default value.
|
||||
assert_matches!(*store.room_load_settings.read().await, RoomLoadSettings::All);
|
||||
|
||||
// Load rooms.
|
||||
store
|
||||
.load_rooms(
|
||||
user_id,
|
||||
RoomLoadSettings::One(room_id_1.to_owned()),
|
||||
&room_info_notable_update_sender,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check the last room load settings.
|
||||
assert_matches!(
|
||||
*store.room_load_settings.read().await,
|
||||
RoomLoadSettings::One(ref room_id) => {
|
||||
assert_eq!(room_id, room_id_1);
|
||||
}
|
||||
);
|
||||
|
||||
// Check the loaded rooms.
|
||||
let rooms = store.rooms();
|
||||
assert_eq!(rooms.len(), 1);
|
||||
|
||||
assert_eq!(rooms[0].room_id(), room_id_1);
|
||||
assert_eq!(rooms[0].own_user_id(), user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,9 @@ pub enum DependentQueuedRequestKind {
|
||||
/// the final media event with the remote MXC URIs.
|
||||
FinishUpload {
|
||||
/// Local echo for the event (containing the local MXC URIs).
|
||||
local_echo: RoomMessageEventContent,
|
||||
///
|
||||
/// `Box` the local echo so that it reduces the size of the whole enum.
|
||||
local_echo: Box<RoomMessageEventContent>,
|
||||
|
||||
/// Transaction id for the file upload.
|
||||
file_upload: OwnedTransactionId,
|
||||
|
||||
@@ -42,8 +42,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
send_queue::SentRequestKey, ChildTransactionId, DependentQueuedRequest,
|
||||
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind, StateChanges,
|
||||
StoreError,
|
||||
DependentQueuedRequestKind, QueueWedgeError, QueuedRequest, QueuedRequestKind,
|
||||
RoomLoadSettings, StateChanges, StoreError,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::{
|
||||
@@ -194,8 +194,11 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
memberships: RoomMemberships,
|
||||
) -> Result<Vec<OwnedUserId>, Self::Error>;
|
||||
|
||||
/// Get all the pure `RoomInfo`s the store knows about.
|
||||
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error>;
|
||||
/// Get a set of pure `RoomInfo`s the store knows about.
|
||||
async fn get_room_infos(
|
||||
&self,
|
||||
room_load_settings: &RoomLoadSettings,
|
||||
) -> Result<Vec<RoomInfo>, Self::Error>;
|
||||
|
||||
/// Get all the users that use the given display name in the given room.
|
||||
///
|
||||
@@ -574,8 +577,11 @@ impl<T: StateStore> StateStore for EraseStateStoreError<T> {
|
||||
self.0.get_user_ids(room_id, memberships).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>, Self::Error> {
|
||||
self.0.get_room_infos().await.map_err(Into::into)
|
||||
async fn get_room_infos(
|
||||
&self,
|
||||
room_load_settings: &RoomLoadSettings,
|
||||
) -> Result<Vec<RoomInfo>, Self::Error> {
|
||||
self.0.get_room_infos(room_load_settings).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_users_with_display_name(
|
||||
|
||||
@@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
|
||||
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
|
||||
store::Store,
|
||||
store::BaseStateStore,
|
||||
};
|
||||
|
||||
/// Generalized representation of a `/sync` response.
|
||||
@@ -85,7 +85,7 @@ impl RoomUpdates {
|
||||
/// Update the caches for the rooms that received updates.
|
||||
///
|
||||
/// This will only fill the in-memory caches, not save the info on disk.
|
||||
pub(crate) async fn update_in_memory_caches(&self, store: &Store) {
|
||||
pub(crate) async fn update_in_memory_caches(&self, store: &BaseStateStore) {
|
||||
for room in self
|
||||
.leave
|
||||
.keys()
|
||||
|
||||
@@ -18,23 +18,26 @@
|
||||
|
||||
use ruma::{owned_user_id, UserId};
|
||||
|
||||
use crate::{store::StoreConfig, BaseClient, SessionMeta};
|
||||
use crate::{
|
||||
store::{RoomLoadSettings, StoreConfig},
|
||||
BaseClient, SessionMeta,
|
||||
};
|
||||
|
||||
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
|
||||
/// one otherwise.
|
||||
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
|
||||
let client = BaseClient::with_store_config(StoreConfig::new(
|
||||
"cross-process-store-locks-holder-name".to_owned(),
|
||||
));
|
||||
let client =
|
||||
BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
|
||||
let user_id =
|
||||
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
|
||||
client
|
||||
.set_session_meta(
|
||||
.activate(
|
||||
SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
|
||||
RoomLoadSettings::default(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("set_session_meta failed!");
|
||||
.expect("`activate` failed!");
|
||||
client
|
||||
}
|
||||
|
||||
@@ -6,11 +6,20 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
### Features
|
||||
|
||||
- Add a simple TTL cache implementation. The `TtlCache` struct can be used as a
|
||||
key/value map that expires items after 15 minutes.
|
||||
([#4663](https://github.com/matrix-org/matrix-rust-sdk/pull/4663))
|
||||
|
||||
## [0.10.0] - 2025-02-04
|
||||
|
||||
- [**breaking**]: `SyncTimelineEvent` and `TimelineEvent` have been fused into a single type
|
||||
`TimelineEvent`, and its field `push_actions` has been made `Option`al (it is set to `None` when
|
||||
we couldn't compute the push actions, because we lacked some information).
|
||||
- [**breaking**]: `SyncTimelineEvent` and `TimelineEvent` have been
|
||||
fused into a single type `TimelineEvent`, and its field `push_actions`
|
||||
has been made `Option`al (it is set to `None` when we couldn't
|
||||
compute the push actions, because we lacked some information).
|
||||
([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568))
|
||||
|
||||
## [0.9.0] - 2024-12-18
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
|
||||
@@ -302,6 +302,9 @@ pub struct EncryptionInfo {
|
||||
/// Callers that persist this should mark the state as dirty when a device
|
||||
/// change is received down the sync.
|
||||
pub verification_state: VerificationState,
|
||||
/// The Megolm session ID that was used to encrypt this event, or None if
|
||||
/// this info was stored before we collected this data.
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a matrix room event that has been returned from `/sync`,
|
||||
@@ -540,6 +543,19 @@ impl TimelineEventKind {
|
||||
TimelineEventKind::PlainText { event } => event,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Megolm session ID that was used to send this event, if it was
|
||||
/// encrypted.
|
||||
pub fn session_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
TimelineEventKind::Decrypted(decrypted_room_event) => {
|
||||
decrypted_room_event.encryption_info.session_id.as_ref()
|
||||
}
|
||||
TimelineEventKind::UnableToDecrypt { utd_info, .. } => utd_info.session_id.as_ref(),
|
||||
TimelineEventKind::PlainText { .. } => None,
|
||||
}
|
||||
.map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -1042,6 +1058,7 @@ mod tests {
|
||||
sender_claimed_keys: Default::default(),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
session_id: Some("xyz".to_owned()),
|
||||
},
|
||||
unsigned_encryption_info: Some(BTreeMap::from([(
|
||||
UnsignedEventLocation::RelationsReplace,
|
||||
@@ -1080,6 +1097,7 @@ mod tests {
|
||||
}
|
||||
},
|
||||
"verification_state": "Verified",
|
||||
"session_id": "xyz",
|
||||
},
|
||||
"unsigned_encryption_info": {
|
||||
"RelationsReplace": {"UnableToDecrypt": {
|
||||
@@ -1128,6 +1146,7 @@ mod tests {
|
||||
event.encryption_info().unwrap().algorithm_info,
|
||||
AlgorithmInfo::MegolmV1AesSha2 { .. }
|
||||
);
|
||||
assert_eq!(event.encryption_info().unwrap().session_id, None);
|
||||
|
||||
// Test that the previous format, with an undecryptable unsigned event, can also
|
||||
// be deserialized.
|
||||
@@ -1283,49 +1302,57 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_verification_level() {
|
||||
assert_json_snapshot!(VerificationLevel::VerificationViolation);
|
||||
assert_json_snapshot!(VerificationLevel::UnsignedDevice);
|
||||
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::InsecureSource));
|
||||
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::MissingDevice));
|
||||
assert_json_snapshot!(VerificationLevel::UnverifiedIdentity);
|
||||
with_settings!({ prepend_module_to_snapshot => false }, {
|
||||
assert_json_snapshot!(VerificationLevel::VerificationViolation);
|
||||
assert_json_snapshot!(VerificationLevel::UnsignedDevice);
|
||||
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::InsecureSource));
|
||||
assert_json_snapshot!(VerificationLevel::None(DeviceLinkProblem::MissingDevice));
|
||||
assert_json_snapshot!(VerificationLevel::UnverifiedIdentity);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_verification_states() {
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::UnsignedDevice));
|
||||
assert_json_snapshot!(VerificationState::Unverified(
|
||||
VerificationLevel::VerificationViolation
|
||||
));
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::InsecureSource,
|
||||
)));
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::MissingDevice,
|
||||
)));
|
||||
assert_json_snapshot!(VerificationState::Verified);
|
||||
with_settings!({ prepend_module_to_snapshot => false }, {
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::UnsignedDevice));
|
||||
assert_json_snapshot!(VerificationState::Unverified(
|
||||
VerificationLevel::VerificationViolation
|
||||
));
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::InsecureSource,
|
||||
)));
|
||||
assert_json_snapshot!(VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::MissingDevice,
|
||||
)));
|
||||
assert_json_snapshot!(VerificationState::Verified);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_shield_states() {
|
||||
assert_json_snapshot!(ShieldState::None);
|
||||
assert_json_snapshot!(ShieldState::Red {
|
||||
code: ShieldStateCode::UnverifiedIdentity,
|
||||
message: "a message"
|
||||
});
|
||||
assert_json_snapshot!(ShieldState::Grey {
|
||||
code: ShieldStateCode::AuthenticityNotGuaranteed,
|
||||
message: "authenticity of this message cannot be guaranteed",
|
||||
with_settings!({ prepend_module_to_snapshot => false }, {
|
||||
assert_json_snapshot!(ShieldState::None);
|
||||
assert_json_snapshot!(ShieldState::Red {
|
||||
code: ShieldStateCode::UnverifiedIdentity,
|
||||
message: "a message"
|
||||
});
|
||||
assert_json_snapshot!(ShieldState::Grey {
|
||||
code: ShieldStateCode::AuthenticityNotGuaranteed,
|
||||
message: "authenticity of this message cannot be guaranteed",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_test_shield_codes() {
|
||||
assert_json_snapshot!(ShieldStateCode::AuthenticityNotGuaranteed);
|
||||
assert_json_snapshot!(ShieldStateCode::UnknownDevice);
|
||||
assert_json_snapshot!(ShieldStateCode::UnsignedDevice);
|
||||
assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity);
|
||||
assert_json_snapshot!(ShieldStateCode::SentInClear);
|
||||
assert_json_snapshot!(ShieldStateCode::VerificationViolation);
|
||||
with_settings!({ prepend_module_to_snapshot => false }, {
|
||||
assert_json_snapshot!(ShieldStateCode::AuthenticityNotGuaranteed);
|
||||
assert_json_snapshot!(ShieldStateCode::UnknownDevice);
|
||||
assert_json_snapshot!(ShieldStateCode::UnsignedDevice);
|
||||
assert_json_snapshot!(ShieldStateCode::UnverifiedIdentity);
|
||||
assert_json_snapshot!(ShieldStateCode::SentInClear);
|
||||
assert_json_snapshot!(ShieldStateCode::VerificationViolation);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1341,7 +1368,9 @@ mod tests {
|
||||
]),
|
||||
};
|
||||
|
||||
assert_json_snapshot!(info)
|
||||
with_settings!({ prepend_module_to_snapshot => false }, {
|
||||
assert_json_snapshot!(info)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1354,9 +1383,10 @@ mod tests {
|
||||
sender_claimed_keys: Default::default(),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
session_id: Some("mysessionid76".to_owned()),
|
||||
};
|
||||
|
||||
with_settings!({sort_maps =>true}, {
|
||||
with_settings!({ sort_maps => true, prepend_module_to_snapshot => false }, {
|
||||
assert_json_snapshot!(info)
|
||||
})
|
||||
}
|
||||
@@ -1383,6 +1413,7 @@ mod tests {
|
||||
]),
|
||||
},
|
||||
verification_state: VerificationState::Verified,
|
||||
session_id: Some("mysessionid112".to_owned()),
|
||||
},
|
||||
unsigned_encryption_info: Some(BTreeMap::from([(
|
||||
UnsignedEventLocation::RelationsThreadLatestEvent,
|
||||
@@ -1397,7 +1428,7 @@ mod tests {
|
||||
push_actions: Default::default(),
|
||||
};
|
||||
|
||||
with_settings!({sort_maps =>true}, {
|
||||
with_settings!({ sort_maps => true, prepend_module_to_snapshot => false }, {
|
||||
// We use directly the serde_json formatter here, because of a bug in insta
|
||||
// not serializing custom BTreeMap key enum https://github.com/mitsuhiko/insta/issues/689
|
||||
assert_json_snapshot! {
|
||||
|
||||
@@ -47,13 +47,13 @@ where
|
||||
let _ = future.await;
|
||||
});
|
||||
|
||||
JoinHandle { remote_handle, abort_handle }
|
||||
JoinHandle { remote_handle: Some(remote_handle), abort_handle }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug)]
|
||||
pub struct JoinHandle<T> {
|
||||
remote_handle: RemoteHandle<T>,
|
||||
remote_handle: Option<RemoteHandle<T>>,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
@@ -62,6 +62,20 @@ impl<T> JoinHandle<T> {
|
||||
pub fn abort(&self) {
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.abort_handle.is_aborted()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T> Drop for JoinHandle<T> {
|
||||
fn drop(&mut self) {
|
||||
// don't abort the spawned future
|
||||
if let Some(h) = self.remote_handle.take() {
|
||||
h.forget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -72,8 +86,10 @@ impl<T: 'static> Future for JoinHandle<T> {
|
||||
if self.abort_handle.is_aborted() {
|
||||
// The future has been aborted. It is not possible to poll it again.
|
||||
Poll::Ready(Err(JoinError))
|
||||
} else if let Some(handle) = self.remote_handle.as_mut() {
|
||||
Pin::new(handle).poll(cx).map(Ok)
|
||||
} else {
|
||||
Pin::new(&mut self.remote_handle).poll(cx).map(Ok)
|
||||
Poll::Ready(Err(JoinError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ pub mod sleep;
|
||||
pub mod store_locks;
|
||||
pub mod timeout;
|
||||
pub mod tracing_timer;
|
||||
pub mod ttl_cache;
|
||||
|
||||
// We cannot currently measure test coverage in the WASM environment, so
|
||||
// js_tracing is incorrectly flagged as untested. Disable coverage checking for
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
iter::repeat_n,
|
||||
ops::{ControlFlow, Not},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
@@ -133,7 +134,8 @@ impl UpdateToVectorDiff {
|
||||
///
|
||||
/// * [`Update::NewItemsChunk`] and [`Update::NewGapChunk`] are inserting a
|
||||
/// new pair with a chunk length of 0 at the appropriate index,
|
||||
/// * [`Update::RemoveChunk`] is removing a pair,
|
||||
/// * [`Update::RemoveChunk`] is removing a pair, and is potentially
|
||||
/// emitting [`VectorDiff`],
|
||||
/// * [`Update::PushItems`] is increasing the length of the appropriate pair
|
||||
/// by the number of new items, and is potentially emitting
|
||||
/// [`VectorDiff`],
|
||||
@@ -264,11 +266,10 @@ impl UpdateToVectorDiff {
|
||||
| Update::NewGapChunk { previous, new, next, .. } => {
|
||||
match (previous, next) {
|
||||
// New chunk at the end.
|
||||
(Some(previous), None) => {
|
||||
debug_assert!(
|
||||
matches!(self.chunks.back(), Some((p, _)) if p == previous),
|
||||
"Inserting new chunk at the end: The previous chunk is invalid"
|
||||
);
|
||||
(Some(_previous), None) => {
|
||||
// No need to check `previous`. It's possible that the linked chunk is
|
||||
// lazily loaded, chunk by chunk. The `next` is always reliable, but the
|
||||
// `previous` might not exist in-memory yet.
|
||||
|
||||
self.chunks.push_back((*new, 0));
|
||||
}
|
||||
@@ -284,7 +285,7 @@ impl UpdateToVectorDiff {
|
||||
}
|
||||
|
||||
// New chunk is inserted between 2 chunks.
|
||||
(Some(previous), Some(next)) => {
|
||||
(Some(_previous), Some(next)) => {
|
||||
let next_chunk_index = self
|
||||
.chunks
|
||||
.iter()
|
||||
@@ -296,10 +297,9 @@ impl UpdateToVectorDiff {
|
||||
// or `ObservableUpdates` contain a bug.
|
||||
.expect("Inserting new chunk: The chunk is not found");
|
||||
|
||||
debug_assert!(
|
||||
matches!(self.chunks.get(next_chunk_index - 1), Some((p, _)) if p == previous),
|
||||
"Inserting new chunk: The previous chunk is invalid"
|
||||
);
|
||||
// No need to check `previous`. It's possible that the linked chunk is
|
||||
// lazily loaded, chunk by chunk. The `next` is always reliable, but the
|
||||
// `previous` might not exist in-memory yet.
|
||||
|
||||
self.chunks.insert(next_chunk_index, (*new, 0));
|
||||
}
|
||||
@@ -319,22 +319,17 @@ impl UpdateToVectorDiff {
|
||||
}
|
||||
}
|
||||
|
||||
Update::RemoveChunk(expected_chunk_identifier) => {
|
||||
let chunk_index = self
|
||||
.chunks
|
||||
.iter()
|
||||
.position(|(chunk_identifier, _)| {
|
||||
chunk_identifier == expected_chunk_identifier
|
||||
})
|
||||
// SAFETY: Assuming `LinkedChunk` and `ObservableUpdates` are not buggy, and
|
||||
// assuming `Self::chunks` is correctly initialized, it is not possible to
|
||||
// remove a chunk that does not exist. If this predicate fails, it means
|
||||
// `LinkedChunk` or `ObservableUpdates` contain a bug.
|
||||
.expect("Removing a chunk: The chunk is not found");
|
||||
Update::RemoveChunk(chunk_identifier) => {
|
||||
let (offset, (chunk_index, _)) =
|
||||
self.map_to_offset(&Position(*chunk_identifier, 0));
|
||||
|
||||
// It's OK to ignore the result. The `chunk_index` exists because it's been
|
||||
// found, and we don't care about its associated value.
|
||||
let _ = self.chunks.remove(chunk_index);
|
||||
let (_, number_of_items) = self
|
||||
.chunks
|
||||
.remove(chunk_index)
|
||||
.expect("Removing an index out of the bounds");
|
||||
|
||||
// Removing at the same index because each `Remove` shifts items to the left.
|
||||
diffs.extend(repeat_n(VectorDiff::Remove { index: offset }, number_of_items));
|
||||
}
|
||||
|
||||
Update::PushItems { at: position, items } => {
|
||||
@@ -478,7 +473,7 @@ mod tests {
|
||||
use imbl::{vector, Vector};
|
||||
|
||||
use super::{
|
||||
super::{ChunkIdentifierGenerator, EmptyChunk, LinkedChunk},
|
||||
super::{Chunk, ChunkIdentifierGenerator, LinkedChunk, Update},
|
||||
VectorDiff,
|
||||
};
|
||||
|
||||
@@ -635,10 +630,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let removed_item = linked_chunk
|
||||
.remove_item_at(
|
||||
linked_chunk.item_position(|item| *item == 'c').unwrap(),
|
||||
EmptyChunk::Remove,
|
||||
)
|
||||
.remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(removed_item, 'c');
|
||||
assert_items_eq!(
|
||||
@@ -658,10 +650,7 @@ mod tests {
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 7 }]);
|
||||
|
||||
let removed_item = linked_chunk
|
||||
.remove_item_at(
|
||||
linked_chunk.item_position(|item| *item == 'z').unwrap(),
|
||||
EmptyChunk::Remove,
|
||||
)
|
||||
.remove_item_at(linked_chunk.item_position(|item| *item == 'z').unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(removed_item, 'z');
|
||||
assert_items_eq!(
|
||||
@@ -852,6 +841,99 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_vector_remove_chunk() {
|
||||
let mut linked_chunk = LinkedChunk::<3, char, ()>::new_with_update_history();
|
||||
let mut as_vector = linked_chunk.as_vector().unwrap();
|
||||
|
||||
let mut accumulator = Vector::new();
|
||||
|
||||
assert!(as_vector.take().is_empty());
|
||||
|
||||
linked_chunk.push_items_back(['a', 'b']);
|
||||
linked_chunk.push_gap_back(());
|
||||
linked_chunk.push_items_back(['c']);
|
||||
linked_chunk.push_gap_back(());
|
||||
linked_chunk.push_items_back(['d', 'e', 'f', 'g']);
|
||||
|
||||
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['c'] [-] ['d', 'e', 'f'] ['g']);
|
||||
|
||||
// From an `ObservableVector` point of view, it would look like:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+
|
||||
// | a | b | c | d | e | f | g |
|
||||
// +---+---+---+---+---+---+---+
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// |
|
||||
// new
|
||||
apply_and_assert_eq(
|
||||
&mut accumulator,
|
||||
as_vector.take(),
|
||||
&[
|
||||
VectorDiff::Append { values: vector!['a', 'b'] },
|
||||
VectorDiff::Append { values: vector!['c'] },
|
||||
VectorDiff::Append { values: vector!['d', 'e', 'f'] },
|
||||
VectorDiff::Append { values: vector!['g'] },
|
||||
],
|
||||
);
|
||||
|
||||
// Empty a chunk, and remove it once it is empty.
|
||||
linked_chunk
|
||||
.remove_item_at(linked_chunk.item_position(|item| *item == 'c').unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_items_eq!(linked_chunk, ['a', 'b'] [-] [-] ['d', 'e', 'f'] ['g']);
|
||||
|
||||
// From an `ObservableVector` point of view, it would look like:
|
||||
//
|
||||
// 0 1 2 3 4 5 6
|
||||
// +---+---+---+---+---+---+
|
||||
// | a | b | d | e | f | g |
|
||||
// +---+---+---+---+---+---+
|
||||
// ^
|
||||
// |
|
||||
// `c` has been removed
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[VectorDiff::Remove { index: 2 }]);
|
||||
|
||||
// Remove a gap.
|
||||
linked_chunk
|
||||
.remove_empty_chunk_at(linked_chunk.chunk_identifier(Chunk::is_gap).unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_items_eq!(linked_chunk, ['a', 'b'] [-] ['d', 'e', 'f'] ['g']);
|
||||
|
||||
// From an `ObservableVector` point of view, nothing changes.
|
||||
apply_and_assert_eq(&mut accumulator, as_vector.take(), &[]);
|
||||
|
||||
// Remove a non-empty chunk. This is not possible with the public
|
||||
// `LinkedChunk` API yet, but let's try.
|
||||
let d_e_and_f = linked_chunk.item_position(|item| *item == 'f').unwrap().chunk_identifier();
|
||||
let updates = linked_chunk.updates().unwrap();
|
||||
updates.push(Update::RemoveChunk(d_e_and_f));
|
||||
// Note that `linked_chunk` is getting out of sync with `AsVector`
|
||||
// but it's just a test. Better, it's the end of the test.
|
||||
|
||||
// From an `ObservableVector` point of view, it would look like:
|
||||
//
|
||||
// 0 1 2 3
|
||||
// +---+---+---+
|
||||
// | a | b | g |
|
||||
// +---+---+---+
|
||||
// ^
|
||||
// |
|
||||
// `d`, `e` and `f` have been removed
|
||||
apply_and_assert_eq(
|
||||
&mut accumulator,
|
||||
as_vector.take(),
|
||||
&[
|
||||
VectorDiff::Remove { index: 2 },
|
||||
VectorDiff::Remove { index: 2 },
|
||||
VectorDiff::Remove { index: 2 },
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod proptests {
|
||||
use proptest::prelude::*;
|
||||
@@ -917,7 +999,7 @@ mod tests {
|
||||
continue;
|
||||
};
|
||||
|
||||
linked_chunk.remove_item_at(position, EmptyChunk::Remove).expect("Failed to remove an item");
|
||||
linked_chunk.remove_item_at(position).expect("Failed to remove an item");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
// 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::{BTreeMap, HashSet},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
Chunk, ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, Ends, LinkedChunk,
|
||||
ObservableUpdates, RawChunk,
|
||||
};
|
||||
|
||||
/// A temporary chunk representation in the [`LinkedChunkBuilder`].
|
||||
///
|
||||
/// Instead of using linking the chunks with pointers, this uses
|
||||
/// [`ChunkIdentifier`] as the temporary links to the previous and next chunks,
|
||||
/// which will get resolved later when re-building the full data structure. This
|
||||
/// allows using chunks that references other chunks that aren't known yet.
|
||||
struct TemporaryChunk<Item, Gap> {
|
||||
previous: Option<ChunkIdentifier>,
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: ChunkContent<Item, Gap>,
|
||||
}
|
||||
|
||||
/// A data structure to rebuild a linked chunk from its raw representation.
|
||||
///
|
||||
/// A linked chunk can be rebuilt incrementally from its internal
|
||||
/// representation, with the chunks being added *in any order*, as long as they
|
||||
/// form a single connected component eventually (viz., there's no
|
||||
/// subgraphs/sublists isolated from the one final linked list). If they don't,
|
||||
/// then the final call to [`LinkedChunkBuilder::build()`] will result in an
|
||||
/// error).
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct LinkedChunkBuilder<const CAP: usize, Item, Gap> {
|
||||
/// Work-in-progress chunks.
|
||||
chunks: BTreeMap<ChunkIdentifier, TemporaryChunk<Item, Gap>>,
|
||||
|
||||
/// Is the final `LinkedChunk` expected to include an update history, as if
|
||||
/// it were created with [`LinkedChunk::new_with_update_history`]?
|
||||
build_with_update_history: bool,
|
||||
}
|
||||
|
||||
impl<const CAP: usize, Item, Gap> Default for LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const CAP: usize, Item, Gap> LinkedChunkBuilder<CAP, Item, Gap> {
|
||||
/// Create an empty [`LinkedChunkBuilder`] with no update history.
|
||||
pub fn new() -> Self {
|
||||
Self { chunks: Default::default(), build_with_update_history: false }
|
||||
}
|
||||
|
||||
/// Stash a gap chunk with its content.
|
||||
///
|
||||
/// This can be called even if the previous and next chunks have not been
|
||||
/// added yet. Resolving these chunks will happen at the time of calling
|
||||
/// [`LinkedChunkBuilder::build()`].
|
||||
pub fn push_gap(
|
||||
&mut self,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
id: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
content: Gap,
|
||||
) {
|
||||
let chunk = TemporaryChunk { previous, next, content: ChunkContent::Gap(content) };
|
||||
self.chunks.insert(id, chunk);
|
||||
}
|
||||
|
||||
/// Stash an item chunk with its contents.
|
||||
///
|
||||
/// This can be called even if the previous and next chunks have not been
|
||||
/// added yet. Resolving these chunks will happen at the time of calling
|
||||
/// [`LinkedChunkBuilder::build()`].
|
||||
pub fn push_items(
|
||||
&mut self,
|
||||
previous: Option<ChunkIdentifier>,
|
||||
id: ChunkIdentifier,
|
||||
next: Option<ChunkIdentifier>,
|
||||
items: impl IntoIterator<Item = Item>,
|
||||
) {
|
||||
let chunk = TemporaryChunk {
|
||||
previous,
|
||||
next,
|
||||
content: ChunkContent::Items(items.into_iter().collect()),
|
||||
};
|
||||
self.chunks.insert(id, chunk);
|
||||
}
|
||||
|
||||
/// Request that the resulting linked chunk will have an update history, as
|
||||
/// if it were created with [`LinkedChunk::new_with_update_history`].
|
||||
pub fn with_update_history(&mut self) {
|
||||
self.build_with_update_history = true;
|
||||
}
|
||||
|
||||
/// Run all error checks before reconstructing the full linked chunk.
|
||||
///
|
||||
/// Must be called after checking `self.chunks` isn't empty in
|
||||
/// [`Self::build`].
|
||||
///
|
||||
/// Returns the identifier of the first chunk.
|
||||
fn check_consistency(&mut self) -> Result<ChunkIdentifier, LinkedChunkBuilderError> {
|
||||
// Look for the first id.
|
||||
let first_id =
|
||||
self.chunks.iter().find_map(|(id, chunk)| chunk.previous.is_none().then_some(*id));
|
||||
|
||||
// There's no first chunk, but we've checked that `self.chunks` isn't empty:
|
||||
// it's a malformed list.
|
||||
let Some(first_id) = first_id else {
|
||||
return Err(LinkedChunkBuilderError::MissingFirstChunk);
|
||||
};
|
||||
|
||||
// We're going to iterate from the first to the last chunk.
|
||||
// Keep track of chunks we've already visited.
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
// Start from the first chunk.
|
||||
let mut maybe_cur = Some(first_id);
|
||||
|
||||
while let Some(cur) = maybe_cur {
|
||||
// The chunk must be referenced in `self.chunks`.
|
||||
let Some(chunk) = self.chunks.get(&cur) else {
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id: cur });
|
||||
};
|
||||
|
||||
if let ChunkContent::Items(items) = &chunk.content {
|
||||
if items.len() > CAP {
|
||||
return Err(LinkedChunkBuilderError::ChunkTooLarge { id: cur });
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not the first chunk,
|
||||
if cur != first_id {
|
||||
// It must have a previous link.
|
||||
let Some(prev) = chunk.previous else {
|
||||
return Err(LinkedChunkBuilderError::MultipleFirstChunks {
|
||||
first_candidate: first_id,
|
||||
second_candidate: cur,
|
||||
});
|
||||
};
|
||||
|
||||
// And we must have visited its predecessor at this point, since we've
|
||||
// iterated from the first chunk.
|
||||
if !visited.contains(&prev) {
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id: prev });
|
||||
}
|
||||
}
|
||||
|
||||
// Add the current chunk to the list of seen chunks.
|
||||
if !visited.insert(cur) {
|
||||
// If we didn't insert, then it was already visited: there's a cycle!
|
||||
return Err(LinkedChunkBuilderError::Cycle { repeated: cur });
|
||||
}
|
||||
|
||||
// Move on to the next chunk. If it's none, we'll quit the loop.
|
||||
maybe_cur = chunk.next;
|
||||
}
|
||||
|
||||
// If there are more chunks than those we've visited: some of them were not
|
||||
// linked to the "main" branch of the linked list, so we had multiple connected
|
||||
// components.
|
||||
if visited.len() != self.chunks.len() {
|
||||
return Err(LinkedChunkBuilderError::MultipleConnectedComponents);
|
||||
}
|
||||
|
||||
Ok(first_id)
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Result<Option<LinkedChunk<CAP, Item, Gap>>, LinkedChunkBuilderError> {
|
||||
if self.chunks.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Run checks.
|
||||
let first_id = self.check_consistency()?;
|
||||
|
||||
// We're now going to iterate from the first to the last chunk. As we're doing
|
||||
// this, we're also doing a few other things:
|
||||
//
|
||||
// - rebuilding the final `Chunk`s one by one, that will be linked using
|
||||
// pointers,
|
||||
// - counting items from the item chunks we'll encounter,
|
||||
// - finding the max `ChunkIdentifier` (`max_chunk_id`).
|
||||
|
||||
let mut max_chunk_id = first_id.index();
|
||||
|
||||
// Small helper to graduate a temporary chunk into a final one. As we're doing
|
||||
// this, we're also updating the maximum chunk id (that will be used to
|
||||
// set up the id generator), and the number of items in this chunk.
|
||||
|
||||
let mut graduate_chunk = |id: ChunkIdentifier| {
|
||||
let temp = self.chunks.remove(&id)?;
|
||||
|
||||
// Update the maximum chunk identifier, while we're around.
|
||||
max_chunk_id = max_chunk_id.max(id.index());
|
||||
|
||||
// Graduate the current temporary chunk into a final chunk.
|
||||
let chunk_ptr = Chunk::new_leaked(id, temp.content);
|
||||
|
||||
Some((temp.next, chunk_ptr))
|
||||
};
|
||||
|
||||
let Some((mut next_chunk_id, first_chunk_ptr)) = graduate_chunk(first_id) else {
|
||||
// Can't really happen, but oh well.
|
||||
return Err(LinkedChunkBuilderError::MissingFirstChunk);
|
||||
};
|
||||
|
||||
let mut prev_chunk_ptr = first_chunk_ptr;
|
||||
|
||||
while let Some(id) = next_chunk_id {
|
||||
let Some((new_next, mut chunk_ptr)) = graduate_chunk(id) else {
|
||||
// Can't really happen, but oh well.
|
||||
return Err(LinkedChunkBuilderError::MissingChunk { id });
|
||||
};
|
||||
|
||||
let chunk = unsafe { chunk_ptr.as_mut() };
|
||||
|
||||
// Link the current chunk to its previous one.
|
||||
let prev_chunk = unsafe { prev_chunk_ptr.as_mut() };
|
||||
prev_chunk.next = Some(chunk_ptr);
|
||||
chunk.previous = Some(prev_chunk_ptr);
|
||||
|
||||
// Prepare for the next iteration.
|
||||
prev_chunk_ptr = chunk_ptr;
|
||||
next_chunk_id = new_next;
|
||||
}
|
||||
|
||||
debug_assert!(self.chunks.is_empty());
|
||||
|
||||
// Maintain the convention that `Ends::last` may be unset.
|
||||
let last_chunk_ptr = prev_chunk_ptr;
|
||||
let last_chunk_ptr =
|
||||
if first_chunk_ptr == last_chunk_ptr { None } else { Some(last_chunk_ptr) };
|
||||
let links = Ends { first: first_chunk_ptr, last: last_chunk_ptr };
|
||||
|
||||
let chunk_identifier_generator =
|
||||
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(ChunkIdentifier::new(
|
||||
max_chunk_id,
|
||||
));
|
||||
|
||||
let updates =
|
||||
if self.build_with_update_history { Some(ObservableUpdates::new()) } else { None };
|
||||
|
||||
Ok(Some(LinkedChunk { links, chunk_identifier_generator, updates, marker: PhantomData }))
|
||||
}
|
||||
|
||||
/// Fills a linked chunk builder from all the given raw parts.
|
||||
pub fn from_raw_parts(raws: Vec<RawChunk<Item, Gap>>) -> Self {
|
||||
let mut this = Self::new();
|
||||
for raw in raws {
|
||||
match raw.content {
|
||||
ChunkContent::Gap(gap) => {
|
||||
this.push_gap(raw.previous, raw.identifier, raw.next, gap);
|
||||
}
|
||||
ChunkContent::Items(vec) => {
|
||||
this.push_items(raw.previous, raw.identifier, raw.next, vec);
|
||||
}
|
||||
}
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum LinkedChunkBuilderError {
|
||||
#[error("chunk with id {} is too large", id.index())]
|
||||
ChunkTooLarge { id: ChunkIdentifier },
|
||||
|
||||
#[error("there's no first chunk")]
|
||||
MissingFirstChunk,
|
||||
|
||||
#[error("there are multiple first chunks")]
|
||||
MultipleFirstChunks { first_candidate: ChunkIdentifier, second_candidate: ChunkIdentifier },
|
||||
|
||||
#[error("unable to resolve chunk with id {}", id.index())]
|
||||
MissingChunk { id: ChunkIdentifier },
|
||||
|
||||
#[error("rebuilt chunks form a cycle: repeated identifier: {}", repeated.index())]
|
||||
Cycle { repeated: ChunkIdentifier },
|
||||
|
||||
#[error("multiple connected components")]
|
||||
MultipleConnectedComponents,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use super::LinkedChunkBuilder;
|
||||
use crate::linked_chunk::{ChunkIdentifier, LinkedChunkBuilderError};
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
// Building an empty linked chunk works, and returns `None`.
|
||||
let lc = lcb.build().unwrap();
|
||||
assert!(lc.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_success() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
// Note: cid2 is missing on purpose, to confirm that it's fine to have holes in
|
||||
// the chunk id space.
|
||||
let cid3 = ChunkIdentifier::new(3);
|
||||
|
||||
// Check that we can successfully create a linked chunk, independently of the
|
||||
// order in which chunks are added.
|
||||
//
|
||||
// The final chunk will contain [cid0 <-> cid1 <-> cid3], in this order.
|
||||
|
||||
// Adding chunk cid0.
|
||||
lcb.push_items(None, cid0, Some(cid1), vec!['a', 'b', 'c']);
|
||||
// Adding chunk cid3.
|
||||
lcb.push_items(Some(cid1), cid3, None, vec!['d', 'e']);
|
||||
// Adding chunk cid1.
|
||||
lcb.push_gap(Some(cid0), cid1, Some(cid3), 'g');
|
||||
|
||||
let mut lc =
|
||||
lcb.build().expect("building works").expect("returns a non-empty linked chunk");
|
||||
|
||||
// Check the entire content first.
|
||||
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e']);
|
||||
|
||||
// Run checks on the first chunk.
|
||||
let mut chunks = lc.chunks();
|
||||
let first_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert!(first_chunk.previous().is_none());
|
||||
assert_eq!(first_chunk.identifier(), cid0);
|
||||
}
|
||||
|
||||
// Run checks on the second chunk.
|
||||
let second_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert_eq!(second_chunk.identifier(), first_chunk.next().unwrap().identifier());
|
||||
assert_eq!(second_chunk.previous().unwrap().identifier(), first_chunk.identifier());
|
||||
assert_eq!(second_chunk.identifier(), cid1);
|
||||
}
|
||||
|
||||
// Run checks on the third chunk.
|
||||
let third_chunk = chunks.next().unwrap();
|
||||
{
|
||||
assert_eq!(third_chunk.identifier(), second_chunk.next().unwrap().identifier());
|
||||
assert_eq!(third_chunk.previous().unwrap().identifier(), second_chunk.identifier());
|
||||
assert!(third_chunk.next().is_none());
|
||||
assert_eq!(third_chunk.identifier(), cid3);
|
||||
}
|
||||
|
||||
// There's no more chunk.
|
||||
assert!(chunks.next().is_none());
|
||||
|
||||
// The linked chunk had 5 items.
|
||||
assert_eq!(lc.num_items(), 5);
|
||||
|
||||
// Now, if we add a new chunk, its identifier should be the previous one we used
|
||||
// + 1.
|
||||
lc.push_gap_back('h');
|
||||
|
||||
let last_chunk = lc.chunks().last().unwrap();
|
||||
assert_eq!(last_chunk.identifier(), ChunkIdentifier::new(cid3.index() + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_too_large() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
|
||||
// Adding a chunk with 4 items will fail, because the max capacity specified in
|
||||
// the builder generics is 3.
|
||||
lcb.push_items(None, cid0, None, vec!['a', 'b', 'c', 'd']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::ChunkTooLarge { id }) => {
|
||||
assert_eq!(id, cid0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_first_chunk() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
let cid2 = ChunkIdentifier::new(2);
|
||||
|
||||
lcb.push_gap(Some(cid2), cid0, Some(cid1), 'g');
|
||||
lcb.push_items(Some(cid0), cid1, Some(cid2), ['a', 'b', 'c']);
|
||||
lcb.push_items(Some(cid1), cid2, Some(cid0), ['d', 'e', 'f']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MissingFirstChunk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_first_chunks() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
// Second chunk lies and pretends to be the first too.
|
||||
lcb.push_items(None, cid1, Some(cid0), ['a', 'b', 'c']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleFirstChunks { first_candidate, second_candidate }) => {
|
||||
assert_eq!(first_candidate, cid0);
|
||||
assert_eq!(second_candidate, cid1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_chunk() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MissingChunk { id }) => {
|
||||
assert_eq!(id, cid1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
lcb.push_gap(Some(cid0), cid1, Some(cid0), 'g');
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::Cycle { repeated }) => {
|
||||
assert_eq!(repeated, cid0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_connected_components() {
|
||||
let mut lcb = LinkedChunkBuilder::<3, char, char>::new();
|
||||
|
||||
let cid0 = ChunkIdentifier::new(0);
|
||||
let cid1 = ChunkIdentifier::new(1);
|
||||
let cid2 = ChunkIdentifier::new(2);
|
||||
|
||||
// cid0 and cid1 are linked to each other.
|
||||
lcb.push_gap(None, cid0, Some(cid1), 'g');
|
||||
lcb.push_items(Some(cid0), cid1, None, ['a', 'b', 'c']);
|
||||
// cid2 stands on its own.
|
||||
lcb.push_items(None, cid2, None, ['d', 'e', 'f']);
|
||||
|
||||
let res = lcb.build();
|
||||
assert_matches!(res, Err(LinkedChunkBuilderError::MultipleConnectedComponents));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,10 +15,15 @@
|
||||
//! Implementation for a _relational linked chunk_, see
|
||||
//! [`RelationalLinkedChunk`].
|
||||
|
||||
use ruma::{OwnedRoomId, RoomId};
|
||||
use std::{collections::HashMap, hash::Hash};
|
||||
|
||||
use super::{ChunkContent, RawChunk};
|
||||
use crate::linked_chunk::{ChunkIdentifier, Position, Update};
|
||||
use ruma::{OwnedEventId, OwnedRoomId, RoomId};
|
||||
|
||||
use super::{ChunkContent, ChunkIdentifierGenerator, RawChunk};
|
||||
use crate::{
|
||||
deserialized_responses::TimelineEvent,
|
||||
linked_chunk::{ChunkIdentifier, Position, Update},
|
||||
};
|
||||
|
||||
/// A row of the [`RelationalLinkedChunk::chunks`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -31,10 +36,10 @@ struct ChunkRow {
|
||||
|
||||
/// A row of the [`RelationalLinkedChunk::items`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ItemRow<Item, Gap> {
|
||||
struct ItemRow<ItemId, Gap> {
|
||||
room_id: OwnedRoomId,
|
||||
position: Position,
|
||||
item: Either<Item, Gap>,
|
||||
item: Either<ItemId, Gap>,
|
||||
}
|
||||
|
||||
/// Kind of item.
|
||||
@@ -66,23 +71,49 @@ enum Either<Item, Gap> {
|
||||
///
|
||||
/// [`LinkedChunk`]: super::LinkedChunk
|
||||
#[derive(Debug)]
|
||||
pub struct RelationalLinkedChunk<Item, Gap> {
|
||||
pub struct RelationalLinkedChunk<ItemId, Item, Gap> {
|
||||
/// Chunks.
|
||||
chunks: Vec<ChunkRow>,
|
||||
|
||||
/// Items.
|
||||
items: Vec<ItemRow<Item, Gap>>,
|
||||
/// Items chunks.
|
||||
items_chunks: Vec<ItemRow<ItemId, Gap>>,
|
||||
|
||||
/// The items' content themselves.
|
||||
items: HashMap<OwnedRoomId, HashMap<ItemId, Item>>,
|
||||
}
|
||||
|
||||
impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
/// The [`IndexableItem`] trait is used to mark items that can be indexed into a
|
||||
/// [`RelationalLinkedChunk`].
|
||||
pub trait IndexableItem {
|
||||
type ItemId: Hash + PartialEq + Eq + Clone;
|
||||
|
||||
/// Return the identifier of the item.
|
||||
fn id(&self) -> Self::ItemId;
|
||||
}
|
||||
|
||||
impl IndexableItem for TimelineEvent {
|
||||
type ItemId = OwnedEventId;
|
||||
|
||||
fn id(&self) -> Self::ItemId {
|
||||
self.event_id()
|
||||
.expect("all events saved into a relational linked chunk must have a valid event id")
|
||||
}
|
||||
}
|
||||
|
||||
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
|
||||
where
|
||||
Item: IndexableItem<ItemId = ItemId>,
|
||||
ItemId: Hash + PartialEq + Eq + Clone,
|
||||
{
|
||||
/// Create a new relational linked chunk.
|
||||
pub fn new() -> Self {
|
||||
Self { chunks: Vec::new(), items: Vec::new() }
|
||||
Self { chunks: Vec::new(), items_chunks: Vec::new(), items: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Removes all the chunks and items from this relational linked chunk.
|
||||
pub fn clear(&mut self) {
|
||||
self.chunks.clear();
|
||||
self.items_chunks.clear();
|
||||
self.items.clear();
|
||||
}
|
||||
|
||||
@@ -97,7 +128,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
|
||||
Update::NewGapChunk { previous, new, next, gap } => {
|
||||
insert_chunk(&mut self.chunks, room_id, previous, new, next);
|
||||
self.items.push(ItemRow {
|
||||
self.items_chunks.push(ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(new, 0),
|
||||
item: Either::Gap(gap),
|
||||
@@ -108,7 +139,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
remove_chunk(&mut self.chunks, room_id, chunk_identifier);
|
||||
|
||||
let indices_to_remove = self
|
||||
.items
|
||||
.items_chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
@@ -121,16 +152,21 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for index_to_remove in indices_to_remove.into_iter().rev() {
|
||||
self.items.remove(index_to_remove);
|
||||
self.items_chunks.remove(index_to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
Update::PushItems { mut at, items } => {
|
||||
for item in items {
|
||||
self.items.push(ItemRow {
|
||||
let item_id = item.id();
|
||||
self.items
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(item_id.clone(), item);
|
||||
self.items_chunks.push(ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: at,
|
||||
item: Either::Item(item),
|
||||
item: Either::Item(item_id),
|
||||
});
|
||||
at.increment_index();
|
||||
}
|
||||
@@ -138,7 +174,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
|
||||
Update::ReplaceItem { at, item } => {
|
||||
let existing = self
|
||||
.items
|
||||
.items_chunks
|
||||
.iter_mut()
|
||||
.find(|item| item.position == at)
|
||||
.expect("trying to replace at an unknown position");
|
||||
@@ -146,14 +182,16 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
matches!(existing.item, Either::Item(..)),
|
||||
"trying to replace a gap with an item"
|
||||
);
|
||||
existing.item = Either::Item(item);
|
||||
let item_id = item.id();
|
||||
self.items.entry(room_id.to_owned()).or_default().insert(item_id.clone(), item);
|
||||
existing.item = Either::Item(item_id);
|
||||
}
|
||||
|
||||
Update::RemoveItem { at } => {
|
||||
let mut entry_to_remove = None;
|
||||
|
||||
for (nth, ItemRow { room_id: room_id_candidate, position, .. }) in
|
||||
self.items.iter_mut().enumerate()
|
||||
self.items_chunks.iter_mut().enumerate()
|
||||
{
|
||||
// Filter by room ID.
|
||||
if room_id != room_id_candidate {
|
||||
@@ -175,12 +213,13 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
}
|
||||
}
|
||||
|
||||
self.items.remove(entry_to_remove.expect("Remove an unknown item"));
|
||||
self.items_chunks.remove(entry_to_remove.expect("Remove an unknown item"));
|
||||
// We deliberately keep the item in the items collection.
|
||||
}
|
||||
|
||||
Update::DetachLastItems { at } => {
|
||||
let indices_to_remove = self
|
||||
.items
|
||||
.items_chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
@@ -194,7 +233,7 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for index_to_remove in indices_to_remove.into_iter().rev() {
|
||||
self.items.remove(index_to_remove);
|
||||
self.items_chunks.remove(index_to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +241,8 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
|
||||
Update::Clear => {
|
||||
self.chunks.retain(|chunk| chunk.room_id != room_id);
|
||||
self.items.retain(|chunk| chunk.room_id != room_id);
|
||||
self.items_chunks.retain(|chunk| chunk.room_id != room_id);
|
||||
// We deliberately leave the items intact.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,116 +332,259 @@ impl<Item, Gap> RelationalLinkedChunk<Item, Gap> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator that yields items of a particular room, in no
|
||||
/// particular order.
|
||||
pub fn unordered_room_items<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Iterator<Item = (&'a Item, Position)> {
|
||||
self.items_chunks.iter().filter_map(move |item_row| {
|
||||
if item_row.room_id == room_id {
|
||||
match &item_row.item {
|
||||
Either::Item(item_id) => {
|
||||
Some((self.items.get(room_id)?.get(item_id)?, item_row.position))
|
||||
}
|
||||
Either::Gap(..) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Return an iterator over all items of all rooms, without their actual
|
||||
/// positions.
|
||||
///
|
||||
/// This will include out-of-band items.
|
||||
pub fn items(&self) -> impl Iterator<Item = (&Item, &RoomId)> {
|
||||
self.items
|
||||
.iter()
|
||||
.flat_map(|(room_id, items)| items.values().map(|item| (item, room_id.as_ref())))
|
||||
}
|
||||
|
||||
/// Save a single item "out-of-band" in the relational linked chunk.
|
||||
pub fn save_item(&mut self, room_id: OwnedRoomId, item: Item) {
|
||||
let id = item.id();
|
||||
self.items.entry(room_id).or_default().insert(id, item);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> RelationalLinkedChunk<Item, Gap>
|
||||
impl<ItemId, Item, Gap> RelationalLinkedChunk<ItemId, Item, Gap>
|
||||
where
|
||||
Gap: Clone,
|
||||
Item: Clone,
|
||||
ItemId: Hash + PartialEq + Eq,
|
||||
{
|
||||
/// Reloads the chunks.
|
||||
/// Loads all the chunks.
|
||||
///
|
||||
/// Return an error result if the data was malformed in the struct, with a
|
||||
/// string message explaining details about the error.
|
||||
pub fn reload_chunks(&self, room_id: &RoomId) -> Result<Vec<RawChunk<Item, Gap>>, String> {
|
||||
let mut result = Vec::new();
|
||||
#[doc(hidden)]
|
||||
pub fn load_all_chunks(&self, room_id: &RoomId) -> Result<Vec<RawChunk<Item, Gap>>, String> {
|
||||
self.chunks
|
||||
.iter()
|
||||
.filter(|chunk| chunk.room_id == room_id)
|
||||
.map(|chunk_row| load_raw_chunk(self, chunk_row, room_id))
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
}
|
||||
|
||||
for chunk_row in self.chunks.iter().filter(|chunk| chunk.room_id == room_id) {
|
||||
// Find all items that correspond to the chunk.
|
||||
let mut items = self
|
||||
.items
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.room_id == room_id && row.position.chunk_identifier() == chunk_row.chunk
|
||||
})
|
||||
.peekable();
|
||||
pub fn load_last_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<(Option<RawChunk<Item, Gap>>, ChunkIdentifierGenerator), String> {
|
||||
// Find the latest chunk identifier to generate a `ChunkIdentifierGenerator`.
|
||||
let chunk_identifier_generator = match self
|
||||
.chunks
|
||||
.iter()
|
||||
.filter_map(|chunk_row| (chunk_row.room_id == room_id).then_some(chunk_row.chunk))
|
||||
.max()
|
||||
{
|
||||
Some(last_chunk_identifier) => {
|
||||
ChunkIdentifierGenerator::new_from_previous_chunk_identifier(last_chunk_identifier)
|
||||
}
|
||||
None => ChunkIdentifierGenerator::new_from_scratch(),
|
||||
};
|
||||
|
||||
// Look at the first chunk item type, to reconstruct the chunk at hand.
|
||||
let Some(first) = items.peek() else {
|
||||
// The only possibility is that we created an empty items chunk; mark it as
|
||||
// such, and continue.
|
||||
result.push(RawChunk {
|
||||
content: ChunkContent::Items(Vec::new()),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
// Find the last chunk.
|
||||
let mut number_of_chunks = 0;
|
||||
let mut chunk_row = None;
|
||||
|
||||
match &first.item {
|
||||
Either::Item(_) => {
|
||||
// Collect all the related items.
|
||||
let mut collected_items = Vec::new();
|
||||
for row in items {
|
||||
match &row.item {
|
||||
Either::Item(item) => {
|
||||
collected_items.push((item.clone(), row.position.index()))
|
||||
}
|
||||
Either::Gap(_) => {
|
||||
return Err(format!(
|
||||
"unexpected gap in items chunk {}",
|
||||
chunk_row.chunk.index()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for chunk_row_candidate in &self.chunks {
|
||||
if chunk_row_candidate.room_id == room_id {
|
||||
number_of_chunks += 1;
|
||||
|
||||
// Sort them by their position.
|
||||
collected_items.sort_unstable_by_key(|(_item, index)| *index);
|
||||
if chunk_row_candidate.next_chunk.is_none() {
|
||||
chunk_row = Some(chunk_row_candidate);
|
||||
|
||||
result.push(RawChunk {
|
||||
content: ChunkContent::Items(
|
||||
collected_items.into_iter().map(|(item, _index)| item).collect(),
|
||||
),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
}
|
||||
|
||||
Either::Gap(gap) => {
|
||||
assert!(items.next().is_some(), "we just peeked the gap");
|
||||
|
||||
// We shouldn't have more than one item row for this chunk.
|
||||
if items.next().is_some() {
|
||||
return Err(format!(
|
||||
"there shouldn't be more than one item row attached in gap chunk {}",
|
||||
chunk_row.chunk.index()
|
||||
));
|
||||
}
|
||||
|
||||
result.push(RawChunk {
|
||||
content: ChunkContent::Gap(gap.clone()),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
let chunk_row = match chunk_row {
|
||||
// Chunk has been found, all good.
|
||||
Some(chunk_row) => chunk_row,
|
||||
|
||||
// Chunk is not found and there is zero chunk for this room, this is consistent, all
|
||||
// good.
|
||||
None if number_of_chunks == 0 => {
|
||||
return Ok((None, chunk_identifier_generator));
|
||||
}
|
||||
|
||||
// Chunk is not found **but** there are chunks for this room, this is inconsistent. The
|
||||
// linked chunk is malformed.
|
||||
//
|
||||
// Returning `Ok(None)` would be invalid here: we must return an error.
|
||||
None => {
|
||||
return Err(
|
||||
"last chunk is not found but chunks exist: the linked chunk contains a cycle"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Build the chunk.
|
||||
load_raw_chunk(self, chunk_row, room_id)
|
||||
.map(|raw_chunk| (Some(raw_chunk), chunk_identifier_generator))
|
||||
}
|
||||
|
||||
pub fn load_previous_chunk(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
before_chunk_identifier: ChunkIdentifier,
|
||||
) -> Result<Option<RawChunk<Item, Gap>>, String> {
|
||||
// Find the chunk before the chunk identified by `before_chunk_identifier`.
|
||||
let Some(chunk_row) = self.chunks.iter().find(|chunk_row| {
|
||||
chunk_row.room_id == room_id && chunk_row.next_chunk == Some(before_chunk_identifier)
|
||||
}) else {
|
||||
// Chunk is not found.
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Build the chunk.
|
||||
load_raw_chunk(self, chunk_row, room_id).map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, Gap> Default for RelationalLinkedChunk<Item, Gap> {
|
||||
impl<ItemId, Item, Gap> Default for RelationalLinkedChunk<ItemId, Item, Gap>
|
||||
where
|
||||
Item: IndexableItem<ItemId = ItemId>,
|
||||
ItemId: Hash + PartialEq + Eq + Clone,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_raw_chunk<ItemId, Item, Gap>(
|
||||
relational_linked_chunk: &RelationalLinkedChunk<ItemId, Item, Gap>,
|
||||
chunk_row: &ChunkRow,
|
||||
room_id: &RoomId,
|
||||
) -> Result<RawChunk<Item, Gap>, String>
|
||||
where
|
||||
Item: Clone,
|
||||
Gap: Clone,
|
||||
ItemId: Hash + PartialEq + Eq,
|
||||
{
|
||||
// Find all items that correspond to the chunk.
|
||||
let mut items = relational_linked_chunk
|
||||
.items_chunks
|
||||
.iter()
|
||||
.filter(|item_row| {
|
||||
item_row.room_id == room_id && item_row.position.chunk_identifier() == chunk_row.chunk
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let Some(first_item) = items.peek() else {
|
||||
// No item. It means it is a chunk of kind `Items` and that it is empty!
|
||||
return Ok(RawChunk {
|
||||
content: ChunkContent::Items(Vec::new()),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
});
|
||||
};
|
||||
|
||||
Ok(match first_item.item {
|
||||
// This is a chunk of kind `Items`.
|
||||
Either::Item(_) => {
|
||||
// Collect all the items.
|
||||
let mut collected_items = Vec::new();
|
||||
|
||||
for item_row in items {
|
||||
match &item_row.item {
|
||||
Either::Item(item_id) => {
|
||||
collected_items.push((item_id, item_row.position.index()))
|
||||
}
|
||||
|
||||
Either::Gap(_) => {
|
||||
return Err(format!(
|
||||
"unexpected gap in items chunk {}",
|
||||
chunk_row.chunk.index()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort them by their position.
|
||||
collected_items.sort_unstable_by_key(|(_item, index)| *index);
|
||||
|
||||
RawChunk {
|
||||
content: ChunkContent::Items(
|
||||
collected_items
|
||||
.into_iter()
|
||||
.filter_map(|(item_id, _index)| {
|
||||
relational_linked_chunk.items.get(room_id)?.get(item_id).cloned()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
}
|
||||
}
|
||||
|
||||
Either::Gap(ref gap) => {
|
||||
assert!(items.next().is_some(), "we just peeked the gap");
|
||||
|
||||
// We shouldn't have more than one item row for this chunk.
|
||||
if items.next().is_some() {
|
||||
return Err(format!(
|
||||
"there shouldn't be more than one item row attached in gap chunk {}",
|
||||
chunk_row.chunk.index()
|
||||
));
|
||||
}
|
||||
|
||||
RawChunk {
|
||||
content: ChunkContent::Gap(gap.clone()),
|
||||
previous: chunk_row.previous_chunk,
|
||||
identifier: chunk_row.chunk,
|
||||
next: chunk_row.next_chunk,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use ruma::room_id;
|
||||
|
||||
use super::{ChunkIdentifier as CId, *};
|
||||
use crate::linked_chunk::LinkedChunkBuilder;
|
||||
use super::{super::lazy_loader::from_all_chunks, ChunkIdentifier as CId, *};
|
||||
|
||||
impl IndexableItem for char {
|
||||
type ItemId = char;
|
||||
|
||||
fn id(&self) -> Self::ItemId {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_items_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -452,13 +635,13 @@ mod tests {
|
||||
],
|
||||
);
|
||||
// Items have not been modified.
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
assert!(relational_linked_chunk.items_chunks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_gap_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -503,7 +686,7 @@ mod tests {
|
||||
);
|
||||
// Items contains the gap.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
relational_linked_chunk.items_chunks,
|
||||
&[ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
position: Position::new(CId::new(1), 0),
|
||||
@@ -515,7 +698,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_remove_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -555,13 +738,13 @@ mod tests {
|
||||
],
|
||||
);
|
||||
// Items no longer contains the gap.
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
assert!(relational_linked_chunk.items_chunks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -599,7 +782,7 @@ mod tests {
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
relational_linked_chunk.items_chunks,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
@@ -648,7 +831,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_remove_item() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -679,7 +862,7 @@ mod tests {
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
relational_linked_chunk.items_chunks,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
@@ -703,7 +886,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_detach_last_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -744,7 +927,7 @@ mod tests {
|
||||
);
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
relational_linked_chunk.items_chunks,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
@@ -778,21 +961,21 @@ mod tests {
|
||||
#[test]
|
||||
fn test_start_and_end_reattach_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk
|
||||
.apply_updates(room_id, vec![Update::StartReattachItems, Update::EndReattachItems]);
|
||||
|
||||
// Nothing happened.
|
||||
assert!(relational_linked_chunk.chunks.is_empty());
|
||||
assert!(relational_linked_chunk.items.is_empty());
|
||||
assert!(relational_linked_chunk.items_chunks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear() {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
r0,
|
||||
@@ -835,7 +1018,7 @@ mod tests {
|
||||
|
||||
// Items contains the pushed items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
relational_linked_chunk.items_chunks,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: r0.to_owned(),
|
||||
@@ -875,7 +1058,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
relational_linked_chunk.items_chunks,
|
||||
&[ItemRow {
|
||||
room_id: r1.to_owned(),
|
||||
position: Position::new(CId::new(0), 0),
|
||||
@@ -885,20 +1068,20 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reload_empty_linked_chunk() {
|
||||
fn test_load_empty_linked_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
// When I reload the linked chunk components from an empty store,
|
||||
let relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
|
||||
let result = relational_linked_chunk.reload_chunks(room_id).unwrap();
|
||||
let relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
|
||||
let result = relational_linked_chunk.load_all_chunks(room_id).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reload_linked_chunk_with_empty_items() {
|
||||
fn test_load_all_chunks_with_empty_items() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
|
||||
|
||||
// When I store an empty items chunks,
|
||||
relational_linked_chunk.apply_updates(
|
||||
@@ -907,11 +1090,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// It correctly gets reloaded as such.
|
||||
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
|
||||
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
|
||||
.build()
|
||||
.expect("building succeeds")
|
||||
.expect("this leads to a non-empty linked chunk");
|
||||
let lc =
|
||||
from_all_chunks::<3, _, _>(relational_linked_chunk.load_all_chunks(room_id).unwrap())
|
||||
.expect("building succeeds")
|
||||
.expect("this leads to a non-empty linked chunk");
|
||||
|
||||
assert_items_eq!(lc, []);
|
||||
}
|
||||
@@ -919,7 +1101,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_rebuild_linked_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, char>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, char>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -942,11 +1124,10 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
let raws = relational_linked_chunk.reload_chunks(room_id).unwrap();
|
||||
let lc = LinkedChunkBuilder::<3, _, _>::from_raw_parts(raws)
|
||||
.build()
|
||||
.expect("building succeeds")
|
||||
.expect("this leads to a non-empty linked chunk");
|
||||
let lc =
|
||||
from_all_chunks::<3, _, _>(relational_linked_chunk.load_all_chunks(room_id).unwrap())
|
||||
.expect("building succeeds")
|
||||
.expect("this leads to a non-empty linked chunk");
|
||||
|
||||
// The linked chunk is correctly reloaded.
|
||||
assert_items_eq!(lc, ['a', 'b', 'c'] [-] ['d', 'e', 'f']);
|
||||
@@ -955,7 +1136,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_replace_item() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<char, ()>::new();
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
@@ -982,7 +1163,7 @@ mod tests {
|
||||
|
||||
// Items contains the pushed *and* replaced items.
|
||||
assert_eq!(
|
||||
relational_linked_chunk.items,
|
||||
relational_linked_chunk.items_chunks,
|
||||
&[
|
||||
ItemRow {
|
||||
room_id: room_id.to_owned(),
|
||||
@@ -1002,4 +1183,199 @@ mod tests {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unordered_events() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let other_room_id = room_id!("!r1:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['a', 'b', 'c'] },
|
||||
Update::NewItemsChunk { previous: Some(CId::new(0)), new: CId::new(1), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec!['d', 'e', 'f'] },
|
||||
],
|
||||
);
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
other_room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(0), 0), items: vec!['x', 'y', 'z'] },
|
||||
],
|
||||
);
|
||||
|
||||
let mut events = relational_linked_chunk.unordered_room_items(room_id);
|
||||
|
||||
assert_eq!(events.next().unwrap(), (&'a', Position::new(CId::new(0), 0)));
|
||||
assert_eq!(events.next().unwrap(), (&'b', Position::new(CId::new(0), 1)));
|
||||
assert_eq!(events.next().unwrap(), (&'c', Position::new(CId::new(0), 2)));
|
||||
assert_eq!(events.next().unwrap(), (&'d', Position::new(CId::new(1), 0)));
|
||||
assert_eq!(events.next().unwrap(), (&'e', Position::new(CId::new(1), 1)));
|
||||
assert_eq!(events.next().unwrap(), (&'f', Position::new(CId::new(1), 2)));
|
||||
assert!(events.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_last_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
// Case #1: no last chunk.
|
||||
{
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
relational_linked_chunk.load_last_chunk(room_id).unwrap();
|
||||
|
||||
assert!(last_chunk.is_none());
|
||||
assert_eq!(chunk_identifier_generator.current(), 0);
|
||||
}
|
||||
|
||||
// Case #2: only one chunk is present.
|
||||
{
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(42), next: None },
|
||||
Update::PushItems { at: Position::new(CId::new(42), 0), items: vec!['a', 'b'] },
|
||||
],
|
||||
);
|
||||
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
relational_linked_chunk.load_last_chunk(room_id).unwrap();
|
||||
|
||||
assert_matches!(last_chunk, Some(last_chunk) => {
|
||||
assert_eq!(last_chunk.identifier, 42);
|
||||
assert!(last_chunk.previous.is_none());
|
||||
assert!(last_chunk.next.is_none());
|
||||
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items, &['a', 'b']);
|
||||
});
|
||||
});
|
||||
assert_eq!(chunk_identifier_generator.current(), 42);
|
||||
}
|
||||
|
||||
// Case #3: more chunks are present.
|
||||
{
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk {
|
||||
previous: Some(CId::new(42)),
|
||||
new: CId::new(7),
|
||||
next: None,
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(7), 0),
|
||||
items: vec!['c', 'd', 'e'],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let (last_chunk, chunk_identifier_generator) =
|
||||
relational_linked_chunk.load_last_chunk(room_id).unwrap();
|
||||
|
||||
assert_matches!(last_chunk, Some(last_chunk) => {
|
||||
assert_eq!(last_chunk.identifier, 7);
|
||||
assert_matches!(last_chunk.previous, Some(previous) => {
|
||||
assert_eq!(previous, 42);
|
||||
});
|
||||
assert!(last_chunk.next.is_none());
|
||||
assert_matches!(last_chunk.content, ChunkContent::Items(items) => {
|
||||
assert_eq!(items.len(), 3);
|
||||
assert_eq!(items, &['c', 'd', 'e']);
|
||||
});
|
||||
});
|
||||
assert_eq!(chunk_identifier_generator.current(), 42);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_last_chunk_with_a_cycle() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },
|
||||
Update::NewItemsChunk {
|
||||
// Because `previous` connects to chunk #0, it will create a cycle.
|
||||
// Chunk #0 will have a `next` set to chunk #1! Consequently, the last chunk
|
||||
// **does not exist**. We have to detect this cycle.
|
||||
previous: Some(CId::new(0)),
|
||||
new: CId::new(1),
|
||||
next: Some(CId::new(0)),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
relational_linked_chunk.load_last_chunk(room_id).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_previous_chunk() {
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
let mut relational_linked_chunk = RelationalLinkedChunk::<_, char, ()>::new();
|
||||
|
||||
// Case #1: no chunk at all, equivalent to having an inexistent
|
||||
// `before_chunk_identifier`.
|
||||
{
|
||||
let previous_chunk =
|
||||
relational_linked_chunk.load_previous_chunk(room_id, CId::new(153)).unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
|
||||
// Case #2: there is one chunk only: we request the previous on this
|
||||
// one, it doesn't exist.
|
||||
{
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![Update::NewItemsChunk { previous: None, new: CId::new(42), next: None }],
|
||||
);
|
||||
|
||||
let previous_chunk =
|
||||
relational_linked_chunk.load_previous_chunk(room_id, CId::new(42)).unwrap();
|
||||
|
||||
assert!(previous_chunk.is_none());
|
||||
}
|
||||
|
||||
// Case #3: there is two chunks.
|
||||
{
|
||||
relational_linked_chunk.apply_updates(
|
||||
room_id,
|
||||
vec![
|
||||
// new chunk before the one that exists.
|
||||
Update::NewItemsChunk {
|
||||
previous: None,
|
||||
new: CId::new(7),
|
||||
next: Some(CId::new(42)),
|
||||
},
|
||||
Update::PushItems {
|
||||
at: Position::new(CId::new(7), 0),
|
||||
items: vec!['a', 'b', 'c'],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let previous_chunk =
|
||||
relational_linked_chunk.load_previous_chunk(room_id, CId::new(42)).unwrap();
|
||||
|
||||
assert_matches!(previous_chunk, Some(previous_chunk) => {
|
||||
assert_eq!(previous_chunk.identifier, 7);
|
||||
assert!(previous_chunk.previous.is_none());
|
||||
assert_matches!(previous_chunk.next, Some(next) => {
|
||||
assert_eq!(next, 42);
|
||||
});
|
||||
assert_matches!(previous_chunk.content, ChunkContent::Items(items) => {
|
||||
assert_eq!(items.len(), 3);
|
||||
assert_eq!(items, &['a', 'b', 'c']);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,11 @@ impl<Item, Gap> ObservableUpdates<Item, Gap> {
|
||||
self.inner.write().unwrap().push(update);
|
||||
}
|
||||
|
||||
/// Clear all pending updates.
|
||||
pub(super) fn clear_pending(&mut self) {
|
||||
self.inner.write().unwrap().clear_pending();
|
||||
}
|
||||
|
||||
/// Take new updates.
|
||||
///
|
||||
/// Updates that have been taken will not be read again.
|
||||
@@ -242,6 +247,19 @@ impl<Item, Gap> UpdatesInner<Item, Gap> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all pending updates.
|
||||
fn clear_pending(&mut self) {
|
||||
self.updates.clear();
|
||||
|
||||
// Reset all the per-reader indices.
|
||||
for idx in self.last_index_per_reader.values_mut() {
|
||||
*idx = 0;
|
||||
}
|
||||
|
||||
// No need to wake the wakers; they're waiting for a new update, and we
|
||||
// just made them all disappear.
|
||||
}
|
||||
|
||||
/// Take new updates; it considers the caller is the main reader, i.e. it
|
||||
/// will use the [`Self::MAIN_READER_TOKEN`].
|
||||
///
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user