Compare commits
1071 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 | |||
| 4c46e42201 | |||
| 0d4bc65e28 | |||
| 5e1bae02fe | |||
| 77a67de7df | |||
| f27eb4d1c8 | |||
| 05814c5559 | |||
| d5d9898fb4 | |||
| f641a639cd | |||
| 3f71d9a379 | |||
| b077f45e78 | |||
| 648d527f2f | |||
| 8513547e92 | |||
| d18669e8d9 | |||
| a0426251a3 | |||
| 2739c5bf27 | |||
| 381f4d419f | |||
| 049021fe27 | |||
| 4db32b15ba | |||
| 9ab5547065 | |||
| df3cb002a5 | |||
| 6ebd4295b9 | |||
| c5104d68fd | |||
| 0064839283 | |||
| 2727d72916 | |||
| 33a2cc3031 | |||
| 38097f90b2 | |||
| 47d08683a2 | |||
| 619346acad | |||
| 525f9866a4 | |||
| d7dc1c9b5b | |||
| 7da3aaaa8a | |||
| 5aaa6bf187 | |||
| af9a5edd59 | |||
| 6e764644b3 | |||
| 8dc2ec9dc4 | |||
| 4e1ae3d5e9 | |||
| 582b3a91d6 | |||
| f7467ff57a | |||
| 57919f5480 | |||
| b8949cfe26 | |||
| 8d27b0c811 | |||
| 3707d2fb81 | |||
| eaaa5e17a0 | |||
| e3958b754c | |||
| 78d9e1292f | |||
| d594b4dad7 | |||
| 3f40ad83a5 | |||
| 5049d1a3b6 | |||
| 29862fc9bd | |||
| 585224b2fa | |||
| 0dc5e69ace | |||
| b323802ab0 | |||
| 252786d2ef | |||
| 97cbe57d3f | |||
| 0d8ad159c3 | |||
| 9d732395ce | |||
| 6a772d1c56 | |||
| b71499ffe6 | |||
| 4dadf8581a | |||
| 4d4cd61363 | |||
| 6f42b0a67b | |||
| e3b348761e | |||
| d755a8a3aa | |||
| 66e3ddec47 | |||
| 720d443452 | |||
| 33d691a58e | |||
| c2f39c1086 | |||
| df9c355aed | |||
| 0334ff3f64 | |||
| 8262726369 | |||
| a4a6bf540d | |||
| 9b38f38aea | |||
| 2e05cc74bf | |||
| eb9b86971a | |||
| 31e7ec182c | |||
| 5e8f3b2bc8 | |||
| c0b91c4b0e | |||
| 46d90afa9c | |||
| 29e19b729b | |||
| 20c1eff391 | |||
| 3e40db3d7f | |||
| 8ca5983093 | |||
| 144f568a5c | |||
| 34c7dd48ae | |||
| 6c7d8c16bb | |||
| 2c930df8aa | |||
| 834bed2b1a | |||
| 8d530ef220 | |||
| 542d68dcda | |||
| 50696a0d74 | |||
| 182fc6fd8f | |||
| fe85cddf88 | |||
| 9ff3761cac | |||
| a311dcbd3e | |||
| 447bd67fe1 | |||
| d8ba2b521c | |||
| 8de15429fb | |||
| 3f398d8934 | |||
| f8ec957193 | |||
| 7cc121ab38 | |||
| 8a4918309a | |||
| 30d7fac927 | |||
| be71c6df56 | |||
| 06ad67f99c | |||
| 28fb6f7c27 | |||
| 842d32d41b | |||
| b52cf8327a | |||
| d14526f161 | |||
| 1b8a6b705c | |||
| 7c2b15fe86 | |||
| 3085f05d51 | |||
| 4344e06707 | |||
| 173ec75bb3 | |||
| 1d3f8bf898 | |||
| 5b3b87d3e2 | |||
| 6dc5b33d87 | |||
| 408b843156 | |||
| 0820170261 | |||
| 254ac6f2ce | |||
| 468a7ac883 | |||
| 3e610c80e1 | |||
| f43edbd31f | |||
| 7c57f2cee4 | |||
| 8d612eca46 | |||
| 98f4d55aa0 | |||
| 709b09c4ec | |||
| 818876a22e | |||
| 2657eb7866 | |||
| aaecbf07f2 | |||
| f336638a17 | |||
| 839fbe477c | |||
| 35ad5441d3 | |||
| 756dec264d | |||
| 87983ab610 | |||
| 66ffc3448e | |||
| c6e308717d | |||
| 4c4dd03411 | |||
| 2d0f873342 | |||
| 041627ec4a | |||
| da4b8004f2 | |||
| 3428494468 | |||
| 0c2046f93b | |||
| eb31f035e6 | |||
| df51404a14 | |||
| 3e78e441d4 | |||
| 02c2e55855 | |||
| a528624274 | |||
| 1d83d42e9f | |||
| 4684cfb780 | |||
| 991c9ad610 | |||
| e826c54a42 | |||
| 9ae658c1b9 | |||
| 4341aaf65c | |||
| ad847a82c8 | |||
| dad3e6839f | |||
| d078ef6155 | |||
| 210c5749f1 | |||
| 0c74abbc50 | |||
| dbadfe19b0 | |||
| f3e43dbfa4 | |||
| b846a6dd81 | |||
| c82e469fc3 | |||
| f2c9a8f723 | |||
| 2e16021f14 | |||
| 47fc073b70 | |||
| 160600e8c0 | |||
| 993c103270 | |||
| e077980ba2 | |||
| 63d14b798b | |||
| 077d63a9fc | |||
| 453c4e12db | |||
| f7db52e069 | |||
| 2bd8c56e64 | |||
| f231c74314 | |||
| 2cb6ee8e6d | |||
| c24770a774 | |||
| 7fa06cb028 | |||
| 50383098ff | |||
| 6f780a499c | |||
| 425e48a46d | |||
| 3dd81fbe2c | |||
| 6a0333e812 | |||
| b3a789af90 | |||
| 560e582e41 | |||
| de7397a20e | |||
| 8bd94318c0 | |||
| fe3cc09ae0 | |||
| 3a3cc54067 | |||
| 47f8b32ea1 | |||
| 49748dbd4b | |||
| 25ea5fdd73 | |||
| 5fadde5a6d | |||
| b6be4d5170 | |||
| c9bac4ff2b | |||
| fedf7d214f | |||
| c969f903b7 | |||
| bd5d7aafee | |||
| e015a531da | |||
| b9014a5e2a | |||
| e9487b0851 | |||
| c60bfb877a | |||
| ee32b1f600 | |||
| 9641aa9082 | |||
| a8ca77f4fc | |||
| e647ff935e | |||
| 7f04a9a18b | |||
| 67d2cb790d | |||
| 279c78b3e2 | |||
| 9514388108 | |||
| e6dc10933c | |||
| c456356424 | |||
| 5af326b36e | |||
| 5548f38393 | |||
| d9c1188f87 | |||
| 588702756b | |||
| d6a74d389d | |||
| d807d71e22 | |||
| 587545ae82 | |||
| 49985e5476 | |||
| 4fbe79a27d | |||
| f61ad19ae6 | |||
| c9a49006f6 | |||
| ca9eb70db5 | |||
| f173aea6e4 | |||
| e37ad11b47 | |||
| d6c2a63f5c | |||
| 4ebf5056be | |||
| a79d409f9d | |||
| 5941495e68 | |||
| b3491582d0 | |||
| def4bbbed2 | |||
| 1dd2b2c9e8 | |||
| e4b269e0de | |||
| cb72d4375f | |||
| 7ec384c61a | |||
| 7466f77eae | |||
| 526b5c4630 | |||
| 4043f9bf5d | |||
| ff5dcbf631 | |||
| 6c053a86bf | |||
| 0cae54cc3f | |||
| 692aceba50 | |||
| c4a86a3d0a | |||
| 5f5aa81174 | |||
| 6e0f258a39 | |||
| c4bfbd0f44 | |||
| 8e0ee47637 | |||
| 0915eeed51 | |||
| fb54e869e9 | |||
| 9e97ed3134 | |||
| b926c4287a | |||
| ddf4d575b7 | |||
| 2a954e3ce3 | |||
| b837865226 | |||
| eac5a5eb35 | |||
| d6c64027f6 | |||
| df4b69666c | |||
| 5675ac7f46 | |||
| 6b2233f8c4 | |||
| 61dd560499 | |||
| 62567ca6eb | |||
| 46dc2a9c5e | |||
| 891583b70e | |||
| e19bdbfd59 | |||
| 14d0cc1935 | |||
| b8d0384da7 | |||
| 4e0a6d15ca | |||
| 251433382f | |||
| 34e993435d | |||
| dc2775e194 | |||
| 45c3752cae | |||
| ed178602d7 | |||
| 35a03278c3 | |||
| 9e69b631ee | |||
| 5ff556f6c3 | |||
| d64960679f | |||
| 7ff1170681 | |||
| 55e25a3717 | |||
| 3f977b79fa | |||
| aca8c8b8ee | |||
| 47c24b9a17 | |||
| 47445b10f1 | |||
| c5a9a1e215 | |||
| 2ef14ded41 | |||
| 8205da898e | |||
| 618e47250d | |||
| 5110aa64aa | |||
| bcad0a3059 | |||
| b7b88f58d2 | |||
| 412fcab4dc | |||
| 8e75a940f7 | |||
| 70fb7899e6 | |||
| 1480fada6e | |||
| c50358366f | |||
| adb4428a69 | |||
| 667a8e684c | |||
| f4b50db972 | |||
| 1abb2efc51 | |||
| c4132252d3 | |||
| 51c76a15ad | |||
| f1842ba5d0 | |||
| d8dd72fd9c | |||
| 054f5e28f6 | |||
| 38e35b99d0 | |||
| 39afb531ef | |||
| c1ff5ff49f | |||
| 2358e4c32f | |||
| 409fccb709 | |||
| b25fd830ec | |||
| 02ab57870a | |||
| eca3749b28 | |||
| 3f17325bac | |||
| 23c09b2c9d | |||
| c1f8232450 | |||
| e28073361d | |||
| 1c2fb1ab72 | |||
| be89e3aacb | |||
| 36427b0e12 | |||
| f8a9d12c88 | |||
| 5f5e979e16 | |||
| 519f281844 | |||
| 3b31bbec0c | |||
| f2942db316 | |||
| e4712be946 | |||
| bc8c4f5e58 | |||
| fe9354a886 | |||
| d00ff8fa1f | |||
| 60f521cc23 | |||
| bcb9a86a00 | |||
| 3f0712010f | |||
| d89194f071 | |||
| a20ad728b5 | |||
| 0d546dce5f | |||
| 38cc9fb7c8 | |||
| 616c193a30 | |||
| 4a88e7cfee | |||
| 5d0fed5e53 | |||
| 9975365a1e | |||
| f18e0b18a1 | |||
| b18100228e | |||
| de568837fb | |||
| bc582ae101 |
+5
-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,5 +59,8 @@ 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
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
java-version: '17'
|
||||
|
||||
- name: Install android sdk
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.4
|
||||
uses: malinskiy/action-android/install-sdk@release/0.1.7
|
||||
|
||||
- name: Install android ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
|
||||
+44
-46
@@ -18,6 +18,9 @@ concurrency:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Insta.rs is run directly via cargo test. We don't want insta.rs to create new snapshots files.
|
||||
# Just want it to run the tests (option `no` instead of `auto`).
|
||||
INSTA_UPDATE: no
|
||||
|
||||
jobs:
|
||||
xtask:
|
||||
@@ -221,12 +224,16 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m], no-default'
|
||||
cmd: matrix-sdk-no-default
|
||||
|
||||
- name: '[m]-ui'
|
||||
cmd: matrix-sdk-ui
|
||||
check_only: true
|
||||
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
|
||||
- name: '[m], no-default, wasm-flags'
|
||||
cmd: matrix-sdk-no-default
|
||||
|
||||
- name: '[m], indexeddb stores'
|
||||
cmd: matrix-sdk-indexeddb-stores
|
||||
|
||||
@@ -245,6 +252,7 @@ jobs:
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: qmaru/wasm-pack-action@v0.5.0
|
||||
if: '!matrix.check_only'
|
||||
with:
|
||||
version: v0.10.3
|
||||
|
||||
@@ -274,27 +282,10 @@ jobs:
|
||||
target/debug/xtask ci wasm ${{ matrix.cmd }}
|
||||
|
||||
- name: Wasm-Pack test
|
||||
if: '!matrix.check_only'
|
||||
run: |
|
||||
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
|
||||
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
run: |
|
||||
cargo fmt -- --check
|
||||
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
@@ -304,10 +295,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.28.3
|
||||
uses: crate-ci/typos@v1.31.1
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
lint:
|
||||
name: Lint
|
||||
needs: xtask
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -323,8 +314,8 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2024-11-26
|
||||
components: clippy
|
||||
toolchain: nightly-2025-02-20
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -338,6 +329,10 @@ jobs:
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Check Formatting
|
||||
run: |
|
||||
target/debug/xtask ci style
|
||||
|
||||
- name: Clippy
|
||||
run: |
|
||||
target/debug/xtask ci clippy
|
||||
@@ -347,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:
|
||||
@@ -406,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
|
||||
|
||||
@@ -30,6 +30,33 @@ integration tests that need a running synapse instance. These tests reside in
|
||||
synapse for testing purposes.
|
||||
|
||||
|
||||
### Snapshot Testing
|
||||
|
||||
You can add/review snapshot tests using [insta.rs](https://insta.rs)
|
||||
|
||||
Every new struct/enum that derives `Serialize` `Deserialise` should have a snapshot test for it.
|
||||
Any code change that breaks serialisation will then break a test, the author will then have to decide
|
||||
how to handle migration and test it if needed.
|
||||
|
||||
|
||||
And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool:
|
||||
|
||||
Unix:
|
||||
```
|
||||
curl -LsSf https://insta.rs/install.sh | sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
```
|
||||
powershell -c "irm https://insta.rs/install.ps1 | iex"
|
||||
```
|
||||
|
||||
Usual flow is to first run the test, then review them.
|
||||
```
|
||||
cargo insta test
|
||||
cargo insta review
|
||||
```
|
||||
|
||||
## Pull requests
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
|
||||
Generated
+550
-986
File diff suppressed because it is too large
Load Diff
+52
-42
@@ -18,45 +18,49 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.82"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.93"
|
||||
anyhow = "1.0.95"
|
||||
aquamarine = "0.6.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.83"
|
||||
as_variant = "1.2.0"
|
||||
async-trait = "0.1.85"
|
||||
as_variant = "1.3.0"
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.38"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.7.0"
|
||||
eyeball-im = { version = "0.7.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.9.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.21"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
getrandom = { version = "0.2.15", default-features = false }
|
||||
gloo-timers = "0.3.0"
|
||||
growable-bloom-filter = "2.1.1"
|
||||
hkdf = "0.12.4"
|
||||
hmac = "0.12.1"
|
||||
http = "1.1.0"
|
||||
imbl = "3.0.0"
|
||||
indexmap = "2.6.0"
|
||||
itertools = "0.13.0"
|
||||
http = "1.2.0"
|
||||
imbl = "5.0.0"
|
||||
indexmap = "2.7.1"
|
||||
insta = { version = "1.42.1", features = ["json", "redactions"] }
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.69"
|
||||
mime = "0.3.17"
|
||||
once_cell = "1.20.2"
|
||||
pbkdf2 = { version = "0.12.2" }
|
||||
pin-project-lite = "0.2.15"
|
||||
proptest = { version = "1.5.0", default-features = false, features = ["std"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
ruma = { version = "0.12.0", features = [
|
||||
# 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.2", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -71,17 +75,17 @@ ruma = { version = "0.12.0", features = [
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
ruma-common = "0.15.0"
|
||||
serde = "1.0.151"
|
||||
serde_html_form = "0.2.0"
|
||||
serde_json = "1.0.91"
|
||||
ruma-common = "0.15.2"
|
||||
serde = "1.0.217"
|
||||
serde_html_form = "0.2.7"
|
||||
serde_json = "1.0.138"
|
||||
sha2 = "0.10.8"
|
||||
similar-asserts = "1.6.0"
|
||||
similar-asserts = "1.6.1"
|
||||
stream_assert = "0.1.1"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "2.0.3"
|
||||
tokio = { version = "1.41.1", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tempfile = "3.16.0"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
tracing-subscriber = "0.3.18"
|
||||
@@ -89,29 +93,25 @@ unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.4"
|
||||
uuid = "1.11.0"
|
||||
vodozemac = { version = "0.8.1", features = ["insecure-pk-encryption"] }
|
||||
uuid = "1.12.1"
|
||||
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
|
||||
wasm-bindgen = "0.2.84"
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
web-sys = "0.3.69"
|
||||
wiremock = "0.6.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.9.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.9.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.9.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.9.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.9.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.9.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.9.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.9.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`
|
||||
@@ -124,6 +124,9 @@ debug = 0
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
sha2 = { opt-level = 2 }
|
||||
# faster runs for insta.rs snapshot testing
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
# Custom profile with full debugging info, use `--profile dbg` to select
|
||||
[profile.dbg]
|
||||
@@ -136,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,42 +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 in an alpha state, things that are implemented generally work but
|
||||
the API will change in breaking ways.
|
||||
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.
|
||||
|
||||
If you are interested in using the matrix-sdk now is the time to try it out and
|
||||
provide feedback.
|
||||
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
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Upgrades 0.5 ➜ 0.6
|
||||
|
||||
This is a rough migration guide to help you upgrade your code using matrix-sdk 0.5 to the newly released matrix-sdk 0.6 . While it won't cover all edge cases and problems, we are trying to get the most common issues covered. If you experience any other difficulties in upgrade or need support with using the matrix-sdk in general, please approach us in our [matrix-sdk channel on matrix.org][matrix-channel].
|
||||
|
||||
## Minimum Supported Rust Version Update: `1.60`
|
||||
|
||||
We have updated the minimal rust version you need in order to build `matrix-sdk`, as we require some new dependency resolving features from it:
|
||||
|
||||
> These crates are built with the Rust language version 2021 and require a minimum compiler version of 1.60
|
||||
|
||||
## Dependencies
|
||||
|
||||
Many dependencies have been upgraded. Most notably, we are using `ruma` at version `0.7.0` now. It has seen some renamings and restructurings since our last release, so you might find that some Types have new names now.
|
||||
|
||||
## Repo Structure Updates
|
||||
|
||||
If you are looking at the repository itself, you will find we've rearranged the code quite a bit: we have split out any bindings-specific and testing related crates (and other things) into respective folders, and we've moved all `examples` into its own top-level-folder with each example as their own crate (rendering them easier to find and copy as starting points), all in all slimming down the `crates` folder to the core aspects.
|
||||
|
||||
|
||||
## Architecture Changes / API overall
|
||||
|
||||
### Builder Pattern
|
||||
|
||||
We are moving to the [builder pattern][] (familiar from e.g. `std::io:process:Command`) as the main configurable path for many aspects of the API, including to construct Matrix-Requests and workflows. This has been and is an on-going effort, and this release sees a lot of APIs transitioning to this pattern, you should already be familiar with from the `matrix_sdk::Client::builder()` in `0.5`. This pattern been extended onto:
|
||||
- the [login configuration][login builder] and [login with sso][ssologin builder],
|
||||
- [`SledStore` configuratiion][sled-store builder]
|
||||
- [`Indexeddb` configuration][indexeddb builder]
|
||||
|
||||
Most have fallback (though maybe with deprecation warning) support for an existing code path, but these are likely to be removed in upcoming releases.
|
||||
|
||||
### Splitting of concerns: Media
|
||||
|
||||
In an effort to declutter the `Client` API dedicated types have been created dealing with specific concerns in one place. In `0.5` we introduced `client.account()`, and `client.encryption()`, we are doing the same with `client.media()` to manage media and attachments in one place with the [`media::Media` type][media typ] now.
|
||||
|
||||
The signatures of media uploads, have also changed slightly: rather than expecting a reader `R: Read + Seek`, it now is a simple `&[u8]`. Which also means no more unnecessary `seek(0)` to reset the cursor, as we are just taking an immutable reference now.
|
||||
|
||||
### Event Handling & sync updaes
|
||||
|
||||
If you are using the `client.register_event_handler` function to receive updates on incoming sync events, you'll find yourself with a deprecation warning now. That is because we've refactored and redesigned the event handler logic to allowing `removing` of event handlers on the fly, too. For that the new `add_event_handler()` (and `add_room_event_handler`) will hand you an `EventHandlerHandle` (pardon the pun), which you can pass to `remove_event_handler`, or by using the convenient `client.event_handler_drop_guard` to create a `DropGuard` that will remove the handler when the guard is dropped. While the code still works, we recommend you switch to the new one, as we will be removing the `register_event_handler` and `register_event_handler_context` in a coming release.
|
||||
|
||||
Secondly, you will find a new [`sync_with_result_callback` sync function][sync with result]. Other than the previous sync functions, this will pass the entire `Result` to your callback, allowing you to handle errors or even raise some yourself to stop the loop. Further more, it will propagate any unhandled errors (it still handles retries as before) to the outer caller, allowing the higher level to decide how to handle that (e.g. in case of a network failure). This result-returning-behavior also punshes through the existing `sync` and `sync_with_callback`-API, allowing you to handle them on a higher level now (rather than the futures just resolving). If you find that warning, just adding a `?` to the `.await` of the call is probably the quickest way to move forward.
|
||||
|
||||
### Refresh Tokens
|
||||
|
||||
This release now [supports `refresh_token`s][refresh tokens PR] as part of the [`Session`][session]. It is implemented with a default-flag in serde so deserializing a previously serialized Session (e.g. in a store) will work as before. As part of `refresh_token` support, you can now configure the client via `ClientBuilder.request_refresh_token()` to refresh the access token automagically on certain failures or do it manually by calling `client.refresh_access_token()` yourself. Auto-refresh is _off_ by default.
|
||||
|
||||
You can stay informed about updates on the access token by listening to `client.session_tokens_signal()`.
|
||||
|
||||
### Further changes
|
||||
|
||||
- [`MessageOptions`][message options] has been updated to Matrix 1.3 by making the `from` parameter optional (and function signatures have been updated, too). You can now request the server sends you messages from the first one you are allowed to have received.
|
||||
- `client.user_id()` is not a `future` anymore. Remove any `.await` you had behind it.
|
||||
- `verified()`, `blacklisted()` and `deleted()` on `matrix_sdk::encryption::identities::Device` have been renamed with a `is_` prefix.
|
||||
- `verified()` on `matrix_sdk::encryption::identities::UserIdentity`, too has been prefixed with `is_` and thus is now called `is_verified()`.
|
||||
- The top-level crypto and state-store types of Indexeddb and Sled have been renamed to unique types>
|
||||
- `state_store` and `crypto_store` do not need to be boxed anymore when passed to the [`StoreConfig`][store config]
|
||||
- Indexeddb's `SerializationError` is now `IndexedDBStoreError`
|
||||
- Javascript specific features are now behind the `js` feature-gate
|
||||
- The new experimental next generation of sync ("sliding sync"), with a totally revamped api, can be found behind the optional `sliding-sync`-feature-gate
|
||||
|
||||
|
||||
## Quick Troubleshooting
|
||||
|
||||
You find yourself focused with any of these, here are the steps to follow to upgrade your code accordingly:
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::register_event_handler`: Use [`Client::add_event_handler`](#method.add_event_handler) instead
|
||||
|
||||
As it says on the tin: we have deprecated this function in favor of the newer removable handler approach (see above). You can still continue to use this `fn` for now, but it will be removed in a future release.
|
||||
|
||||
### warning: use of deprecated associated function `matrix_sdk::Client::login`: Replaced by [`Client::login_username`](#method.login_username)
|
||||
|
||||
We have replaced the login facilities with a `LoginBuilder` and recommend you use that from now on. This isn't an error yet, but the function will be removed in a future release.
|
||||
|
||||
### expected slice `[u8]`, found struct ...
|
||||
|
||||
We've updated the `send_attachment` and `Media` signatures to use `&[u8]` rather than `reader: Read + Seek` as it is more convenient and common place for most architectures anyways. If you are using `File::open(path)?` to get that handler, you can just replace that with `std::fs::read(path)?`
|
||||
|
||||
### no method named `verified` found for struct `matrix_sdk::encryption::identities::Device` in the current scope
|
||||
|
||||
Boolean flags like `verified`, `deleted`, `blacklisted`, etc have been renamed with a `is_` prefix. So, just follow the cargo suggestion:
|
||||
```
|
||||
|
|
||||
69 | device.verified()
|
||||
| ^^^^^^^^ help: there is an associated function with a similar name: `is_verified`
|
||||
```
|
||||
|
||||
### unresolved import `matrix_sdk::ruma::events::AnySyncRoomEvent`
|
||||
|
||||
Ruma has been updated to `0.7.0`, you will find some ruma Events names have changed, most notably, the `AnySyncRoomEvent` is now named `AnySyncTimelineEvent` (and not `AnySyncStateEvent`, which cargo wrongly suggests). Just rename the import and usage of it.
|
||||
|
||||
### `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
||||
You are seeing something along the lines of:
|
||||
```
|
||||
19 | if room_member.state_key != client.user_id().await.unwrap() {
|
||||
| ^^^^^^ `std::option::Option<&matrix_sdk::ruma::UserId>` is not a future
|
||||
|
|
||||
= help: the trait `Future` is not implemented for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
= note: std::option::Option<&matrix_sdk::ruma::UserId> must be a future or must implement `IntoFuture` to be awaited
|
||||
= note: required because of the requirements on the impl of `IntoFuture` for `std::option::Option<&matrix_sdk::ruma::UserId>`
|
||||
help: remove the `.await`
|
||||
|
|
||||
19 - if room_member.state_key != client.user_id().await.unwrap() {
|
||||
19 + if room_member.state_key != client.user_id().unwrap() {
|
||||
```
|
||||
|
||||
You are using `client.user_id().await` but `user_id()` is no longer `async`. Just follow the cargo suggestion and remove the `.await`, it is not necessary any longer.
|
||||
|
||||
|
||||
[matrix-channel]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
|
||||
[builder pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
|
||||
[login builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.LoginBuilder.html
|
||||
[ssologin builder]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.SsoLoginBuilder.html
|
||||
[sled-store builder]: https://docs.rs/matrix-sdk-sled/latest/matrix_sdk_sled/struct.SledStateStoreBuilder.html
|
||||
[indexeddb builder]: https://docs.rs/matrix-sdk-indexeddb/latest/matrix_sdk_indexeddb/struct.IndexeddbStateStoreBuilder.html
|
||||
[media type]: https://docs.rs/matrix-sdk/latest/matrix_sdk//media/struct.Media.html
|
||||
[sync with result]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Client.html#method.sync_with_result_callback
|
||||
[session]: https://docs.rs/matrix-sdk/latest/matrix_sdk/struct.Session.html
|
||||
[refresh tokens PR]: https://github.com/matrix-org/matrix-rust-sdk/pull/892
|
||||
[store config]: https://docs.rs/matrix-sdk-base/latest/matrix_sdk_base/store/struct.StoreConfig.html
|
||||
[message options]: https://docs.rs/matrix-sdk/latest/matrix_sdk/room/struct.MessagesOptions.html
|
||||
@@ -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,33 +1,24 @@
|
||||
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, utils::IntoRawStateEventContent,
|
||||
};
|
||||
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, EventBuilder, 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,
|
||||
device_id,
|
||||
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
|
||||
owned_room_id, owned_user_id,
|
||||
events::room::member::{MembershipState, RoomMemberEvent},
|
||||
mxc_uri, owned_room_id, owned_user_id,
|
||||
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;
|
||||
@@ -35,28 +26,17 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let ev_builder = EventBuilder::new();
|
||||
let f = EventFactory::new().room(&room_id);
|
||||
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
|
||||
let member_content_json = json!({
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support",
|
||||
});
|
||||
let member_content: Raw<RoomMemberEventContent> =
|
||||
member_content_json.into_raw_state_event_content().cast();
|
||||
for i in 0..MEMBERS_IN_ROOM {
|
||||
let user_id = OwnedUserId::try_from(format!("@user_{}:matrix.org", i)).unwrap();
|
||||
let state_key = user_id.to_string();
|
||||
let event: Raw<RoomMemberEvent> = ev_builder
|
||||
.make_state_event(
|
||||
&user_id,
|
||||
&room_id,
|
||||
&state_key,
|
||||
member_content.deserialize().unwrap(),
|
||||
None,
|
||||
)
|
||||
.cast();
|
||||
let event = f
|
||||
.member(&user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.avatar_url(mxc_uri!("mxc://example.org/SEsfnsuifSDFSSEF"))
|
||||
.display_name("Alice Margatroid")
|
||||
.reason("Looking for support")
|
||||
.into_raw();
|
||||
member_events.push(event);
|
||||
}
|
||||
|
||||
@@ -75,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");
|
||||
@@ -123,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);
|
||||
|
||||
@@ -147,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];
|
||||
@@ -171,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);
|
||||
@@ -185,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();
|
||||
@@ -201,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 {
|
||||
@@ -220,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::{
|
||||
config::StoreConfig,
|
||||
matrix_auth::{MatrixSession, MatrixSessionTokens},
|
||||
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);
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::{env, error::Error, path::PathBuf, process::Command};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
@@ -39,7 +44,7 @@ fn setup_x86_64_android_workaround() {
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &PathBuf) -> String {
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
#[uniffi(flat_error)]
|
||||
pub enum DehydrationError {
|
||||
#[error(transparent)]
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
Pickle(#[from] matrix_sdk_crypto::vodozemac::DehydratedDeviceError),
|
||||
#[error(transparent)]
|
||||
LegacyPickle(#[from] matrix_sdk_crypto::vodozemac::LibolmPickleError),
|
||||
#[error(transparent)]
|
||||
MissingSigningKey(#[from] matrix_sdk_crypto::SignatureError),
|
||||
#[error(transparent)]
|
||||
@@ -35,6 +37,9 @@ impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for Dehydrati
|
||||
match value {
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Json(e) => Self::Json(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Pickle(e) => Self::Pickle(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::LegacyPickle(e) => {
|
||||
Self::LegacyPickle(e)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::MissingSigningKey(e) => {
|
||||
Self::MissingSigningKey(e)
|
||||
}
|
||||
|
||||
@@ -507,6 +507,7 @@ fn collect_sessions(
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
shared_history: false,
|
||||
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
};
|
||||
|
||||
@@ -680,15 +681,20 @@ pub struct EncryptionSettings {
|
||||
|
||||
impl From<EncryptionSettings> for RustEncryptionSettings {
|
||||
fn from(v: EncryptionSettings) -> Self {
|
||||
let sharing_strategy = if v.only_allow_trusted_devices {
|
||||
CollectStrategy::OnlyTrustedDevices
|
||||
} else if v.error_on_verified_user_problem {
|
||||
CollectStrategy::ErrorOnVerifiedUserProblem
|
||||
} else {
|
||||
CollectStrategy::AllDevices
|
||||
};
|
||||
|
||||
RustEncryptionSettings {
|
||||
algorithm: v.algorithm.into(),
|
||||
rotation_period: Duration::from_secs(v.rotation_period),
|
||||
rotation_period_msgs: v.rotation_period_msgs,
|
||||
history_visibility: v.history_visibility.into(),
|
||||
sharing_strategy: CollectStrategy::DeviceBasedStrategy {
|
||||
only_allow_trusted_devices: v.only_allow_trusted_devices,
|
||||
error_on_verified_user_problem: v.error_on_verified_user_problem,
|
||||
},
|
||||
sharing_strategy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,8 +791,7 @@ impl VerificationRequest {
|
||||
// task.
|
||||
let should_break = matches!(
|
||||
state,
|
||||
RustVerificationRequestState::Done { .. }
|
||||
| RustVerificationRequestState::Cancelled { .. }
|
||||
RustVerificationRequestState::Done | RustVerificationRequestState::Cancelled { .. }
|
||||
);
|
||||
|
||||
let state = Self::convert_verification_request(&request, state);
|
||||
|
||||
@@ -9,6 +9,7 @@ readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.7.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -22,3 +23,6 @@ syn = { version = "2.0.43", features = ["full", "extra-traits"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
# 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.
|
||||
|
||||
- `EventSendState` now has two additional variants: `CrossSigningNotSetup` and
|
||||
`SendingFromUnverifiedDevice`. These indicate that your own device is not
|
||||
properly cross-signed, which is a requirement when using the identity-based
|
||||
@@ -26,11 +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,8 +56,6 @@ workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"rustls-tls", # note: differ from block below
|
||||
@@ -70,8 +69,6 @@ workspace = true
|
||||
features = [
|
||||
"anyhow",
|
||||
"e2e-encryption",
|
||||
"experimental-oidc",
|
||||
"experimental-sliding-sync",
|
||||
"experimental-widgets",
|
||||
"markdown",
|
||||
"native-tls", # note: differ from block above
|
||||
@@ -84,4 +81,4 @@ features = [
|
||||
workspace = true
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
release = true
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::{env, error::Error, path::PathBuf, process::Command};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
@@ -39,7 +44,7 @@ fn setup_x86_64_android_workaround() {
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &PathBuf) -> String {
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
|
||||
@@ -8,8 +8,3 @@ dictionary Mentions {
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
@@ -5,18 +5,14 @@ use std::{
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
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::oauth::{
|
||||
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
|
||||
},
|
||||
event_cache::EventCacheError,
|
||||
media::{
|
||||
MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters,
|
||||
MediaThumbnailSettings,
|
||||
MediaRetentionPolicy, MediaThumbnailSettings,
|
||||
},
|
||||
oidc::{
|
||||
registrations::{ClientId, OidcRegistrations},
|
||||
requests::account_management::AccountManagementActionFull,
|
||||
types::{
|
||||
client_credentials::ClientCredentials,
|
||||
registration::{
|
||||
ClientMetadata, ClientMetadataVerificationError, VerifiedClientMetadata,
|
||||
},
|
||||
requests::Prompt as SdkOidcPrompt,
|
||||
},
|
||||
OidcAuthorizationData, OidcSession,
|
||||
},
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1153,15 +1167,40 @@ impl Client {
|
||||
self.inner.is_room_alias_available(&alias).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Creates a new room alias associated with the provided room id.
|
||||
pub async fn create_room_alias(
|
||||
/// Set the media retention policy.
|
||||
pub async fn set_media_retention_policy(
|
||||
&self,
|
||||
room_alias: String,
|
||||
room_id: String,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), ClientError> {
|
||||
let room_alias = RoomAliasId::parse(room_alias)?;
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
self.inner.create_room_alias(&room_alias, &room_id).await.map_err(Into::into)
|
||||
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?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,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);
|
||||
}
|
||||
@@ -1262,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> {
|
||||
@@ -1290,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,
|
||||
@@ -1389,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>,
|
||||
}
|
||||
|
||||
@@ -1436,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 {
|
||||
@@ -1462,6 +1535,9 @@ pub enum RoomVisibility {
|
||||
|
||||
/// Indicates that the room will not be shown in the published room list.
|
||||
Private,
|
||||
|
||||
/// A custom value that's not present in the spec.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl From<RoomVisibility> for Visibility {
|
||||
@@ -1469,6 +1545,17 @@ impl From<RoomVisibility> for Visibility {
|
||||
match value {
|
||||
RoomVisibility::Public => Self::Public,
|
||||
RoomVisibility::Private => Self::Private,
|
||||
RoomVisibility::Custom { value } => value.as_str().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Visibility> for RoomVisibility {
|
||||
fn from(value: Visibility) -> Self {
|
||||
match value {
|
||||
Visibility::Public => Self::Public,
|
||||
Visibility::Private => Self::Private,
|
||||
_ => Self::Custom { value: value.as_str().to_owned() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1532,10 +1619,9 @@ impl Session {
|
||||
match auth_api {
|
||||
// Build the session from the regular Matrix Auth Session.
|
||||
AuthApi::Matrix(a) => {
|
||||
let matrix_sdk::matrix_auth::MatrixSession {
|
||||
let matrix_sdk::authentication::matrix::MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::matrix_auth::MatrixSessionTokens { access_token, refresh_token },
|
||||
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
|
||||
} = a.session().context("Missing session")?;
|
||||
|
||||
Ok(Session {
|
||||
@@ -1549,30 +1635,13 @@ impl Session {
|
||||
})
|
||||
}
|
||||
// Build the session from the OIDC UserSession.
|
||||
AuthApi::Oidc(api) => {
|
||||
let matrix_sdk::oidc::UserSession {
|
||||
AuthApi::OAuth(api) => {
|
||||
let matrix_sdk::authentication::oauth::UserSession {
|
||||
meta: matrix_sdk::SessionMeta { user_id, device_id },
|
||||
tokens:
|
||||
matrix_sdk::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 {
|
||||
@@ -1588,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 {
|
||||
@@ -1605,46 +1678,27 @@ 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::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::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::matrix_auth::MatrixSession {
|
||||
let session = matrix_sdk::authentication::matrix::MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta {
|
||||
user_id: user_id.try_into()?,
|
||||
device_id: device_id.into(),
|
||||
},
|
||||
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
|
||||
access_token,
|
||||
refresh_token,
|
||||
},
|
||||
tokens: matrix_sdk::SessionTokens { access_token, refresh_token },
|
||||
};
|
||||
|
||||
Ok(AuthSession::Matrix(session))
|
||||
@@ -1654,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)]
|
||||
@@ -1729,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,
|
||||
}
|
||||
@@ -1792,7 +1795,6 @@ impl MediaFileHandle {
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SlidingSyncVersion {
|
||||
None,
|
||||
Proxy { url: String },
|
||||
Native,
|
||||
}
|
||||
|
||||
@@ -1800,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,
|
||||
}
|
||||
}
|
||||
@@ -1812,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,
|
||||
})
|
||||
}
|
||||
@@ -1822,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.
|
||||
@@ -1834,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::PathBuf, sync::Arc, time::Duration};
|
||||
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);
|
||||
@@ -509,8 +579,8 @@ impl ClientBuilder {
|
||||
}
|
||||
|
||||
if let Some(session_paths) = &builder.session_paths {
|
||||
let data_path = PathBuf::from(&session_paths.data_path);
|
||||
let cache_path = PathBuf::from(&session_paths.cache_path);
|
||||
let data_path = Path::new(&session_paths.data_path);
|
||||
let cache_path = Path::new(&session_paths.cache_path);
|
||||
|
||||
debug!(
|
||||
data_path = %data_path.to_string_lossy(),
|
||||
@@ -518,14 +588,32 @@ impl ClientBuilder {
|
||||
"Creating directories for data and cache stores.",
|
||||
);
|
||||
|
||||
fs::create_dir_all(&data_path)?;
|
||||
fs::create_dir_all(&cache_path)?;
|
||||
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());
|
||||
}
|
||||
@@ -281,7 +281,7 @@ impl Encryption {
|
||||
}
|
||||
|
||||
pub async fn is_last_device(&self) -> Result<bool> {
|
||||
Ok(self.inner.recovery().are_we_the_last_man_standing().await?)
|
||||
Ok(self.inner.recovery().is_last_device().await?)
|
||||
}
|
||||
|
||||
pub async fn wait_for_backup_upload_steady_state(
|
||||
@@ -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());
|
||||
}
|
||||
@@ -495,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)]
|
||||
@@ -548,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() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,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,20 +1,23 @@
|
||||
use std::{collections::HashMap, fmt, fmt::Display};
|
||||
use std::{collections::HashMap, fmt, fmt::Display, time::SystemTime};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest,
|
||||
room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
authentication::oauth::OAuthError, encryption::CryptoStoreError, event_cache::EventCacheError,
|
||||
reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use ruma::api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::room_list::RoomListError;
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum ClientError {
|
||||
#[error("client error: {msg}")]
|
||||
Generic { msg: String },
|
||||
#[error("api error {code}: {msg}")]
|
||||
MatrixApi { kind: ErrorKind, code: String, msg: String },
|
||||
}
|
||||
|
||||
impl ClientError {
|
||||
@@ -43,7 +46,22 @@ impl From<UnexpectedUniFFICallbackError> for ClientError {
|
||||
|
||||
impl From<matrix_sdk::Error> for ClientError {
|
||||
fn from(e: matrix_sdk::Error) -> Self {
|
||||
Self::new(e)
|
||||
match e {
|
||||
matrix_sdk::Error::Http(http_error) => {
|
||||
if let Some(api_error) = http_error.as_client_api_error() {
|
||||
if let ErrorBody::Standard { kind, message } = &api_error.body {
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return Self::Generic { msg: message.to_string() };
|
||||
};
|
||||
return Self::MatrixApi { kind, code, msg: message.to_owned() };
|
||||
}
|
||||
}
|
||||
Self::Generic { msg: http_error.to_string() }
|
||||
}
|
||||
_ => Self::Generic { msg: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,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)
|
||||
}
|
||||
}
|
||||
@@ -155,6 +173,18 @@ impl From<RoomSendQueueError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NotYetImplemented> for ClientError {
|
||||
fn from(_: NotYetImplemented) -> Self {
|
||||
Self::new("This functionality is not implemented yet.")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FocusEventError> for ClientError {
|
||||
fn from(e: FocusEventError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
|
||||
/// String.
|
||||
///
|
||||
@@ -258,6 +288,8 @@ pub enum RoomError {
|
||||
TimelineUnavailable,
|
||||
#[error("Invalid thumbnail data")]
|
||||
InvalidThumbnailData,
|
||||
#[error("Invalid replied to event ID")]
|
||||
InvalidRepliedToEventId,
|
||||
#[error("Failed sending attachment")]
|
||||
FailedSendingAttachment,
|
||||
}
|
||||
@@ -321,3 +353,439 @@ impl From<matrix_sdk::Error> for NotificationSettingsError {
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("not implemented yet")]
|
||||
pub struct NotYetImplemented;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, uniffi::Enum)]
|
||||
// Please keep the variants sorted alphabetically.
|
||||
pub enum ErrorKind {
|
||||
/// `M_BAD_ALIAS`
|
||||
///
|
||||
/// One or more [room aliases] within the `m.room.canonical_alias` event do
|
||||
/// not point to the room ID for which the state event is to be sent to.
|
||||
///
|
||||
/// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
BadAlias,
|
||||
|
||||
/// `M_BAD_JSON`
|
||||
///
|
||||
/// The request contained valid JSON, but it was malformed in some way, e.g.
|
||||
/// missing required keys, invalid values for keys.
|
||||
BadJson,
|
||||
|
||||
/// `M_BAD_STATE`
|
||||
///
|
||||
/// The state change requested cannot be performed, such as attempting to
|
||||
/// unban a user who is not banned.
|
||||
BadState,
|
||||
|
||||
/// `M_BAD_STATUS`
|
||||
///
|
||||
/// The application service returned a bad status.
|
||||
BadStatus {
|
||||
/// The HTTP status code of the response.
|
||||
status: Option<u16>,
|
||||
|
||||
/// The body of the response.
|
||||
body: Option<String>,
|
||||
},
|
||||
|
||||
/// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
|
||||
///
|
||||
/// The user is unable to reject an invite to join the [server notices]
|
||||
/// room.
|
||||
///
|
||||
/// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
|
||||
CannotLeaveServerNoticeRoom,
|
||||
|
||||
/// `M_CANNOT_OVERWRITE_MEDIA`
|
||||
///
|
||||
/// The [`create_content_async`] endpoint was called with a media ID that
|
||||
/// already has content.
|
||||
///
|
||||
/// [`create_content_async`]: crate::media::create_content_async
|
||||
CannotOverwriteMedia,
|
||||
|
||||
/// `M_CAPTCHA_INVALID`
|
||||
///
|
||||
/// The Captcha provided did not match what was expected.
|
||||
CaptchaInvalid,
|
||||
|
||||
/// `M_CAPTCHA_NEEDED`
|
||||
///
|
||||
/// A Captcha is required to complete the request.
|
||||
CaptchaNeeded,
|
||||
|
||||
/// `M_CONNECTION_FAILED`
|
||||
///
|
||||
/// The connection to the application service failed.
|
||||
ConnectionFailed,
|
||||
|
||||
/// `M_CONNECTION_TIMEOUT`
|
||||
///
|
||||
/// The connection to the application service timed out.
|
||||
ConnectionTimeout,
|
||||
|
||||
/// `M_DUPLICATE_ANNOTATION`
|
||||
///
|
||||
/// The request is an attempt to send a [duplicate annotation].
|
||||
///
|
||||
/// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
|
||||
DuplicateAnnotation,
|
||||
|
||||
/// `M_EXCLUSIVE`
|
||||
///
|
||||
/// The resource being requested is reserved by an application service, or
|
||||
/// the application service making the request has not created the
|
||||
/// resource.
|
||||
Exclusive,
|
||||
|
||||
/// `M_FORBIDDEN`
|
||||
///
|
||||
/// Forbidden access, e.g. joining a room without permission, failed login.
|
||||
Forbidden,
|
||||
|
||||
/// `M_GUEST_ACCESS_FORBIDDEN`
|
||||
///
|
||||
/// The room or resource does not permit [guests] to access it.
|
||||
///
|
||||
/// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
|
||||
GuestAccessForbidden,
|
||||
|
||||
/// `M_INCOMPATIBLE_ROOM_VERSION`
|
||||
///
|
||||
/// The client attempted to join a room that has a version the server does
|
||||
/// not support.
|
||||
IncompatibleRoomVersion {
|
||||
/// The room's version.
|
||||
room_version: String,
|
||||
},
|
||||
|
||||
/// `M_INVALID_PARAM`
|
||||
///
|
||||
/// A parameter that was specified has the wrong value. For example, the
|
||||
/// server expected an integer and instead received a string.
|
||||
InvalidParam,
|
||||
|
||||
/// `M_INVALID_ROOM_STATE`
|
||||
///
|
||||
/// The initial state implied by the parameters to the [`create_room`]
|
||||
/// request is invalid, e.g. the user's `power_level` is set below that
|
||||
/// necessary to set the room name.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
InvalidRoomState,
|
||||
|
||||
/// `M_INVALID_USERNAME`
|
||||
///
|
||||
/// The desired user name is not valid.
|
||||
InvalidUsername,
|
||||
|
||||
/// `M_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request has been refused due to [rate limiting]: too many requests
|
||||
/// have been sent in a short period of time.
|
||||
///
|
||||
/// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
|
||||
LimitExceeded {
|
||||
/// How long a client should wait before they can try again.
|
||||
retry_after_ms: Option<u64>,
|
||||
},
|
||||
|
||||
/// `M_MISSING_PARAM`
|
||||
///
|
||||
/// A required parameter was missing from the request.
|
||||
MissingParam,
|
||||
|
||||
/// `M_MISSING_TOKEN`
|
||||
///
|
||||
/// No [access token] was specified for the request, but one is required.
|
||||
///
|
||||
/// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
MissingToken,
|
||||
|
||||
/// `M_NOT_FOUND`
|
||||
///
|
||||
/// No resource was found for this request.
|
||||
NotFound,
|
||||
|
||||
/// `M_NOT_JSON`
|
||||
///
|
||||
/// The request did not contain valid JSON.
|
||||
NotJson,
|
||||
|
||||
/// `M_NOT_YET_UPLOADED`
|
||||
///
|
||||
/// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used
|
||||
/// and the content is not yet available.
|
||||
///
|
||||
/// [`create_mxc_uri`]: crate::media::create_mxc_uri
|
||||
NotYetUploaded,
|
||||
|
||||
/// `M_RESOURCE_LIMIT_EXCEEDED`
|
||||
///
|
||||
/// The request cannot be completed because the homeserver has reached a
|
||||
/// resource limit imposed on it. For example, a homeserver held in a
|
||||
/// shared hosting environment may reach a resource limit if it starts
|
||||
/// using too much memory or disk space.
|
||||
ResourceLimitExceeded {
|
||||
/// A URI giving a contact method for the server administrator.
|
||||
admin_contact: String,
|
||||
},
|
||||
|
||||
/// `M_ROOM_IN_USE`
|
||||
///
|
||||
/// The [room alias] specified in the [`create_room`] request is already
|
||||
/// taken.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
/// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
|
||||
RoomInUse,
|
||||
|
||||
/// `M_SERVER_NOT_TRUSTED`
|
||||
///
|
||||
/// The client's request used a third-party server, e.g. identity server,
|
||||
/// that this server does not trust.
|
||||
ServerNotTrusted,
|
||||
|
||||
/// `M_THREEPID_AUTH_FAILED`
|
||||
///
|
||||
/// Authentication could not be performed on the [third-party identifier].
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidAuthFailed,
|
||||
|
||||
/// `M_THREEPID_DENIED`
|
||||
///
|
||||
/// The server does not permit this [third-party identifier]. This may
|
||||
/// happen if the server only permits, for example, email addresses from
|
||||
/// a particular domain.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidDenied,
|
||||
|
||||
/// `M_THREEPID_IN_USE`
|
||||
///
|
||||
/// The [third-party identifier] is already in use by another user.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidInUse,
|
||||
|
||||
/// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
|
||||
///
|
||||
/// The homeserver does not support adding a [third-party identifier] of the
|
||||
/// given medium.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidMediumNotSupported,
|
||||
|
||||
/// `M_THREEPID_NOT_FOUND`
|
||||
///
|
||||
/// No account matching the given [third-party identifier] could be found.
|
||||
///
|
||||
/// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
|
||||
ThreepidNotFound,
|
||||
|
||||
/// `M_TOO_LARGE`
|
||||
///
|
||||
/// The request or entity was too large.
|
||||
TooLarge,
|
||||
|
||||
/// `M_UNABLE_TO_AUTHORISE_JOIN`
|
||||
///
|
||||
/// The room is [restricted] and none of the conditions can be validated by
|
||||
/// the homeserver. This can happen if the homeserver does not know
|
||||
/// about any of the rooms listed as conditions, for example.
|
||||
///
|
||||
/// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToAuthorizeJoin,
|
||||
|
||||
/// `M_UNABLE_TO_GRANT_JOIN`
|
||||
///
|
||||
/// A different server should be attempted for the join. This is typically
|
||||
/// because the resident server can see that the joining user satisfies
|
||||
/// one or more conditions, such as in the case of [restricted rooms],
|
||||
/// but the resident server would be unable to meet the authorization
|
||||
/// rules.
|
||||
///
|
||||
/// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
|
||||
UnableToGrantJoin,
|
||||
|
||||
/// `M_UNAUTHORIZED`
|
||||
///
|
||||
/// The request was not correctly authorized. Usually due to login failures.
|
||||
Unauthorized,
|
||||
|
||||
/// `M_UNKNOWN`
|
||||
///
|
||||
/// An unknown error has occurred.
|
||||
Unknown,
|
||||
|
||||
/// `M_UNKNOWN_TOKEN`
|
||||
///
|
||||
/// The [access or refresh token] specified was not recognized.
|
||||
///
|
||||
/// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
|
||||
UnknownToken {
|
||||
/// If this is `true`, the client is in a "[soft logout]" state, i.e.
|
||||
/// the server requires re-authentication but the session is not
|
||||
/// invalidated. The client can acquire a new access token by
|
||||
/// specifying the device ID it is already using to the login API.
|
||||
///
|
||||
/// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
|
||||
soft_logout: bool,
|
||||
},
|
||||
|
||||
/// `M_UNRECOGNIZED`
|
||||
///
|
||||
/// The server did not understand the request.
|
||||
///
|
||||
/// This is expected to be returned with a 404 HTTP status code if the
|
||||
/// endpoint is not implemented or a 405 HTTP status code if the
|
||||
/// endpoint is implemented, but the incorrect HTTP method is used.
|
||||
Unrecognized,
|
||||
|
||||
/// `M_UNSUPPORTED_ROOM_VERSION`
|
||||
///
|
||||
/// The request to [`create_room`] used a room version that the server does
|
||||
/// not support.
|
||||
///
|
||||
/// [`create_room`]: crate::room::create_room
|
||||
UnsupportedRoomVersion,
|
||||
|
||||
/// `M_URL_NOT_SET`
|
||||
///
|
||||
/// The application service doesn't have a URL configured.
|
||||
UrlNotSet,
|
||||
|
||||
/// `M_USER_DEACTIVATED`
|
||||
///
|
||||
/// The user ID associated with the request has been deactivated.
|
||||
UserDeactivated,
|
||||
|
||||
/// `M_USER_IN_USE`
|
||||
///
|
||||
/// The desired user ID is already taken.
|
||||
UserInUse,
|
||||
|
||||
/// `M_USER_LOCKED`
|
||||
///
|
||||
/// The account has been [locked] and cannot be used at this time.
|
||||
///
|
||||
/// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
|
||||
UserLocked,
|
||||
|
||||
/// `M_USER_SUSPENDED`
|
||||
///
|
||||
/// The account has been [suspended] and can only be used for limited
|
||||
/// actions at this time.
|
||||
///
|
||||
/// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
|
||||
UserSuspended,
|
||||
|
||||
/// `M_WEAK_PASSWORD`
|
||||
///
|
||||
/// The password was [rejected] by the server for being too weak.
|
||||
///
|
||||
/// [rejected]: https://spec.matrix.org/latest/client-server-api/#notes-on-password-management
|
||||
WeakPassword,
|
||||
|
||||
/// `M_WRONG_ROOM_KEYS_VERSION`
|
||||
///
|
||||
/// The version of the [room keys backup] provided in the request does not
|
||||
/// match the current backup version.
|
||||
///
|
||||
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
|
||||
WrongRoomKeysVersion {
|
||||
/// The currently active backup version.
|
||||
current_version: Option<String>,
|
||||
},
|
||||
|
||||
/// A custom API error.
|
||||
Custom { errcode: String },
|
||||
}
|
||||
|
||||
impl TryFrom<RumaApiErrorKind> for ErrorKind {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RumaApiErrorKind) -> Result<Self, Self::Error> {
|
||||
match &value {
|
||||
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
|
||||
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
|
||||
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
|
||||
RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
|
||||
status: status.map(|code| code.clone().as_u16()),
|
||||
body: body.clone(),
|
||||
}),
|
||||
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
|
||||
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
|
||||
}
|
||||
RumaApiErrorKind::CannotOverwriteMedia => Ok(ErrorKind::CannotOverwriteMedia),
|
||||
RumaApiErrorKind::CaptchaInvalid => Ok(ErrorKind::CaptchaInvalid),
|
||||
RumaApiErrorKind::CaptchaNeeded => Ok(ErrorKind::CaptchaNeeded),
|
||||
RumaApiErrorKind::ConnectionFailed => Ok(ErrorKind::ConnectionFailed),
|
||||
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
|
||||
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
|
||||
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
|
||||
RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
|
||||
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
|
||||
RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
|
||||
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
|
||||
}
|
||||
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
|
||||
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
|
||||
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
|
||||
RumaApiErrorKind::LimitExceeded { retry_after } => {
|
||||
let retry_after_ms = match retry_after {
|
||||
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
|
||||
Some(RetryAfter::DateTime(system_time)) => {
|
||||
let duration = system_time.duration_since(SystemTime::now()).ok();
|
||||
duration.map(|duration| duration.as_millis() as u64)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok(ErrorKind::LimitExceeded { retry_after_ms })
|
||||
}
|
||||
RumaApiErrorKind::MissingParam => Ok(ErrorKind::MissingParam),
|
||||
RumaApiErrorKind::MissingToken => Ok(ErrorKind::MissingToken),
|
||||
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
|
||||
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
|
||||
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
|
||||
RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
|
||||
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
|
||||
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
|
||||
RumaApiErrorKind::ThreepidAuthFailed => Ok(ErrorKind::ThreepidAuthFailed),
|
||||
RumaApiErrorKind::ThreepidDenied => Ok(ErrorKind::ThreepidDenied),
|
||||
RumaApiErrorKind::ThreepidInUse => Ok(ErrorKind::ThreepidInUse),
|
||||
RumaApiErrorKind::ThreepidMediumNotSupported => {
|
||||
Ok(ErrorKind::ThreepidMediumNotSupported)
|
||||
}
|
||||
RumaApiErrorKind::ThreepidNotFound => Ok(ErrorKind::ThreepidNotFound),
|
||||
RumaApiErrorKind::TooLarge => Ok(ErrorKind::TooLarge),
|
||||
RumaApiErrorKind::UnableToAuthorizeJoin => Ok(ErrorKind::UnableToAuthorizeJoin),
|
||||
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
|
||||
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
|
||||
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
|
||||
RumaApiErrorKind::UnknownToken { soft_logout } => {
|
||||
Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
|
||||
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
|
||||
RumaApiErrorKind::UrlNotSet => Ok(ErrorKind::UrlNotSet),
|
||||
RumaApiErrorKind::UserDeactivated => Ok(ErrorKind::UserDeactivated),
|
||||
RumaApiErrorKind::UserInUse => Ok(ErrorKind::UserInUse),
|
||||
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
|
||||
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
|
||||
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
|
||||
RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
|
||||
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
|
||||
}
|
||||
RumaApiErrorKind::_Custom { .. } => {
|
||||
// There is no way to map the extra values since they're private, so we omit
|
||||
// them
|
||||
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
|
||||
}
|
||||
// In any other case, return it as the mapping not being yet implemented
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use ruma::{
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, NotifyType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
@@ -33,8 +34,8 @@ impl TimelineEvent {
|
||||
self.0.sender().to_string()
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> u64 {
|
||||
self.0.origin_server_ts().0.into()
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.0.origin_server_ts().into()
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
|
||||
|
||||
@@ -14,6 +14,7 @@ mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod identity_status_change;
|
||||
mod live_location_share;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
@@ -29,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::{
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 crate::ruma::LocationContent;
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
/// The most recent location content of the user.
|
||||
pub location: LocationContent,
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
pub ts: u64,
|
||||
}
|
||||
/// Details of a users live location share.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LiveLocationShare {
|
||||
/// The user's last known location.
|
||||
pub last_location: LastLocation,
|
||||
/// The live status of the live location share.
|
||||
pub(crate) is_live: bool,
|
||||
/// The user ID of the person sharing their live location.
|
||||
pub user_id: String,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -14,8 +14,11 @@ use tracing_subscriber::{
|
||||
EnvFilter, Layer,
|
||||
};
|
||||
|
||||
use crate::tracing::LogLevel;
|
||||
|
||||
pub fn log_panics() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
|
||||
log_panics::init();
|
||||
}
|
||||
|
||||
@@ -228,12 +231,132 @@ pub struct TracingFileConfiguration {
|
||||
max_files: Option<u64>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
MatrixSdkEventCache,
|
||||
MatrixSdkEventCacheStore,
|
||||
MatrixSdkHttpClient,
|
||||
MatrixSdkOidc,
|
||||
MatrixSdkSendQueue,
|
||||
MatrixSdkSlidingSync,
|
||||
|
||||
// SDK UI modules.
|
||||
MatrixSdkUiTimeline,
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
fn as_str(&self) -> &'static str {
|
||||
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",
|
||||
LogTarget::MatrixSdkCryptoAccount => "matrix_sdk_crypto::olm::account",
|
||||
LogTarget::MatrixSdkOidc => "matrix_sdk::oidc",
|
||||
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
|
||||
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
|
||||
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
|
||||
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
|
||||
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
|
||||
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
|
||||
(LogTarget::Hyper, LogLevel::Warn),
|
||||
(LogTarget::MatrixSdkFfi, LogLevel::Info),
|
||||
(LogTarget::MatrixSdk, LogLevel::Info),
|
||||
(LogTarget::MatrixSdkClient, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkCrypto, LogLevel::Debug),
|
||||
(LogTarget::MatrixSdkCryptoAccount, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkOidc, LogLevel::Trace),
|
||||
(LogTarget::MatrixSdkHttpClient, LogLevel::Debug),
|
||||
(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_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 {
|
||||
/// A filter line following the [RUST_LOG format].
|
||||
/// The desired log level.
|
||||
log_level: LogLevel,
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// [RUST_LOG format]: https://rust-lang-nursery.github.io/rust-cookbook/development_tools/debugging/config_log.html
|
||||
filter: String,
|
||||
/// 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,
|
||||
@@ -242,12 +365,222 @@ pub struct TracingConfiguration {
|
||||
write_to_files: Option<TracingFileConfiguration>,
|
||||
}
|
||||
|
||||
fn build_tracing_filter(config: &TracingConfiguration) -> String {
|
||||
// We are intentionally not setting a global log level because we don't want to
|
||||
// risk third party crates logging sensitive information.
|
||||
// As such we need to make sure that panics will be properly logged.
|
||||
// On 2025-01-08, `log_panics` uses the `panic` target, at the error log level.
|
||||
let mut filters = vec!["panic=error".to_owned()];
|
||||
|
||||
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 {
|
||||
// Otherwise, use the global level.
|
||||
global_level
|
||||
};
|
||||
|
||||
filters.push(format!("{}={}", target.as_str(), 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(&config.filter))
|
||||
.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,
|
||||
trace_log_packs: Vec::new(),
|
||||
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,
|
||||
"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=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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trace_tracing_filter() {
|
||||
let config = super::TracingConfiguration {
|
||||
log_level: super::LogLevel::Trace,
|
||||
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,
|
||||
};
|
||||
|
||||
let filter = build_tracing_filter(&config);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
"panic=error,\
|
||||
hyper=warn,\
|
||||
matrix_sdk_ffi=info,\
|
||||
matrix_sdk=info,\
|
||||
matrix_sdk::client=trace,\
|
||||
matrix_sdk_crypto=trace,\
|
||||
matrix_sdk_crypto::olm::account=trace,\
|
||||
matrix_sdk::oidc=trace,\
|
||||
matrix_sdk::http_client=trace,\
|
||||
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("")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+378
-171
@@ -1,26 +1,27 @@
|
||||
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,
|
||||
event_cache::paginator::PaginatorError,
|
||||
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, PaginationError, RoomExt, TimelineFocus};
|
||||
use matrix_sdk_ui::timeline::{default_event_filter, RoomExt};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::{
|
||||
call::notify,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
message::RoomMessageEventContentWithoutRelation,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
@@ -28,18 +29,22 @@ use ruma::{
|
||||
EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
room_info::RoomInfo,
|
||||
room_member::RoomMember,
|
||||
ruma::{ImageInfo, Mentions, NotifyType},
|
||||
timeline::{DateDividerMode, FocusEventError, ReceiptType, SendHandle, Timeline},
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
ruma::{ImageInfo, LocationContent, Mentions, NotifyType},
|
||||
timeline::{
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
ReceiptType, SendHandle, Timeline,
|
||||
},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
@@ -85,10 +90,6 @@ impl Room {
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Room {
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
/// Returns the room's name from the state event if available, otherwise
|
||||
/// compute a room name based on the room's nature (DM or not) and number of
|
||||
/// members.
|
||||
@@ -109,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 {
|
||||
@@ -160,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.
|
||||
///
|
||||
@@ -198,117 +184,70 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a timeline focused on the given event.
|
||||
///
|
||||
/// Note: this timeline is independent from that returned with
|
||||
/// [`Self::timeline`], and as such it is not cached.
|
||||
pub async fn timeline_focused_on_event(
|
||||
/// Build a new timeline instance with the given configuration.
|
||||
pub async fn timeline_with_configuration(
|
||||
&self,
|
||||
event_id: String,
|
||||
num_context_events: u16,
|
||||
internal_id_prefix: Option<String>,
|
||||
) -> Result<Arc<Timeline>, FocusEventError> {
|
||||
let parsed_event_id = EventId::parse(&event_id).map_err(|err| {
|
||||
FocusEventError::InvalidEventId { event_id: event_id.clone(), err: err.to_string() }
|
||||
})?;
|
||||
|
||||
let room = &self.inner;
|
||||
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
let timeline = match builder
|
||||
.with_focus(TimelineFocus::Event { target: parsed_event_id, num_context_events })
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
if let matrix_sdk_ui::timeline::Error::PaginationError(
|
||||
PaginationError::Paginator(PaginatorError::EventNotFound(..)),
|
||||
) = err
|
||||
{
|
||||
return Err(FocusEventError::EventNotFound { event_id: event_id.to_string() });
|
||||
}
|
||||
return Err(FocusEventError::Other { msg: err.to_string() });
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub async fn pinned_events_timeline(
|
||||
&self,
|
||||
internal_id_prefix: Option<String>,
|
||||
max_events_to_load: u16,
|
||||
max_concurrent_requests: u16,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let room = &self.inner;
|
||||
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder.with_internal_id_prefix(internal_id_prefix);
|
||||
}
|
||||
|
||||
let timeline = builder
|
||||
.with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
/// A timeline instance that can be configured to only include RoomMessage
|
||||
/// type events and filter those further based on their message type.
|
||||
///
|
||||
/// Virtual timeline items will still be provided and the
|
||||
/// `default_event_filter` will be applied before everything else.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `internal_id_prefix` - An optional String that will be prepended to
|
||||
/// all the timeline item's internal IDs, making it possible to
|
||||
/// distinguish different timeline instances from each other.
|
||||
///
|
||||
/// * `allowed_message_types` - A list of `RoomMessageEventMessageType` that
|
||||
/// will be allowed to appear in the timeline
|
||||
pub async fn message_filtered_timeline(
|
||||
&self,
|
||||
internal_id_prefix: Option<String>,
|
||||
allowed_message_types: Vec<RoomMessageEventMessageType>,
|
||||
date_divider_mode: DateDividerMode,
|
||||
configuration: TimelineConfiguration,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
if configuration.track_read_receipts {
|
||||
builder = builder.track_read_marker_and_receipts();
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(date_divider_mode.into());
|
||||
|
||||
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)) => {
|
||||
allowed_message_types.contains(&content.msgtype.into())
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
|
||||
let timeline = builder.build().await?;
|
||||
|
||||
Ok(Timeline::new(timeline))
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> Result<bool, ClientError> {
|
||||
Ok(RUNTIME.block_on(self.inner.is_encrypted())?)
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.room_id().to_string()
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -322,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)
|
||||
@@ -338,14 +277,30 @@ 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> {
|
||||
Ok(RoomInfo::new(&self.inner).await?)
|
||||
RoomInfo::new(&self.inner).await
|
||||
}
|
||||
|
||||
pub fn subscribe_to_room_info_updates(
|
||||
@@ -353,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),
|
||||
@@ -445,20 +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,
|
||||
reason,
|
||||
),
|
||||
None,
|
||||
.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(())
|
||||
}
|
||||
|
||||
@@ -642,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 {
|
||||
@@ -653,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
|
||||
@@ -924,13 +892,15 @@ impl Room {
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn KnockRequestsListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let stream = self.inner.subscribe_to_knock_requests().await?;
|
||||
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());
|
||||
}
|
||||
// Cancel the seen ids cleanup task
|
||||
seen_ids_cleanup_handle.abort();
|
||||
})));
|
||||
|
||||
Ok(handle)
|
||||
@@ -942,6 +912,184 @@ impl Room {
|
||||
let (cache, _drop_guards) = self.inner.event_cache().await?;
|
||||
Ok(cache.debug_string().await)
|
||||
}
|
||||
|
||||
/// Update the canonical alias of the room.
|
||||
///
|
||||
/// Note that publishing the alias in the room directory is done separately.
|
||||
pub async fn update_canonical_alias(
|
||||
&self,
|
||||
alias: Option<String>,
|
||||
alt_aliases: Vec<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let new_alias = alias.map(TryInto::try_into).transpose()?;
|
||||
let new_alt_aliases =
|
||||
alt_aliases.into_iter().map(RoomAliasId::parse).collect::<Result<_, _>>()?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_canonical_alias(new_alias, new_alt_aliases)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Publish a new room alias for this room in the room directory.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `true` if the room alias didn't exist and it's now published.
|
||||
/// - `false` if the room alias was already present so it couldn't be
|
||||
/// published.
|
||||
pub async fn publish_room_alias_in_room_directory(
|
||||
&self,
|
||||
alias: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let new_alias = RoomAliasId::parse(alias)?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.publish_room_alias_in_room_directory(&new_alias)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Remove an existing room alias for this room in the room directory.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `true` if the room alias was present and it's now removed from the
|
||||
/// room directory.
|
||||
/// - `false` if the room alias didn't exist so it couldn't be removed.
|
||||
pub async fn remove_room_alias_from_room_directory(
|
||||
&self,
|
||||
alias: String,
|
||||
) -> Result<bool, ClientError> {
|
||||
let alias = RoomAliasId::parse(alias)?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.remove_room_alias_from_room_directory(&alias)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Enable End-to-end encryption in this room.
|
||||
pub async fn enable_encryption(&self) -> Result<(), ClientError> {
|
||||
self.inner.enable_encryption().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update room history visibility for this room.
|
||||
pub async fn update_history_visibility(
|
||||
&self,
|
||||
visibility: RoomHistoryVisibility,
|
||||
) -> Result<(), ClientError> {
|
||||
let visibility: RumaHistoryVisibility = visibility.try_into()?;
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_room_history_visibility(visibility)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the join rule for this room.
|
||||
pub async fn update_join_rules(&self, new_rule: JoinRule) -> Result<(), ClientError> {
|
||||
let new_rule: RumaJoinRule = new_rule.try_into()?;
|
||||
self.inner.privacy_settings().update_join_rule(new_rule).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Update the room's visibility in the room directory.
|
||||
pub async fn update_room_visibility(
|
||||
&self,
|
||||
visibility: RoomVisibility,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.privacy_settings()
|
||||
.update_room_visibility(visibility.into())
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the visibility for this room in the room directory.
|
||||
///
|
||||
/// [Public](`RoomVisibility::Public`) rooms are listed in the room
|
||||
/// directory and can be found using it.
|
||||
pub async fn get_room_visibility(&self) -> Result<RoomVisibility, ClientError> {
|
||||
let visibility = self.inner.privacy_settings().get_room_visibility().await?;
|
||||
Ok(visibility.into())
|
||||
}
|
||||
|
||||
/// Start the current users live location share in the room.
|
||||
pub async fn start_live_location_share(&self, duration_millis: u64) -> Result<(), ClientError> {
|
||||
self.inner.start_live_location_share(duration_millis, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the current users live location share in the room.
|
||||
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
|
||||
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the current users live location beacon in the room.
|
||||
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_location_beacon(geo_uri)
|
||||
.await
|
||||
.expect("Unable to send live location beacon");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to live location shares in this room, using a `listener` to
|
||||
/// be notified of the changes.
|
||||
///
|
||||
/// The current live location shares will be emitted immediately when
|
||||
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
|
||||
pub fn subscribe_to_live_location_shares(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn LiveLocationShareListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let subscription = room.observe_live_location_shares();
|
||||
let stream = subscription.subscribe();
|
||||
let mut pinned_stream = pin!(stream);
|
||||
|
||||
while let Some(event) = pinned_stream.next().await {
|
||||
let last_location = LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: event.last_location.location.uri.clone().to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset: None,
|
||||
};
|
||||
|
||||
let Some(beacon_info) = event.beacon_info else {
|
||||
warn!("Live location share is missing the associated beacon_info state, skipping event.");
|
||||
continue;
|
||||
};
|
||||
|
||||
listener.call(vec![LiveLocationShare {
|
||||
last_location: LastLocation {
|
||||
location: last_location,
|
||||
ts: event.last_location.ts.0.into(),
|
||||
},
|
||||
is_live: beacon_info.is_live(),
|
||||
user_id: event.user_id.to_string(),
|
||||
}])
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Forget this room.
|
||||
///
|
||||
/// This communicates to the homeserver that it should forget the room.
|
||||
///
|
||||
/// Only left or banned-from rooms can be forgotten.
|
||||
pub async fn forget(&self) -> Result<(), ClientError> {
|
||||
self.inner.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: Sync + Send {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
|
||||
@@ -1255,3 +1403,62 @@ impl TryFrom<ComposerDraftType> for SdkComposerDraftType {
|
||||
Ok(draft_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum RoomHistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes to
|
||||
/// something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from the point
|
||||
/// they joined the room onwards.
|
||||
/// Events stop being accessible when the member's state changes to
|
||||
/// something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when the member
|
||||
/// was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be shared by
|
||||
/// any participating homeserver with anyone, regardless of whether they
|
||||
/// have ever joined the room.
|
||||
WorldReadable,
|
||||
|
||||
/// A custom visibility value.
|
||||
Custom { value: String },
|
||||
}
|
||||
|
||||
impl TryFrom<RumaHistoryVisibility> for RoomHistoryVisibility {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RumaHistoryVisibility) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RumaHistoryVisibility::Invited => Ok(RoomHistoryVisibility::Invited),
|
||||
RumaHistoryVisibility::Shared => Ok(RoomHistoryVisibility::Shared),
|
||||
RumaHistoryVisibility::WorldReadable => Ok(RoomHistoryVisibility::WorldReadable),
|
||||
RumaHistoryVisibility::Joined => Ok(RoomHistoryVisibility::Joined),
|
||||
RumaHistoryVisibility::_Custom(_) => {
|
||||
Ok(RoomHistoryVisibility::Custom { value: value.to_string() })
|
||||
}
|
||||
_ => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RoomHistoryVisibility> for RumaHistoryVisibility {
|
||||
type Error = NotYetImplemented;
|
||||
fn try_from(value: RoomHistoryVisibility) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
RoomHistoryVisibility::Invited => Ok(RumaHistoryVisibility::Invited),
|
||||
RoomHistoryVisibility::Shared => Ok(RumaHistoryVisibility::Shared),
|
||||
RoomHistoryVisibility::Joined => Ok(RumaHistoryVisibility::Joined),
|
||||
RoomHistoryVisibility::WorldReadable => Ok(RumaHistoryVisibility::WorldReadable),
|
||||
RoomHistoryVisibility::Custom { .. } => Err(NotYetImplemented),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,18 +1,20 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use matrix_sdk::{EncryptionState, RoomState};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{Membership, RoomHero},
|
||||
room::{Membership, RoomHero, RoomHistoryVisibility},
|
||||
room_member::RoomMember,
|
||||
};
|
||||
|
||||
#[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.
|
||||
@@ -60,10 +62,12 @@ pub struct RoomInfo {
|
||||
pinned_event_ids: Vec<String>,
|
||||
/// The join rule for this room, if known.
|
||||
join_rule: Option<JoinRule>,
|
||||
/// The history visibility for this room, if known.
|
||||
history_visibility: RoomHistoryVisibility,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> matrix_sdk::Result<Self> {
|
||||
pub(crate) async fn new(room: &matrix_sdk::Room) -> Result<Self, ClientError> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
let power_levels_map = room.users_with_power_levels().await;
|
||||
@@ -81,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(),
|
||||
@@ -128,6 +133,7 @@ impl RoomInfo {
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
pinned_event_ids,
|
||||
join_rule: join_rule.ok(),
|
||||
history_visibility: room.history_visibility_or_default().try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
@@ -566,7 +566,7 @@ impl RoomListItem {
|
||||
}
|
||||
|
||||
async fn room_info(&self) -> Result<RoomInfo, ClientError> {
|
||||
Ok(RoomInfo::new(self.inner.inner_room()).await?)
|
||||
RoomInfo::new(self.inner.inner_room()).await
|
||||
}
|
||||
|
||||
/// The room's current membership state.
|
||||
@@ -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> {
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError>
|
||||
Ok(user_id.matrix_to_uri().to_string())
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomMember {
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
@@ -87,6 +87,7 @@ pub struct RoomMember {
|
||||
pub normalized_power_level: i64,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
pub membership_change_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
@@ -103,6 +104,29 @@ impl TryFrom<SdkRoomMember> for RoomMember {
|
||||
normalized_power_level: m.normalized_power_level(),
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -64,6 +76,20 @@ impl RoomPreview {
|
||||
let invite_details = room.invite_details().await.ok()?;
|
||||
invite_details.inviter.and_then(|m| m.try_into().ok())
|
||||
}
|
||||
|
||||
/// Forget the room if we had access to it, and it was left or banned.
|
||||
pub async fn forget(&self) -> Result<(), ClientError> {
|
||||
let room =
|
||||
self.client.get_room(&self.inner.room_id).context("missing room for a room preview")?;
|
||||
room.forget().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the membership details for the current user.
|
||||
pub async fn own_membership_details(&self) -> Option<RoomMemberWithSenderInfo> {
|
||||
let room = self.client.get_room(&self.inner.room_id)?;
|
||||
room.member_with_sender_info(self.client.user_id()?).await.ok()?.try_into().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomPreview {
|
||||
|
||||
@@ -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 } => {
|
||||
@@ -570,6 +572,7 @@ pub struct ImageInfo {
|
||||
pub thumbnail_info: Option<ThumbnailInfo>,
|
||||
pub thumbnail_source: Option<Arc<MediaSource>>,
|
||||
pub blurhash: Option<String>,
|
||||
pub is_animated: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<ImageInfo> for RumaImageInfo {
|
||||
@@ -582,6 +585,7 @@ impl From<ImageInfo> for RumaImageInfo {
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -603,6 +607,7 @@ impl TryFrom<&ImageInfo> for BaseImageInfo {
|
||||
width: Some(width),
|
||||
size: Some(size),
|
||||
blurhash: Some(blurhash),
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -655,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 {}
|
||||
|
||||
@@ -664,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>,
|
||||
@@ -859,6 +879,7 @@ impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
is_animated: info.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
use crate::{client::UserProfile, error::ClientError, utils::Timestamp};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SessionVerificationEmoji {
|
||||
@@ -39,14 +40,14 @@ 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: u64,
|
||||
first_seen_timestamp: Timestamp,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
@@ -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().get().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)]
|
||||
@@ -38,6 +38,7 @@ pub enum SyncServiceState {
|
||||
Running,
|
||||
Terminated,
|
||||
Error,
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
@@ -47,6 +48,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
|
||||
MatrixSyncServiceState::Running => Self::Running,
|
||||
MatrixSyncServiceState::Terminated => Self::Terminated,
|
||||
MatrixSyncServiceState::Error => Self::Error,
|
||||
MatrixSyncServiceState::Offline => Self::Offline,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,17 +74,17 @@ impl SyncService {
|
||||
}
|
||||
|
||||
pub async fn start(&self) {
|
||||
self.inner.start().await;
|
||||
self.inner.start().await
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.stop().await?)
|
||||
pub async fn stop(&self) {
|
||||
self.inner.stop().await
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -118,6 +120,13 @@ impl SyncServiceBuilder {
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
/// Enable the "offline" mode for the [`SyncService`].
|
||||
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_offline_mode();
|
||||
Arc::new(Self { client: this.client, builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
pub async fn with_utd_hook(
|
||||
self: Arc<Self>,
|
||||
delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
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::{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 {
|
||||
Live,
|
||||
Event { event_id: String, num_context_events: u16 },
|
||||
PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 },
|
||||
}
|
||||
|
||||
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(
|
||||
value: TimelineFocus,
|
||||
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
|
||||
match value {
|
||||
TimelineFocus::Live => Ok(Self::Live),
|
||||
TimelineFocus::Event { event_id, num_context_events } => {
|
||||
let parsed_event_id =
|
||||
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
|
||||
event_id: event_id.clone(),
|
||||
err: err.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(Self::Event { target: parsed_event_id, num_context_events })
|
||||
}
|
||||
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
|
||||
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes how date dividers get inserted, either in between each day or in
|
||||
/// between each month
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DateDividerMode {
|
||||
Daily,
|
||||
Monthly,
|
||||
}
|
||||
|
||||
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
fn from(value: DateDividerMode) -> Self {
|
||||
match value {
|
||||
DateDividerMode::Daily => Self::Daily,
|
||||
DateDividerMode::Monthly => Self::Monthly,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum TimelineFilter {
|
||||
/// Show all the events in the timeline, independent of their type.
|
||||
All,
|
||||
/// 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.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct TimelineConfiguration {
|
||||
/// What should the timeline focus on?
|
||||
pub focus: TimelineFocus,
|
||||
|
||||
/// 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
|
||||
/// distinguish different timeline instances from each other.
|
||||
pub internal_id_prefix: Option<String>,
|
||||
|
||||
/// 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,72 +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},
|
||||
};
|
||||
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(),
|
||||
@@ -136,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<u64>,
|
||||
has_been_edited: bool,
|
||||
MsgLike {
|
||||
content: MsgLikeContent,
|
||||
},
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
UnableToDecrypt {
|
||||
msg: EncryptedMessage,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
@@ -222,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,
|
||||
@@ -319,7 +138,7 @@ pub struct Reaction {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ReactionSenderData {
|
||||
pub sender_id: String,
|
||||
pub timestamp: u64,
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
@@ -462,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,
|
||||
has_been_edited: value.has_been_edited,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,25 +16,27 @@ 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 _};
|
||||
#[cfg(doc)]
|
||||
use matrix_sdk::crypto::CollectStrategy;
|
||||
use matrix_sdk::{
|
||||
attachment::{
|
||||
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
|
||||
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},
|
||||
@@ -48,7 +50,7 @@ use ruma::{
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
room::message::{
|
||||
ForwardThread, LocationMessageEventContent, MessageType,
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
@@ -62,25 +64,26 @@ use tokio::{
|
||||
use tracing::{error, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
#[cfg(doc)]
|
||||
use crate::client_builder::ClientBuilder;
|
||||
use self::content::TimelineItemContent;
|
||||
pub use self::msg_like::MessageContent;
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
event::EventOrTransactionId,
|
||||
helpers::unwrap_or_clone_arc,
|
||||
ruma::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, PollKind, ThumbnailInfo,
|
||||
VideoInfo,
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, PollKind,
|
||||
ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
task_handle::TaskHandle,
|
||||
RUNTIME,
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -101,48 +104,66 @@ impl Timeline {
|
||||
unsafe { Arc::from_raw(Arc::into_raw(inner) as _) }
|
||||
}
|
||||
|
||||
async fn send_attachment(
|
||||
&self,
|
||||
filename: String,
|
||||
fn send_attachment(
|
||||
self: Arc<Self>,
|
||||
params: UploadParameters,
|
||||
attachment_info: AttachmentInfo,
|
||||
mime_type: Option<String>,
|
||||
attachment_config: AttachmentConfig,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Result<(), RoomError> {
|
||||
thumbnail: Option<Thumbnail>,
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let mime_str = mime_type.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let mut request = self.inner.send_attachment(filename, mime_type, attachment_config);
|
||||
let formatted_caption = formatted_body_from(
|
||||
params.caption.as_deref(),
|
||||
params.formatted_caption.map(Into::into),
|
||||
);
|
||||
|
||||
if use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
}
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.thumbnail(thumbnail)
|
||||
.info(attachment_info)
|
||||
.caption(params.caption)
|
||||
.formatted_caption(formatted_caption)
|
||||
.mentions(params.mentions.map(Into::into))
|
||||
.reply(params.reply_params.map(|p| p.try_into()).transpose()?);
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
let handle = SendAttachmentJoinHandle::new(get_runtime_handle().spawn(async move {
|
||||
let mut request =
|
||||
self.inner.send_attachment(params.filename, mime_type, attachment_config);
|
||||
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
if params.use_send_queue {
|
||||
request = request.use_send_queue();
|
||||
}
|
||||
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
get_runtime_handle().spawn(async move {
|
||||
while let Some(progress) = subscriber.next().await {
|
||||
progress_watcher.transmission_progress(progress.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
request.await.map_err(|_| RoomError::FailedSendingAttachment)?;
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_thumbnail_info(
|
||||
thumbnail_url: Option<String>,
|
||||
thumbnail_path: Option<String>,
|
||||
thumbnail_info: Option<ThumbnailInfo>,
|
||||
) -> Result<AttachmentConfig, RoomError> {
|
||||
match (thumbnail_url, thumbnail_info) {
|
||||
(None, None) => Ok(AttachmentConfig::new()),
|
||||
) -> Result<Option<Thumbnail>, RoomError> {
|
||||
match (thumbnail_path, thumbnail_info) {
|
||||
(None, None) => Ok(None),
|
||||
|
||||
(Some(thumbnail_url), Some(thumbnail_info)) => {
|
||||
(Some(thumbnail_path), Some(thumbnail_info)) => {
|
||||
let thumbnail_data =
|
||||
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
fs::read(thumbnail_path).map_err(|_| RoomError::InvalidThumbnailData)?;
|
||||
|
||||
let height = thumbnail_info
|
||||
.height
|
||||
@@ -162,37 +183,89 @@ fn build_thumbnail_info(
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let thumbnail =
|
||||
Thumbnail { data: thumbnail_data, content_type: mime_type, height, width, size };
|
||||
|
||||
Ok(AttachmentConfig::with_thumbnail(thumbnail))
|
||||
Ok(Some(Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
height,
|
||||
width,
|
||||
size,
|
||||
}))
|
||||
}
|
||||
|
||||
_ => {
|
||||
warn!("Ignoring thumbnail because either the thumbnail URL or info isn't defined");
|
||||
Ok(AttachmentConfig::new())
|
||||
warn!("Ignoring thumbnail because either the thumbnail path or info isn't defined");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UploadParameters {
|
||||
/// Filename (previously called "url") for the media to be sent.
|
||||
filename: String,
|
||||
/// Optional non-formatted caption, for clients that support it.
|
||||
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.
|
||||
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_batched().await;
|
||||
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
|
||||
@@ -202,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;
|
||||
});
|
||||
}
|
||||
@@ -221,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);
|
||||
}
|
||||
@@ -233,16 +310,16 @@ impl Timeline {
|
||||
|
||||
/// Paginate backwards, whether we are in focused mode or in live mode.
|
||||
///
|
||||
/// Returns whether we hit the end of the timeline or not.
|
||||
/// Returns whether we hit the start of the timeline or not.
|
||||
pub async fn paginate_backwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.paginate_backwards(num_events).await?)
|
||||
}
|
||||
|
||||
/// Paginate forwards, when in focused mode.
|
||||
/// Paginate forwards, whether we are in focused mode or in live mode.
|
||||
///
|
||||
/// Returns whether we hit the end of the timeline or not.
|
||||
pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.focused_paginate_forwards(num_events).await?)
|
||||
pub async fn paginate_forwards(&self, num_events: u16) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.paginate_forwards(num_events).await?)
|
||||
}
|
||||
|
||||
pub async fn send_read_receipt(
|
||||
@@ -286,171 +363,83 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_image(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
image_info: ImageInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let base_image_info = BaseImageInfo::try_from(&image_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let attachment_info = AttachmentInfo::Image(base_image_info);
|
||||
|
||||
let attachment_config = build_thumbnail_info(thumbnail_url, image_info.thumbnail_info)?
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption);
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
image_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Image(
|
||||
BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, image_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
image_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_video(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
params: UploadParameters,
|
||||
thumbnail_path: Option<String>,
|
||||
video_info: VideoInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let base_video_info: BaseVideoInfo = BaseVideoInfo::try_from(&video_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let attachment_info = AttachmentInfo::Video(base_video_info);
|
||||
|
||||
let attachment_config = build_thumbnail_info(thumbnail_url, video_info.thumbnail_info)?
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
video_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Video(
|
||||
BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
let thumbnail = build_thumbnail_info(thumbnail_path, video_info.thumbnail_info)?;
|
||||
self.send_attachment(
|
||||
params,
|
||||
attachment_info,
|
||||
video_info.mimetype,
|
||||
progress_watcher,
|
||||
thumbnail,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn send_audio(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let attachment_info = AttachmentInfo::Audio(base_audio_info);
|
||||
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
audio_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Audio(
|
||||
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn send_voice_message(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
params: UploadParameters,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let base_audio_info: BaseAudioInfo = BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let attachment_info =
|
||||
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
|
||||
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
audio_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::Voice {
|
||||
audio_info: BaseAudioInfo::try_from(&audio_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
waveform: Some(waveform),
|
||||
};
|
||||
self.send_attachment(params, attachment_info, audio_info.mimetype, progress_watcher, None)
|
||||
}
|
||||
|
||||
pub fn send_file(
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
params: UploadParameters,
|
||||
file_info: FileInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
use_send_queue: bool,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
let base_file_info: BaseFileInfo =
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let attachment_info = AttachmentInfo::File(base_file_info);
|
||||
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(
|
||||
url,
|
||||
file_info.mimetype,
|
||||
attachment_config,
|
||||
progress_watcher,
|
||||
use_send_queue,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
|
||||
let attachment_info = AttachmentInfo::File(
|
||||
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?,
|
||||
);
|
||||
self.send_attachment(params, attachment_info, file_info.mimetype, progress_watcher, None)
|
||||
}
|
||||
|
||||
pub async fn create_poll(
|
||||
@@ -495,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,
|
||||
@@ -505,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(())
|
||||
@@ -675,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(
|
||||
@@ -811,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)]
|
||||
@@ -986,8 +964,9 @@ impl TimelineItem {
|
||||
pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
|
||||
use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem;
|
||||
match self.0.as_virtual()? {
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: ts.0.into() }),
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),
|
||||
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
|
||||
VItem::TimelineStart => Some(VirtualTimelineItem::TimelineStart),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1081,9 +1060,9 @@ pub struct EventTimelineItem {
|
||||
is_own: bool,
|
||||
is_editable: bool,
|
||||
content: TimelineItemContent,
|
||||
timestamp: u64,
|
||||
reactions: Vec<Reaction>,
|
||||
timestamp: Timestamp,
|
||||
local_send_state: Option<EventSendState>,
|
||||
local_created_at: Option<u64>,
|
||||
read_receipts: HashMap<String, Receipt>,
|
||||
origin: Option<EventItemOrigin>,
|
||||
can_be_replied_to: bool,
|
||||
@@ -1092,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.0.into(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
let item = Arc::new(item);
|
||||
let lazy_provider = Arc::new(LazyTimelineItemProvider(item.clone()));
|
||||
let read_receipts =
|
||||
@@ -1118,9 +1083,9 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
timestamp: item.timestamp().0.into(),
|
||||
reactions,
|
||||
timestamp: item.timestamp().into(),
|
||||
local_send_state: item.send_state().map(|s| s.into()),
|
||||
local_created_at: item.local_created_at().map(|t| t.0.into()),
|
||||
read_receipts,
|
||||
origin: item.origin(),
|
||||
can_be_replied_to: item.can_be_replied_to(),
|
||||
@@ -1131,12 +1096,12 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct Receipt {
|
||||
pub timestamp: Option<u64>,
|
||||
pub timestamp: Option<Timestamp>,
|
||||
}
|
||||
|
||||
impl From<ruma::events::receipt::Receipt> for Receipt {
|
||||
fn from(value: ruma::events::receipt::Receipt) -> Self {
|
||||
Receipt { timestamp: value.ts.map(|ts| ts.0.into()) }
|
||||
Receipt { timestamp: value.ts.map(|ts| ts.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1260,11 +1225,14 @@ pub enum VirtualTimelineItem {
|
||||
DateDivider {
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
ts: u64,
|
||||
ts: Timestamp,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
@@ -1287,22 +1255,32 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EditedContent {
|
||||
RoomMessage { content: Arc<RoomMessageEventContentWithoutRelation> },
|
||||
MediaCaption { caption: Option<String>, formatted_caption: Option<FormattedBody> },
|
||||
PollStart { poll_data: PollData },
|
||||
RoomMessage {
|
||||
content: Arc<RoomMessageEventContentWithoutRelation>,
|
||||
},
|
||||
MediaCaption {
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
mentions: Option<Mentions>,
|
||||
},
|
||||
PollStart {
|
||||
poll_data: PollData,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: EditedContent) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
EditedContent::RoomMessage { content } => {
|
||||
Ok(SdkEditedContent::RoomMessage((*content).clone()))
|
||||
}
|
||||
EditedContent::MediaCaption { caption, formatted_caption } => {
|
||||
EditedContent::MediaCaption { caption, formatted_caption, mentions } => {
|
||||
Ok(SdkEditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.map(Into::into),
|
||||
mentions: mentions.map(Into::into),
|
||||
})
|
||||
}
|
||||
EditedContent::PollStart { poll_data } => {
|
||||
@@ -1324,12 +1302,14 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
fn create_caption_edit(
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
mentions: Option<Mentions>,
|
||||
) -> EditedContent {
|
||||
let formatted_caption =
|
||||
formatted_body_from(caption.as_deref(), formatted_caption.map(Into::into));
|
||||
EditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.as_ref().map(Into::into),
|
||||
mentions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1358,21 +1338,8 @@ impl LazyTimelineItemProvider {
|
||||
fn get_send_handle(&self) -> Option<Arc<SendHandle>> {
|
||||
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes how date dividers get inserted, either in between each day or in
|
||||
/// between each month
|
||||
#[derive(Debug, Clone, uniffi::Enum)]
|
||||
pub enum DateDividerMode {
|
||||
Daily,
|
||||
Monthly,
|
||||
}
|
||||
|
||||
impl From<DateDividerMode> for matrix_sdk_ui::timeline::DateDividerMode {
|
||||
fn from(value: DateDividerMode) -> Self {
|
||||
match value {
|
||||
DateDividerMode::Daily => Self::Daily,
|
||||
DateDividerMode::Monthly => Self::Monthly,
|
||||
}
|
||||
fn contains_only_emojis(&self) -> bool {
|
||||
self.0.contains_only_emojis()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -185,6 +185,16 @@ impl LogLevel {
|
||||
LogLevel::Trace => tracing::Level::TRACE,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
||||
@@ -14,10 +14,21 @@
|
||||
|
||||
use std::{mem::ManuallyDrop, ops::Deref};
|
||||
|
||||
use async_compat::TOKIO1 as RUNTIME;
|
||||
use ruma::UInt;
|
||||
use async_compat::get_runtime_handle;
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timestamp(u64);
|
||||
|
||||
impl From<MilliSecondsSinceUnixEpoch> for Timestamp {
|
||||
fn from(date: MilliSecondsSinceUnixEpoch) -> Self {
|
||||
Self(date.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
uniffi::custom_newtype!(Timestamp, u64);
|
||||
|
||||
pub(crate) fn u64_to_uint(u: u64) -> UInt {
|
||||
UInt::new(u).unwrap_or_else(|| {
|
||||
warn!("u64 -> UInt conversion overflowed, falling back to UInt::MAX");
|
||||
@@ -43,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,73 @@ 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
|
||||
|
||||
- [**breaking**] `EventCacheStore` allows to control which media content is
|
||||
allowed in the media cache, and how long it should be kept, with a
|
||||
`MediaRetentionPolicy`:
|
||||
- `EventCacheStore::add_media_content()` has an extra argument,
|
||||
`ignore_policy`, which decides whether a media content should ignore the
|
||||
`MediaRetentionPolicy`. It should be stored alongside the media content.
|
||||
- `EventCacheStore` has four new methods: `media_retention_policy()`,
|
||||
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
|
||||
`clean_up_media_cache()`.
|
||||
- `EventCacheStore` implementations should delegate media cache methods to the
|
||||
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
|
||||
They need to implement the `EventCacheStoreMedia` trait that can be tested
|
||||
with the `event_cache_store_media_integration_tests!` macro.
|
||||
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] Replaced `Room::compute_display_name` with the reintroduced
|
||||
`Room::display_name()`. The new method computes a display name, or return a
|
||||
cached value from the previous successful computation. If you need a sync
|
||||
variant, consider using `Room::cached_display_name()`.
|
||||
([#4470](https://github.com/matrix-org/matrix-rust-sdk/pull/4470))
|
||||
- [**breaking**]: The reexported types `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
|
||||
|
||||
### 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.9.0"
|
||||
version = "0.11.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
@@ -21,16 +21,15 @@ e2e-encryption = ["dep:matrix-sdk-crypto"]
|
||||
js = ["matrix-sdk-common/js", "matrix-sdk-crypto?/js", "ruma/js", "matrix-sdk-store-encryption/js"]
|
||||
qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
experimental-sliding-sync = [
|
||||
"ruma/unstable-msc3575",
|
||||
"ruma/unstable-msc4186",
|
||||
]
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
# https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory
|
||||
# details.
|
||||
test-send-sync = ["matrix-sdk-crypto?/test-send-sync"]
|
||||
test-send-sync = [
|
||||
"matrix-sdk-common/test-send-sync",
|
||||
"matrix-sdk-crypto?/test-send-sync",
|
||||
]
|
||||
|
||||
# "message-ids" feature doesn't do anything and is deprecated.
|
||||
message-ids = []
|
||||
@@ -49,7 +48,7 @@ as_variant = { workspace = true }
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.6.0", features = ["serde"] }
|
||||
bitflags = { version = "2.8.0", features = ["serde"] }
|
||||
decancer = "3.2.8"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im = { workspace = true }
|
||||
@@ -62,7 +61,13 @@ matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = "1.11.1"
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
|
||||
ruma = { workspace = true, features = [
|
||||
"canonical-json",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc3381",
|
||||
"unstable-msc4186",
|
||||
"rand",
|
||||
] }
|
||||
unicode-normalization = { workspace = true }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
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 {
|
||||
@@ -602,7 +610,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_name_equality_cyrilic() {
|
||||
fn test_display_name_equality_cyrillic() {
|
||||
// Display name with scritpure symbols
|
||||
assert_display_name_eq!("alice", "аlice");
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use matrix_sdk_common::store_locks::LockStoreError;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::event_cache::store::EventCacheStoreError;
|
||||
|
||||
/// Result type of the rust-sdk.
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
@@ -42,6 +45,14 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
StateStore(#[from] crate::store::StoreError),
|
||||
|
||||
/// An error happened while manipulating the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheStore(#[from] EventCacheStoreError),
|
||||
|
||||
/// An error happened while attempting to lock the event cache store.
|
||||
#[error(transparent)]
|
||||
EventCacheLock(#[from] LockStoreError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
//! Event cache store and common types shared with `matrix_sdk::event_cache`.
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
|
||||
pub mod store;
|
||||
|
||||
/// The kind of event the event storage holds.
|
||||
pub type Event = SyncTimelineEvent;
|
||||
pub type Event = TimelineEvent;
|
||||
|
||||
/// The kind of gap the event storage holds.
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -18,20 +18,27 @@ use assert_matches::assert_matches;
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::{
|
||||
deserialized_responses::{
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind,
|
||||
AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind,
|
||||
VerificationState,
|
||||
},
|
||||
linked_chunk::{ChunkContent, 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::DynEventCacheStore;
|
||||
use super::{media::IgnoreMediaRetentionPolicy, DynEventCacheStore};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
event_cache::{store::DEFAULT_CHUNK_CAPACITY, Gap},
|
||||
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
|
||||
};
|
||||
|
||||
@@ -39,7 +46,16 @@ use crate::{
|
||||
/// correctly stores event data.
|
||||
///
|
||||
/// Keep in sync with [`check_test_event`].
|
||||
pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
|
||||
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,
|
||||
@@ -48,22 +64,22 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
|
||||
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();
|
||||
|
||||
SyncTimelineEvent {
|
||||
TimelineEvent {
|
||||
kind: TimelineEventKind::Decrypted(DecryptedRoomEvent {
|
||||
event,
|
||||
encryption_info,
|
||||
unsigned_encryption_info: None,
|
||||
}),
|
||||
push_actions: vec![Action::Notify],
|
||||
push_actions: Some(vec![Action::Notify]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +88,9 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent {
|
||||
///
|
||||
/// Keep in sync with [`make_test_event`].
|
||||
#[track_caller]
|
||||
pub fn check_test_event(event: &SyncTimelineEvent, text: &str) {
|
||||
pub fn check_test_event(event: &TimelineEvent, text: &str) {
|
||||
// Check push actions.
|
||||
let actions = &event.push_actions;
|
||||
let actions = event.push_actions.as_ref().unwrap();
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_matches!(&actions[0], Action::Notify);
|
||||
|
||||
@@ -97,7 +113,7 @@ pub fn check_test_event(event: &SyncTimelineEvent, 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 {
|
||||
@@ -111,16 +127,31 @@ 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);
|
||||
|
||||
/// Test that clear all the rooms' linked chunks works.
|
||||
async fn test_clear_all_rooms_chunks(&self);
|
||||
}
|
||||
|
||||
fn rebuild_linked_chunk(raws: Vec<RawChunk<Event, Gap>>) -> Option<LinkedChunk<3, Event, Gap>> {
|
||||
LinkedChunkBuilder::from_raw_parts(raws).build().unwrap()
|
||||
/// Test that removing a room from storage empties all associated data.
|
||||
async fn test_remove_room(&self);
|
||||
|
||||
/// 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))]
|
||||
@@ -162,7 +193,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// Let's add the media.
|
||||
self.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
|
||||
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media failed");
|
||||
|
||||
// Media is present in the cache.
|
||||
assert_eq!(
|
||||
@@ -190,7 +223,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// Let's add the media again.
|
||||
self.add_media_content(&request_file, content.clone())
|
||||
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media again failed");
|
||||
|
||||
@@ -201,9 +234,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// Let's add the thumbnail media.
|
||||
self.add_media_content(&request_thumbnail, thumbnail_content.clone())
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
self.add_media_content(
|
||||
&request_thumbnail,
|
||||
thumbnail_content.clone(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.expect("adding thumbnail failed");
|
||||
|
||||
// Media's thumbnail is present.
|
||||
assert_eq!(
|
||||
@@ -219,9 +256,13 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
);
|
||||
|
||||
// Let's add another media with a different URI.
|
||||
self.add_media_content(&request_other_file, other_content.clone())
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
self.add_media_content(
|
||||
&request_other_file,
|
||||
other_content.clone(),
|
||||
IgnoreMediaRetentionPolicy::No,
|
||||
)
|
||||
.await
|
||||
.expect("adding other media failed");
|
||||
|
||||
// Other file is present.
|
||||
assert_eq!(
|
||||
@@ -273,7 +314,9 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
|
||||
|
||||
// Add the media.
|
||||
self.add_media_content(&req, content.clone()).await.expect("adding media failed");
|
||||
self.add_media_content(&req, content.clone(), IgnoreMediaRetentionPolicy::No)
|
||||
.await
|
||||
.expect("adding media failed");
|
||||
|
||||
// Sanity-check: media is found after adding it.
|
||||
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
|
||||
@@ -299,8 +342,6 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
|
||||
}
|
||||
|
||||
async fn test_handle_updates_and_rebuild_linked_chunk(&self) {
|
||||
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
|
||||
|
||||
let room_id = room_id!("!r0:matrix.org");
|
||||
|
||||
self.handle_linked_chunk_updates(
|
||||
@@ -325,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")],
|
||||
@@ -336,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();
|
||||
|
||||
@@ -376,15 +419,231 @@ 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) {
|
||||
use matrix_sdk_common::linked_chunk::ChunkIdentifier as CId;
|
||||
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
|
||||
@@ -430,15 +689,330 @@ 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) {
|
||||
let r0 = room_id!("!r0:matrix.org");
|
||||
let r1 = room_id!("!r1:matrix.org");
|
||||
|
||||
// Add updates to the first room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r0,
|
||||
vec![
|
||||
// new chunk
|
||||
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![make_test_event(r0, "hello"), make_test_event(r0, "world")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add updates to the second room.
|
||||
self.handle_linked_chunk_updates(
|
||||
r1,
|
||||
vec![
|
||||
// new chunk
|
||||
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![make_test_event(r0, "yummy")],
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to remove content from r0.
|
||||
self.remove_room(r0).await.unwrap();
|
||||
|
||||
// Check that r0 doesn't have a linked chunk anymore.
|
||||
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.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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,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 =
|
||||
@@ -515,6 +1096,41 @@ macro_rules! event_cache_store_integration_tests {
|
||||
get_event_cache_store().await.unwrap().into_event_cache_store();
|
||||
event_cache_store.test_clear_all_rooms_chunks().await;
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_remove_room() {
|
||||
let event_cache_store =
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,405 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Configuration to decide whether or not to keep media in the cache, allowing
|
||||
//! to do periodic cleanups to avoid to have the size of the media cache grow
|
||||
//! indefinitely.
|
||||
//!
|
||||
//! To proceed to a cleanup, first set the [`MediaRetentionPolicy`] to use with
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]. Then call
|
||||
//! [`EventCacheStore::clean_up_media_cache()`].
|
||||
//!
|
||||
//! In the future, other settings will allow to run automatic periodic cleanup
|
||||
//! jobs.
|
||||
//!
|
||||
//! [`EventCacheStore::set_media_retention_policy()`]: crate::event_cache::store::EventCacheStore::set_media_retention_policy
|
||||
//! [`EventCacheStore::clean_up_media_cache()`]: crate::event_cache::store::EventCacheStore::clean_up_media_cache
|
||||
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The retention policy for media content used by the [`EventCacheStore`].
|
||||
///
|
||||
/// [`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.
|
||||
///
|
||||
/// The cache size is defined as the sum of the sizes of all the (possibly
|
||||
/// encrypted) media contents in the cache, excluding any metadata
|
||||
/// associated with them.
|
||||
///
|
||||
/// If this is set and the cache size is bigger than this value, the oldest
|
||||
/// media contents in the cache will be removed during a cleanup until the
|
||||
/// cache size is below this threshold.
|
||||
///
|
||||
/// Note that it is possible for the cache size to temporarily exceed this
|
||||
/// value between two cleanups.
|
||||
///
|
||||
/// Defaults to 400 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_cache_size: Option<u64>,
|
||||
|
||||
/// The maximum authorized size of a single media content, in bytes.
|
||||
///
|
||||
/// The size of a media content is the size taken by the content in the
|
||||
/// database, after it was possibly encrypted, so it might differ from the
|
||||
/// initial size of the content.
|
||||
///
|
||||
/// The maximum authorized size of a single media content is actually the
|
||||
/// lowest value between `max_cache_size` and `max_file_size`.
|
||||
///
|
||||
/// If it is set, media content bigger than the maximum size will not be
|
||||
/// cached. If the maximum size changed after media content that exceeds the
|
||||
/// new value was cached, the corresponding content will be removed
|
||||
/// during a cleanup.
|
||||
///
|
||||
/// Defaults to 20 MiB.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<u64>,
|
||||
|
||||
/// The duration after which unaccessed media content is considered
|
||||
/// expired.
|
||||
///
|
||||
/// If this is set, media content whose last access is older than this
|
||||
/// duration will be removed from the media cache during a cleanup.
|
||||
///
|
||||
/// 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 {
|
||||
/// Create a [`MediaRetentionPolicy`] with the default values.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create an empty [`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,
|
||||
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<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<u64>) -> Self {
|
||||
self.max_file_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the duration before which unaccessed media content is considered
|
||||
/// expired.
|
||||
pub fn with_last_access_expiry(mut self, duration: Option<Duration>) -> Self {
|
||||
self.last_access_expiry = duration;
|
||||
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.
|
||||
///
|
||||
/// Returns `true` if at least one limitation is set.
|
||||
pub fn has_limitations(&self) -> bool {
|
||||
self.max_cache_size.is_some()
|
||||
|| self.max_file_size.is_some()
|
||||
|| self.last_access_expiry.is_some()
|
||||
}
|
||||
|
||||
/// Whether the given size exceeds the maximum authorized size of the media
|
||||
/// cache.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The overall size of the media cache to check, in bytes.
|
||||
pub fn exceeds_max_cache_size(&self, size: u64) -> bool {
|
||||
self.max_cache_size.is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
/// The computed maximum authorized size of a single media content, in
|
||||
/// bytes.
|
||||
///
|
||||
/// This is the lowest value between `max_cache_size` and `max_file_size`.
|
||||
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),
|
||||
(Some(size), None) => Some(size),
|
||||
(Some(max_cache_size), Some(max_file_size)) => Some(max_cache_size.min(max_file_size)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the given size, in bytes, exceeds the computed maximum
|
||||
/// authorized size of a single media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `size` - The size of the media content to check, in bytes.
|
||||
pub fn exceeds_max_file_size(&self, size: u64) -> bool {
|
||||
self.computed_max_file_size().is_some_and(|max_size| size > max_size)
|
||||
}
|
||||
|
||||
/// Whether a content whose last access was at the given time has expired.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `current_time` - The current time.
|
||||
///
|
||||
/// * `last_access_time` - The time when the media content to check was last
|
||||
/// accessed.
|
||||
pub fn has_content_expired(
|
||||
&self,
|
||||
current_time: SystemTime,
|
||||
last_access_time: SystemTime,
|
||||
) -> bool {
|
||||
self.last_access_expiry.is_some_and(|max_duration| {
|
||||
current_time
|
||||
.duration_since(last_access_time)
|
||||
// If this returns an error, the last access time is newer than the current time.
|
||||
// This shouldn't happen but in this case the content cannot be expired.
|
||||
.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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// 400 MiB.
|
||||
max_cache_size: Some(400 * 1024 * 1024),
|
||||
// 20 MiB.
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::time::{Duration, SystemTime};
|
||||
|
||||
use super::MediaRetentionPolicy;
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_has_limitations() {
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_last_access_expiry(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_cache_size(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_file_size(Some(1_024));
|
||||
assert!(policy.has_limitations());
|
||||
|
||||
policy = policy.with_max_file_size(None);
|
||||
assert!(!policy.has_limitations());
|
||||
|
||||
// With default values.
|
||||
assert!(MediaRetentionPolicy::new().has_limitations());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_max_cache_size() {
|
||||
let file_size = 2_048;
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), None);
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(4_096));
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(4_096));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(2_048));
|
||||
assert!(!policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert!(policy.exceeds_max_cache_size(file_size));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_max_file_size() {
|
||||
let file_size = 2_048;
|
||||
|
||||
let mut policy = MediaRetentionPolicy::empty();
|
||||
assert_eq!(policy.computed_max_file_size(), None);
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
// With max_file_size only.
|
||||
policy = policy.with_max_file_size(Some(4_096));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(4_096));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(1_024));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
|
||||
// With max_cache_size as well.
|
||||
policy = policy.with_max_cache_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(2_048));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_file_size(Some(4_096));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(2_048));
|
||||
assert!(!policy.exceeds_max_file_size(file_size));
|
||||
|
||||
policy = policy.with_max_cache_size(Some(1_024));
|
||||
assert_eq!(policy.computed_max_file_size(), Some(1_024));
|
||||
assert!(policy.exceeds_max_file_size(file_size));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_media_retention_policy_has_content_expired() {
|
||||
let epoch = SystemTime::UNIX_EPOCH;
|
||||
let last_access_time = epoch + Duration::from_secs(30);
|
||||
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.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(120)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(60)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(!policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(30)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(!policy.has_content_expired(last_access_time, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_60, last_access_time));
|
||||
assert!(policy.has_content_expired(epoch_plus_120, last_access_time));
|
||||
|
||||
policy = policy.with_last_access_expiry(Some(Duration::from_secs(0)));
|
||||
assert!(!policy.has_content_expired(epoch, last_access_time));
|
||||
assert!(policy.has_content_expired(last_access_time, last_access_time));
|
||||
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));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
// Copyright 2025 Kévin Commaille
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Types and traits regarding media caching of the event cache store.
|
||||
|
||||
mod media_retention_policy;
|
||||
mod media_service;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use self::integration_tests::EventCacheStoreMediaIntegrationTests;
|
||||
pub use self::{
|
||||
media_retention_policy::MediaRetentionPolicy,
|
||||
media_service::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService},
|
||||
};
|
||||
@@ -12,17 +12,33 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, num::NonZeroUsize, sync::RwLock as StdRwLock, time::Instant};
|
||||
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::{MxcUri, OwnedMxcUri, RoomId};
|
||||
use ruma::{
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use super::{
|
||||
compute_filters_string, extract_event_relation,
|
||||
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
|
||||
EventCacheStore, EventCacheStoreError, Result,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::{MediaRequestParameters, UniqueKey as _},
|
||||
@@ -31,30 +47,58 @@ 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,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>,
|
||||
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,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
|
||||
/// A media content in the `MemoryStore`.
|
||||
#[derive(Debug)]
|
||||
struct MediaContent {
|
||||
/// The URI of the content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The unique key of the content.
|
||||
key: String,
|
||||
|
||||
/// The bytes of the content.
|
||||
data: Vec<u8>,
|
||||
|
||||
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
// Given that the store is empty, we won't need to clean it up right away.
|
||||
let last_media_cleanup_time = SystemTime::now();
|
||||
let media_service = MediaService::new();
|
||||
media_service.restore(None, Some(last_media_cleanup_time));
|
||||
|
||||
Self {
|
||||
inner: 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,
|
||||
last_media_cleanup_time,
|
||||
})),
|
||||
media_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,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 })
|
||||
}
|
||||
|
||||
@@ -109,19 +176,104 @@ 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,
|
||||
data: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<()> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push((request.uri().to_owned(), request.unique_key(), data));
|
||||
|
||||
Ok(())
|
||||
self.media_service.add_media_content(self, request, data, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
@@ -133,23 +285,18 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some((mxc, key, _)) = inner.media.iter_mut().find(|(_, key, _)| *key == expected_key)
|
||||
if let Some(media_content) =
|
||||
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
|
||||
{
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
media_content.uri = to.uri().to_owned();
|
||||
media_content.key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
Ok(inner.media.iter().find_map(|(_media_uri, media_key, media_content)| {
|
||||
(media_key == &expected_key).then(|| media_content.to_owned())
|
||||
}))
|
||||
self.media_service.get_media_content(self, request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
|
||||
@@ -157,10 +304,8 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) = inner
|
||||
.media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
let Some(index) =
|
||||
inner.media.iter().position(|media_content| media_content.key == expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -174,24 +319,17 @@ impl EventCacheStore for MemoryStore {
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
Ok(inner.media.iter().find_map(|(media_uri, _media_key, media_content)| {
|
||||
(media_uri == uri).then(|| media_content.to_owned())
|
||||
}))
|
||||
self.media_service.get_media_content_for_uri(self, uri).await
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = inner
|
||||
.media
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(position, (media_uri, _media_key, _media_content))| {
|
||||
(media_uri == &expected_key).then_some(position)
|
||||
})
|
||||
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Iterate in reverse-order so that positions stay valid after first removals.
|
||||
@@ -201,16 +339,246 @@ impl EventCacheStore for MemoryStore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_media_retention_policy(self, policy).await
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.media_service.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.media_service.clean_up_media_cache(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventCacheStoreMedia for MemoryStore {
|
||||
type Error = EventCacheStoreError;
|
||||
|
||||
async fn media_retention_policy_inner(
|
||||
&self,
|
||||
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
|
||||
Ok(self.inner.read().unwrap().media_retention_policy)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().media_retention_policy = Some(policy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
data: Vec<u8>,
|
||||
last_access: SystemTime,
|
||||
policy: MediaRetentionPolicy,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
// Avoid duplication. Let's try to remove it first.
|
||||
self.remove_media_content(request).await?;
|
||||
|
||||
let ignore_policy = ignore_policy.is_yes();
|
||||
|
||||
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
|
||||
// Do not store it.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Now, let's add it.
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.media.push(MediaContent {
|
||||
uri: request.uri().to_owned(),
|
||||
key: request.unique_key(),
|
||||
data,
|
||||
ignore_policy,
|
||||
last_access,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
|
||||
{
|
||||
media_content.ignore_policy = ignore_policy.is_yes();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_inner(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri_inner(
|
||||
&self,
|
||||
expected_uri: &MxcUri,
|
||||
current_time: SystemTime,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First get the content out of the buffer, we are going to put it back at the
|
||||
// end.
|
||||
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(mut content) = inner.media.remove(index) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Clone the data.
|
||||
let data = content.data.clone();
|
||||
|
||||
// Update the last access time.
|
||||
content.last_access = current_time;
|
||||
|
||||
// Put it back in the buffer.
|
||||
inner.media.push(content);
|
||||
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache_inner(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
current_time: SystemTime,
|
||||
) -> Result<(), Self::Error> {
|
||||
if !policy.has_limitations() {
|
||||
// We can safely skip all the checks.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
// First, check media content that exceed the max filesize.
|
||||
if policy.computed_max_file_size().is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
|
||||
});
|
||||
}
|
||||
|
||||
// Then, clean up expired media content.
|
||||
if policy.last_access_expiry.is_some() {
|
||||
inner.media.retain(|content| {
|
||||
content.ignore_policy
|
||||
|| !policy.has_content_expired(current_time, content.last_access)
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, if the cache size is too big, remove old items until it fits.
|
||||
if let Some(max_cache_size) = policy.max_cache_size {
|
||||
// Reverse the iterator because in case the cache size is overflowing, we want
|
||||
// to count the number of old items to remove. Items are sorted by last access
|
||||
// and old items are at the start.
|
||||
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
|
||||
(0u64, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|
||||
|(mut cache_size, mut items_to_remove), (index, content)| {
|
||||
if content.ignore_policy {
|
||||
// Do not count it.
|
||||
return (cache_size, items_to_remove);
|
||||
}
|
||||
|
||||
let remove_item = if items_to_remove.is_empty() {
|
||||
// We have not reached the max cache size yet.
|
||||
if let Some(sum) = cache_size.checked_add(content.data.len() as u64) {
|
||||
cache_size = sum;
|
||||
// Start removing items if we have exceeded the max cache size.
|
||||
cache_size > max_cache_size
|
||||
} else {
|
||||
// The cache size is overflowing, remove the remaining items, since the
|
||||
// max cache size cannot be bigger than
|
||||
// usize::MAX.
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// We have reached the max cache size already, just remove it.
|
||||
true
|
||||
};
|
||||
|
||||
if remove_item {
|
||||
items_to_remove.push(index);
|
||||
}
|
||||
|
||||
(cache_size, items_to_remove)
|
||||
},
|
||||
);
|
||||
|
||||
// The indexes are already in reverse order so we can just iterate in that order
|
||||
// to remove them starting by the end.
|
||||
for index in items_to_remove {
|
||||
inner.media.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
inner.last_media_cleanup_time = current_time;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
|
||||
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EventCacheStore, MemoryStore, Result};
|
||||
use super::{MemoryStore, Result};
|
||||
use crate::event_cache_store_media_integration_tests;
|
||||
|
||||
async fn get_event_cache_store() -> Result<impl EventCacheStore> {
|
||||
async fn get_event_cache_store() -> Result<MemoryStore> {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
event_cache_store_integration_tests!();
|
||||
event_cache_store_integration_tests_time!();
|
||||
event_cache_store_media_integration_tests!(with_media_size_tests);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
pub mod media;
|
||||
mod memory_store;
|
||||
mod traits;
|
||||
|
||||
@@ -31,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;
|
||||
@@ -192,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,12 +16,15 @@ 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::EventCacheStoreError;
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
EventCacheStoreError,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
@@ -57,19 +60,89 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Remove all data tied to a given room from the cache.
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<(), Self::Error> {
|
||||
// Right now, this means removing all the linked chunk. If implementations
|
||||
// override this behavior, they should *also* include this code.
|
||||
self.handle_linked_chunk_updates(room_id, vec![Update::Clear]).await
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -81,6 +154,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Replaces the given media's content key with another one.
|
||||
@@ -155,6 +229,42 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
|
||||
|
||||
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
|
||||
/// keep media content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `policy` - The `MediaRetentionPolicy` to use.
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the current `MediaRetentionPolicy`.
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy;
|
||||
|
||||
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
|
||||
/// the media.
|
||||
///
|
||||
/// The change will be taken into account in the next cleanup.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequestParameters` of the file.
|
||||
///
|
||||
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
|
||||
/// ignored.
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Clean up the media cache with the current `MediaRetentionPolicy`.
|
||||
///
|
||||
/// If there is already an ongoing cleanup, this is a noop.
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
@@ -189,23 +299,68 @@ 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,
|
||||
content: Vec<u8>,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.add_media_content(request, content).await.map_err(Into::into)
|
||||
self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn replace_media_key(
|
||||
@@ -240,6 +395,29 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.0.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.0.clean_up_media_cache().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased [`EventCacheStore`].
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
//! Utilities for working with events to decide whether they are suitable for
|
||||
//! use as a [crate::Room::latest_event].
|
||||
|
||||
#![cfg(any(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::message::SyncRoomMessageEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
};
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent},
|
||||
poll::unstable_start::SyncUnstablePollStartEvent,
|
||||
relation::RelationType,
|
||||
room::{
|
||||
member::{MembershipState, SyncRoomMemberEvent},
|
||||
message::{MessageType, SyncRoomMessageEvent},
|
||||
power_levels::RoomPowerLevels,
|
||||
},
|
||||
sticker::SyncStickerEvent,
|
||||
AnySyncStateEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
},
|
||||
MxcUri, OwnedEventId, UserId,
|
||||
UserId,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedEventId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
@@ -71,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() {
|
||||
@@ -168,7 +169,7 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LatestEvent {
|
||||
/// The actual event.
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -182,7 +183,7 @@ pub struct LatestEvent {
|
||||
#[derive(Deserialize)]
|
||||
struct SerializedLatestEvent {
|
||||
/// The actual event.
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
|
||||
/// The member profile of the event' sender.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -215,7 +216,7 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
Err(err) => variant_errors.push(err),
|
||||
}
|
||||
|
||||
match serde_json::from_str::<SyncTimelineEvent>(raw.get()) {
|
||||
match serde_json::from_str::<TimelineEvent>(raw.get()) {
|
||||
Ok(value) => {
|
||||
return Ok(LatestEvent {
|
||||
event: value,
|
||||
@@ -234,13 +235,13 @@ impl<'de> Deserialize<'de> for LatestEvent {
|
||||
|
||||
impl LatestEvent {
|
||||
/// Create a new [`LatestEvent`] without the sender's profile.
|
||||
pub fn new(event: SyncTimelineEvent) -> Self {
|
||||
pub fn new(event: TimelineEvent) -> Self {
|
||||
Self { event, sender_profile: None, sender_name_is_ambiguous: None }
|
||||
}
|
||||
|
||||
/// Create a new [`LatestEvent`] with maybe the sender's profile.
|
||||
pub fn new_with_sender_details(
|
||||
event: SyncTimelineEvent,
|
||||
event: TimelineEvent,
|
||||
sender_profile: Option<MinimalRoomMemberEvent>,
|
||||
sender_name_is_ambiguous: Option<bool>,
|
||||
) -> Self {
|
||||
@@ -248,17 +249,17 @@ impl LatestEvent {
|
||||
}
|
||||
|
||||
/// Transform [`Self`] into an event.
|
||||
pub fn into_event(self) -> SyncTimelineEvent {
|
||||
pub fn into_event(self) -> TimelineEvent {
|
||||
self.event
|
||||
}
|
||||
|
||||
/// Get a reference to the event.
|
||||
pub fn event(&self) -> &SyncTimelineEvent {
|
||||
pub fn event(&self) -> &TimelineEvent {
|
||||
&self.event
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the event.
|
||||
pub fn event_mut(&mut self) -> &mut SyncTimelineEvent {
|
||||
pub fn event_mut(&mut self) -> &mut TimelineEvent {
|
||||
&mut self.event
|
||||
}
|
||||
|
||||
@@ -298,11 +299,16 @@ impl LatestEvent {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches::assert_matches;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use assert_matches2::assert_let;
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::serde::Raw;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{
|
||||
@@ -340,14 +346,16 @@ mod tests {
|
||||
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
|
||||
UnsignedRoomRedactionEvent,
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt,
|
||||
VoipVersionId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
|
||||
use super::LatestEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use super::{is_suitable_for_latest_event, PossibleLatestEvent};
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
@@ -372,6 +380,7 @@ mod tests {
|
||||
assert_eq!(m.content.msgtype.msgtype(), "m.image");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_polls_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
|
||||
@@ -395,6 +404,7 @@ mod tests {
|
||||
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
|
||||
@@ -417,6 +427,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
|
||||
@@ -439,6 +450,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
@@ -461,6 +473,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event =
|
||||
@@ -483,6 +496,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
@@ -511,6 +525,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
@@ -534,6 +549,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
@@ -553,6 +569,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_replacement_events_are_unsuitable() {
|
||||
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
|
||||
@@ -577,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)]
|
||||
@@ -584,7 +629,7 @@ mod tests {
|
||||
latest_event: LatestEvent,
|
||||
}
|
||||
|
||||
let event = SyncTimelineEvent::new(
|
||||
let event = TimelineEvent::new(
|
||||
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -37,7 +38,6 @@ mod rooms;
|
||||
|
||||
pub mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
@@ -56,9 +56,9 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
Room, RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo,
|
||||
RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMember, 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.
|
||||
@@ -123,7 +123,7 @@ use std::{
|
||||
};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use ruma::{
|
||||
events::{
|
||||
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
|
||||
@@ -202,7 +202,7 @@ impl RoomReadReceipts {
|
||||
///
|
||||
/// Returns whether a new event triggered a new unread/notification/mention.
|
||||
#[inline(always)]
|
||||
fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) {
|
||||
fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
|
||||
if marks_as_unread(event.raw(), user_id) {
|
||||
self.num_unread += 1;
|
||||
}
|
||||
@@ -210,7 +210,11 @@ impl RoomReadReceipts {
|
||||
let mut has_notify = false;
|
||||
let mut has_mention = false;
|
||||
|
||||
for action in &event.push_actions {
|
||||
let Some(actions) = event.push_actions.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for action in actions.iter() {
|
||||
if !has_notify && action.should_notify() {
|
||||
self.num_notifications += 1;
|
||||
has_notify = true;
|
||||
@@ -236,15 +240,14 @@ impl RoomReadReceipts {
|
||||
&mut self,
|
||||
receipt_event_id: &EventId,
|
||||
user_id: &UserId,
|
||||
events: impl IntoIterator<Item = &'a SyncTimelineEvent>,
|
||||
events: impl IntoIterator<Item = &'a TimelineEvent>,
|
||||
) -> bool {
|
||||
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
|
||||
@@ -269,11 +272,11 @@ impl RoomReadReceipts {
|
||||
pub trait PreviousEventsProvider: Send + Sync {
|
||||
/// Returns the list of known timeline events, in sync order, for the given
|
||||
/// room.
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<SyncTimelineEvent>;
|
||||
fn for_room(&self, room_id: &RoomId) -> Vector<TimelineEvent>;
|
||||
}
|
||||
|
||||
impl PreviousEventsProvider for () {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<SyncTimelineEvent> {
|
||||
fn for_room(&self, _: &RoomId) -> Vector<TimelineEvent> {
|
||||
Vector::new()
|
||||
}
|
||||
}
|
||||
@@ -292,7 +295,7 @@ struct ReceiptSelector {
|
||||
|
||||
impl ReceiptSelector {
|
||||
fn new(
|
||||
all_events: &Vector<SyncTimelineEvent>,
|
||||
all_events: &Vector<TimelineEvent>,
|
||||
latest_active_receipt_event: Option<&EventId>,
|
||||
) -> Self {
|
||||
let event_id_to_pos = Self::create_sync_index(all_events.iter());
|
||||
@@ -310,7 +313,7 @@ impl ReceiptSelector {
|
||||
/// Create a mapping of `event_id` -> sync order for all events that have an
|
||||
/// `event_id`.
|
||||
fn create_sync_index<'a>(
|
||||
events: impl Iterator<Item = &'a SyncTimelineEvent> + 'a,
|
||||
events: impl Iterator<Item = &'a TimelineEvent> + 'a,
|
||||
) -> BTreeMap<OwnedEventId, usize> {
|
||||
// TODO: this should be cached and incrementally updated.
|
||||
BTreeMap::from_iter(
|
||||
@@ -405,7 +408,7 @@ impl ReceiptSelector {
|
||||
/// Try to match an implicit receipt, that is, the one we get for events we
|
||||
/// sent ourselves.
|
||||
#[instrument(skip_all)]
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) {
|
||||
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[TimelineEvent]) {
|
||||
for ev in new_events {
|
||||
// Get the `sender` field, if any, or skip this event.
|
||||
let Ok(Some(sender)) = ev.raw().get_field::<OwnedUserId>("sender") else { continue };
|
||||
@@ -432,8 +435,8 @@ impl ReceiptSelector {
|
||||
/// Returns true if there's an event common to both groups of events, based on
|
||||
/// their event id.
|
||||
fn events_intersects<'a>(
|
||||
previous_events: impl Iterator<Item = &'a SyncTimelineEvent>,
|
||||
new_events: &[SyncTimelineEvent],
|
||||
previous_events: impl Iterator<Item = &'a TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
) -> bool {
|
||||
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
|
||||
new_events
|
||||
@@ -454,8 +457,8 @@ pub(crate) fn compute_unread_counts(
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
receipt_event: Option<&ReceiptEventContent>,
|
||||
previous_events: Vector<SyncTimelineEvent>,
|
||||
new_events: &[SyncTimelineEvent],
|
||||
previous_events: Vector<TimelineEvent>,
|
||||
new_events: &[TimelineEvent],
|
||||
read_receipts: &mut RoomReadReceipts,
|
||||
) {
|
||||
debug!(?read_receipts, "Starting.");
|
||||
@@ -620,11 +623,14 @@ mod tests {
|
||||
use std::{num::NonZeroUsize, ops::Not as _};
|
||||
|
||||
use eyeball_im::Vector;
|
||||
use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::{sync_timeline_event, EventBuilder};
|
||||
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
|
||||
use matrix_sdk_test::event_factory::EventFactory;
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::receipt::{ReceiptThread, ReceiptType},
|
||||
events::{
|
||||
receipt::{ReceiptThread, ReceiptType},
|
||||
room::{member::MembershipState, message::MessageType},
|
||||
},
|
||||
owned_event_id, owned_user_id,
|
||||
push::Action,
|
||||
room_id, user_id, EventId, UserId,
|
||||
@@ -638,24 +644,14 @@ mod tests {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
let f = EventFactory::new();
|
||||
|
||||
// A message from somebody else marks the room as unread...
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
});
|
||||
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(other_user_id).into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id));
|
||||
|
||||
// ... but a message from ourselves doesn't.
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
});
|
||||
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(user_id).into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
@@ -665,24 +661,16 @@ mod tests {
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// An edit to a message from somebody else doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": {
|
||||
"body": " * edited message",
|
||||
"m.new_content": {
|
||||
"body": "edited message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$someeventid:localhost",
|
||||
"rel_type": "m.replace"
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.text_msg("* edited message")
|
||||
.edit(
|
||||
event_id!("$someeventid:localhost"),
|
||||
MessageType::text_plain("edited message").into(),
|
||||
)
|
||||
.event_id(event_id!("$ida"))
|
||||
.sender(other_user_id)
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
|
||||
@@ -692,19 +680,11 @@ mod tests {
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// A redact of a message from somebody else doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"reason": "🛑"
|
||||
},
|
||||
"event_id": "$151957878228ssqrJ:localhost",
|
||||
"origin_server_ts": 151957878000000_u64,
|
||||
"sender": other_user_id,
|
||||
"type": "m.room.redaction",
|
||||
"redacts": "$151957878228ssqrj:localhost",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.redaction(event_id!("$151957878228ssqrj:localhost"))
|
||||
.sender(other_user_id)
|
||||
.event_id(event_id!("$151957878228ssqrJ:localhost"))
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
@@ -715,22 +695,11 @@ mod tests {
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
|
||||
// A reaction from somebody else to a message doesn't mark the room as unread.
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"origin_server_ts": 159027581000000_u64,
|
||||
"sender": other_user_id,
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
let ev = EventFactory::new()
|
||||
.reaction(event_id!("$15275047031IXQRj:localhost"), "👍")
|
||||
.sender(other_user_id)
|
||||
.event_id(event_id!("$15275047031IXQRi:localhost"))
|
||||
.into_raw_sync();
|
||||
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
}
|
||||
@@ -739,18 +708,13 @@ mod tests {
|
||||
fn test_state_event_doesnt_mark_as_unread() {
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let event_id = event_id!("$1");
|
||||
let ev = sync_timeline_event!({
|
||||
"content": {
|
||||
"displayname": "Alice",
|
||||
"membership": "join",
|
||||
},
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 1432135524678u64,
|
||||
"sender": user_id,
|
||||
"state_key": user_id,
|
||||
"type": "m.room.member",
|
||||
});
|
||||
|
||||
let ev = EventFactory::new()
|
||||
.member(user_id)
|
||||
.membership(MembershipState::Join)
|
||||
.display_name("Alice")
|
||||
.event_id(event_id)
|
||||
.into_raw_sync();
|
||||
assert!(marks_as_unread(&ev, user_id).not());
|
||||
|
||||
let other_user_id = user_id!("@bob:example.org");
|
||||
@@ -759,17 +723,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_count_unread_and_mentions() {
|
||||
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new_with_push_actions(
|
||||
sync_timeline_event!({
|
||||
"sender": user_id,
|
||||
"type": "m.room.message",
|
||||
"event_id": "$ida",
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
}),
|
||||
push_actions,
|
||||
)
|
||||
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> TimelineEvent {
|
||||
let mut ev = EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$ida"))
|
||||
.into_event();
|
||||
ev.push_actions = Some(push_actions);
|
||||
ev
|
||||
}
|
||||
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
@@ -843,14 +804,12 @@ mod tests {
|
||||
|
||||
// When provided with one event, that's not the receipt event, we don't count
|
||||
// it.
|
||||
fn make_event(event_id: &EventId) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": "@bob:example.org",
|
||||
"type": "m.room.message",
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 12344446,
|
||||
"content": { "body":"A", "msgtype": "m.text" },
|
||||
}))
|
||||
fn make_event(event_id: &EventId) -> TimelineEvent {
|
||||
EventFactory::new()
|
||||
.text_msg("A")
|
||||
.sender(user_id!("@bob:example.org"))
|
||||
.event_id(event_id)
|
||||
.into()
|
||||
}
|
||||
|
||||
let mut receipts = RoomReadReceipts {
|
||||
@@ -948,20 +907,6 @@ mod tests {
|
||||
assert_eq!(receipts.num_mentions, 0);
|
||||
}
|
||||
|
||||
fn sync_timeline_message(
|
||||
sender: &UserId,
|
||||
event_id: impl serde::Serialize,
|
||||
body: impl serde::Serialize,
|
||||
) -> SyncTimelineEvent {
|
||||
SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": sender,
|
||||
"type": "m.room.message",
|
||||
"event_id": event_id,
|
||||
"origin_server_ts": 42,
|
||||
"content": { "body": body, "msgtype": "m.text" },
|
||||
}))
|
||||
}
|
||||
|
||||
/// Smoke test for `compute_unread_counts`.
|
||||
#[test]
|
||||
fn test_basic_compute_unread_counts() {
|
||||
@@ -972,15 +917,14 @@ mod tests {
|
||||
|
||||
let mut previous_events = Vector::new();
|
||||
|
||||
let ev1 = sync_timeline_message(other_user_id, receipt_event_id, "A");
|
||||
let ev2 = sync_timeline_message(other_user_id, "$2", "A");
|
||||
let f = EventFactory::new();
|
||||
let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event();
|
||||
let ev2 = f.text_msg("A").sender(other_user_id).event_id(event_id!("$2")).into_event();
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
receipt_event_id.to_owned(),
|
||||
ReceiptType::Read,
|
||||
user_id.to_owned(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = Default::default();
|
||||
compute_unread_counts(
|
||||
@@ -999,7 +943,8 @@ mod tests {
|
||||
previous_events.push_back(ev1);
|
||||
previous_events.push_back(ev2);
|
||||
|
||||
let new_event = sync_timeline_message(other_user_id, "$3", "A");
|
||||
let new_event =
|
||||
f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event();
|
||||
compute_unread_counts(
|
||||
user_id,
|
||||
room_id,
|
||||
@@ -1013,13 +958,14 @@ mod tests {
|
||||
assert_eq!(read_receipts.num_unread, 2);
|
||||
}
|
||||
|
||||
fn make_test_events(user_id: &UserId) -> Vector<SyncTimelineEvent> {
|
||||
let ev1 = sync_timeline_message(user_id, "$1", "With the lights out, it's less dangerous");
|
||||
let ev2 = sync_timeline_message(user_id, "$2", "Here we are now, entertain us");
|
||||
let ev3 = sync_timeline_message(user_id, "$3", "I feel stupid and contagious");
|
||||
let ev4 = sync_timeline_message(user_id, "$4", "Here we are now, entertain us");
|
||||
let ev5 = sync_timeline_message(user_id, "$5", "Hello, hello, hello, how low?");
|
||||
vec![ev1, ev2, ev3, ev4, ev5].into()
|
||||
fn make_test_events(user_id: &UserId) -> Vector<TimelineEvent> {
|
||||
let f = EventFactory::new().sender(user_id);
|
||||
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
|
||||
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
|
||||
let ev3 = f.text_msg("I feel stupid and contagious").event_id(event_id!("$3"));
|
||||
let ev4 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$4"));
|
||||
let ev5 = f.text_msg("Hello, hello, hello, how low?").event_id(event_id!("$5"));
|
||||
[ev1, ev2, ev3, ev4, ev5].into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
/// Test that when multiple receipts come in a single event, we can still
|
||||
@@ -1035,30 +981,32 @@ mod tests {
|
||||
|
||||
// Given a receipt event marking events 1-3 as read using a combination of
|
||||
// different thread and privacy types,
|
||||
let f = EventFactory::new();
|
||||
for receipt_type_1 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
for receipt_thread_1 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
|
||||
for receipt_type_2 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
|
||||
for receipt_thread_2 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([
|
||||
(
|
||||
owned_event_id!("$2"),
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(
|
||||
event_id!("$2"),
|
||||
user_id,
|
||||
receipt_type_1.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_1.clone(),
|
||||
),
|
||||
(
|
||||
owned_event_id!("$3"),
|
||||
)
|
||||
.add(
|
||||
event_id!("$3"),
|
||||
user_id,
|
||||
receipt_type_2.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_2.clone(),
|
||||
),
|
||||
(
|
||||
owned_event_id!("$1"),
|
||||
)
|
||||
.add(
|
||||
event_id!("$1"),
|
||||
user_id,
|
||||
receipt_type_1.clone(),
|
||||
user_id.to_owned(),
|
||||
receipt_thread_2.clone(),
|
||||
),
|
||||
]);
|
||||
)
|
||||
.into_content();
|
||||
|
||||
// When I compute the notifications for this room (with no new events),
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
@@ -1118,12 +1066,10 @@ mod tests {
|
||||
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$6"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
assert!(read_receipts.pending.is_empty());
|
||||
@@ -1154,12 +1100,10 @@ mod tests {
|
||||
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$1"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = EventFactory::new()
|
||||
.read_receipts()
|
||||
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.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
|
||||
@@ -1190,12 +1134,7 @@ mod tests {
|
||||
let events = make_test_events(uid);
|
||||
|
||||
// An event with no id.
|
||||
let ev6 = SyncTimelineEvent::new(sync_timeline_event!({
|
||||
"sender": uid,
|
||||
"type": "m.room.message",
|
||||
"origin_server_ts": 42,
|
||||
"content": { "body": "yolo", "msgtype": "m.text" },
|
||||
}));
|
||||
let ev6 = EventFactory::new().text_msg("yolo").sender(uid).no_event_id().into_event();
|
||||
|
||||
let index = ReceiptSelector::create_sync_index(events.iter().chain(&[ev6]));
|
||||
|
||||
@@ -1261,8 +1200,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_noop() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1296,8 +1236,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_doesnt_match_known_events() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1332,8 +1273,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_matches_known_events_no_initial() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1373,8 +1315,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_pending_receipts_matches_known_events_with_initial() {
|
||||
let sender = user_id!("@bob:example.org");
|
||||
let ev1 = sync_timeline_message(sender, event_id!("$1"), "yo");
|
||||
let ev2 = sync_timeline_message(sender, event_id!("$2"), "well?");
|
||||
let f = EventFactory::new().sender(sender);
|
||||
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
|
||||
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
|
||||
let events: Vector<_> = vec![ev1, ev2].into();
|
||||
|
||||
{
|
||||
@@ -1412,21 +1355,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_receipt_selector_handle_new_receipt() {
|
||||
let myself = owned_user_id!("@alice:example.org");
|
||||
let myself = user_id!("@alice:example.org");
|
||||
let events = make_test_events(user_id!("@bob:example.org"));
|
||||
|
||||
let f = EventFactory::new();
|
||||
{
|
||||
// Thread receipts are ignored.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$5"),
|
||||
ReceiptType::Read,
|
||||
myself.clone(),
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(
|
||||
event_id!("$5"),
|
||||
myself,
|
||||
ReceiptType::Read,
|
||||
ReceiptThread::Thread(owned_event_id!("$2")),
|
||||
)
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1440,14 +1387,12 @@ mod tests {
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$6"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
assert_eq!(pending.len(), 1);
|
||||
|
||||
@@ -1460,14 +1405,12 @@ mod tests {
|
||||
// receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1479,14 +1422,12 @@ mod tests {
|
||||
// better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$4")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1498,14 +1439,12 @@ mod tests {
|
||||
// new better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
receipt_type.clone(),
|
||||
myself.clone(),
|
||||
receipt_thread.clone(),
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert!(pending.is_empty());
|
||||
|
||||
let best_receipt = selector.select();
|
||||
@@ -1519,23 +1458,14 @@ mod tests {
|
||||
// new better receipt.
|
||||
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
|
||||
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([
|
||||
(
|
||||
owned_event_id!("$4"),
|
||||
ReceiptType::ReadPrivate,
|
||||
myself.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
),
|
||||
(
|
||||
owned_event_id!("$6"),
|
||||
ReceiptType::ReadPrivate,
|
||||
myself.clone(),
|
||||
ReceiptThread::Main,
|
||||
),
|
||||
(owned_event_id!("$3"), ReceiptType::Read, myself.clone(), ReceiptThread::Main),
|
||||
]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.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)
|
||||
.into_content();
|
||||
|
||||
let pending = selector.handle_new_receipt(&myself, &receipt_event);
|
||||
let pending = selector.handle_new_receipt(myself, &receipt_event);
|
||||
assert_eq!(pending.len(), 1);
|
||||
assert_eq!(pending[0], event_id!("$6"));
|
||||
|
||||
@@ -1560,8 +1490,16 @@ mod tests {
|
||||
assert!(best_receipt.is_none());
|
||||
|
||||
// Now, if there are events I've written too...
|
||||
events.push_back(sync_timeline_message(&myself, "$6", "A mulatto, an albino"));
|
||||
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(&myself)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
|
||||
let mut selector = ReceiptSelector::new(&events, None);
|
||||
// And I search for my implicit read receipt,
|
||||
@@ -1573,7 +1511,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_compute_unread_counts_with_implicit_receipt() {
|
||||
let user_id = owned_user_id!("@alice:example.org");
|
||||
let user_id = user_id!("@alice:example.org");
|
||||
let bob = user_id!("@bob:example.org");
|
||||
let room_id = room_id!("!room:example.org");
|
||||
|
||||
@@ -1581,28 +1519,36 @@ mod tests {
|
||||
let mut events = make_test_events(bob);
|
||||
|
||||
// One by me,
|
||||
events.push_back(sync_timeline_message(&user_id, "$6", "A mulatto, an albino"));
|
||||
let f = EventFactory::new();
|
||||
events.push_back(
|
||||
f.text_msg("A mulatto, an albino")
|
||||
.sender(user_id)
|
||||
.event_id(event_id!("$6"))
|
||||
.into_event(),
|
||||
);
|
||||
|
||||
// And others by Bob,
|
||||
events.push_back(sync_timeline_message(bob, "$7", "A mosquito, my libido"));
|
||||
events.push_back(sync_timeline_message(bob, "$8", "A denial, a denial"));
|
||||
events.push_back(
|
||||
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
|
||||
);
|
||||
events.push_back(
|
||||
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
|
||||
);
|
||||
|
||||
let events: Vec<_> = events.into_iter().collect();
|
||||
|
||||
// I have a read receipt attached to one of Bob's event sent before my message,
|
||||
let receipt_event = EventBuilder::new().make_receipt_event_content([(
|
||||
owned_event_id!("$3"),
|
||||
ReceiptType::Read,
|
||||
user_id.clone(),
|
||||
ReceiptThread::Unthreaded,
|
||||
)]);
|
||||
let receipt_event = f
|
||||
.read_receipts()
|
||||
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
|
||||
.into_content();
|
||||
|
||||
let mut read_receipts = RoomReadReceipts::default();
|
||||
|
||||
// And I compute the unread counts for all those new events (no previous events
|
||||
// in that room),
|
||||
compute_unread_counts(
|
||||
&user_id,
|
||||
user_id,
|
||||
room_id,
|
||||
Some(&receipt_event),
|
||||
Vector::new(),
|
||||
|
||||
+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::{
|
||||
Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
|
||||
RoomStateFilter,
|
||||
apply_redaction, EncryptionState, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate,
|
||||
RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState, RoomStateFilter,
|
||||
};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use std::sync::RwLock as SyncRwLock;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
collections::{BTreeMap, BTreeSet, HashSet},
|
||||
mem,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
@@ -24,12 +24,9 @@ use as_variant::as_variant;
|
||||
use bitflags::bitflags;
|
||||
use eyeball::{AsyncLock, ObservableWriteGuard, SharedObservable, Subscriber};
|
||||
use futures_util::{Stream, StreamExt};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEventKind;
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_common::ring_buffer::RingBuffer;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use ruma::events::AnySyncTimelineEvent;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
|
||||
events::{
|
||||
@@ -51,7 +48,7 @@ use ruma::{
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
},
|
||||
tag::{TagEventContent, Tags},
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
|
||||
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent,
|
||||
RoomAccountDataEventType, StateEventType, SyncStateEvent,
|
||||
},
|
||||
room::RoomType,
|
||||
@@ -67,12 +64,11 @@ use super::{
|
||||
members::MemberRoomInfo, BaseRoomInfo, RoomCreateWithCreatorEventContent, RoomDisplayName,
|
||||
RoomMember, RoomNotableTags,
|
||||
};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
deserialized_responses::{
|
||||
DisplayName, MemberEvent, RawSyncOrStrippedState, SyncOrStrippedState,
|
||||
DisplayName, MemberEvent, RawMemberEvent, RawSyncOrStrippedState, SyncOrStrippedState,
|
||||
},
|
||||
latest_event::LatestEvent,
|
||||
notification_settings::RoomNotificationMode,
|
||||
read_receipts::RoomReadReceipts,
|
||||
store::{DynStateStore, Result as StoreResult, StateStoreExt},
|
||||
@@ -166,7 +162,7 @@ pub struct Room {
|
||||
/// not sure whether holding too many of them might make the cache too
|
||||
/// slow to load on startup. Keeping them here means they are not cached
|
||||
/// to disk but held in memory.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
|
||||
|
||||
/// A map for ids of room membership events in the knocking state linked to
|
||||
@@ -174,6 +170,9 @@ pub struct Room {
|
||||
/// user has marked as seen so they can be ignored.
|
||||
pub seen_knock_request_ids_map:
|
||||
SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
|
||||
|
||||
/// A sender that will notify receivers when room member updates happen.
|
||||
pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
|
||||
}
|
||||
|
||||
/// The room summary containing member counts and members that should be used to
|
||||
@@ -262,12 +261,19 @@ fn heroes_filter<'a>(
|
||||
move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id)
|
||||
}
|
||||
|
||||
/// The kind of room member updates that just happened.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RoomMembersUpdate {
|
||||
/// The whole list room members was reloaded.
|
||||
FullReload,
|
||||
/// A few members were updated, their user ids are included.
|
||||
Partial(BTreeSet<OwnedUserId>),
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// The size of the latest_encrypted_events RingBuffer
|
||||
// SAFETY: `new_unchecked` is safe because 10 is not zero.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize =
|
||||
unsafe { std::num::NonZeroUsize::new_unchecked(10) };
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize = std::num::NonZeroUsize::new(10).unwrap();
|
||||
|
||||
pub(crate) fn new(
|
||||
own_user_id: &UserId,
|
||||
@@ -286,17 +292,19 @@ impl Room {
|
||||
room_info: RoomInfo,
|
||||
room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
|
||||
) -> Self {
|
||||
let (room_member_updates_sender, _) = broadcast::channel(10);
|
||||
Self {
|
||||
own_user_id: own_user_id.into(),
|
||||
room_id: room_info.room_id.clone(),
|
||||
store,
|
||||
inner: SharedObservable::new(room_info),
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
|
||||
Self::MAX_ENCRYPTED_EVENTS,
|
||||
))),
|
||||
room_info_notable_update_sender,
|
||||
seen_knock_request_ids_map: SharedObservable::new_async(None),
|
||||
room_member_updates_sender,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +389,17 @@ impl Room {
|
||||
self.inner.read().members_synced
|
||||
}
|
||||
|
||||
/// Mark this Room as holding all member information.
|
||||
///
|
||||
/// Useful in tests if we want to persuade the Room not to sync when asked
|
||||
/// about its members.
|
||||
#[cfg(feature = "testing")]
|
||||
pub fn mark_members_synced(&self) {
|
||||
self.inner.update(|info| {
|
||||
info.members_synced = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Mark this Room as still missing member information.
|
||||
pub fn mark_members_missing(&self) {
|
||||
self.inner.update_if(|info| {
|
||||
@@ -407,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> {
|
||||
@@ -512,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
|
||||
@@ -609,18 +618,40 @@ impl Room {
|
||||
self.inner.read().active_room_call_participants()
|
||||
}
|
||||
|
||||
/// Return the cached display name of the room if it was provided via sync,
|
||||
/// or otherwise calculate it, taking into account its name, aliases and
|
||||
/// members.
|
||||
/// Calculate a room's display name, or return the cached value, taking into
|
||||
/// account its name, aliases and members.
|
||||
///
|
||||
/// The display name is calculated according to [this algorithm][spec].
|
||||
///
|
||||
/// This is automatically recomputed on every successful sync, and the
|
||||
/// cached result can be retrieved in
|
||||
/// [`Self::cached_display_name`].
|
||||
/// While the underlying computation can be slow, the result is cached and
|
||||
/// returned on the following calls. The cache is also filled on every
|
||||
/// successful sync, since a sync may cause a change in the display
|
||||
/// name.
|
||||
///
|
||||
/// If you need a variant that's sync (but with the drawback that it returns
|
||||
/// an `Option`), consider using [`Room::cached_display_name`].
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
pub async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
pub async fn display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
if let Some(name) = self.cached_display_name() {
|
||||
Ok(name)
|
||||
} else {
|
||||
self.compute_display_name().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Force recalculating a room's display name, taking into account its name,
|
||||
/// aliases and members.
|
||||
///
|
||||
/// The display name is calculated according to [this algorithm][spec].
|
||||
///
|
||||
/// ⚠ This may be slowish to compute. As such, the result is cached and can
|
||||
/// be retrieved via [`Room::cached_display_name`] (sync, returns an option)
|
||||
/// or [`Room::display_name`] (async, always returns a value), which should
|
||||
/// be preferred in general.
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
pub(crate) async fn compute_display_name(&self) -> StoreResult<RoomDisplayName> {
|
||||
enum DisplayNameOrSummary {
|
||||
Summary(RoomSummary),
|
||||
DisplayName(RoomDisplayName),
|
||||
@@ -857,8 +888,7 @@ impl Room {
|
||||
|
||||
/// Returns the cached computed display name, if available.
|
||||
///
|
||||
/// This cache is refilled every time we call
|
||||
/// [`Self::compute_display_name`].
|
||||
/// This cache is refilled every time we call [`Self::display_name`].
|
||||
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
|
||||
self.inner.read().cached_display_name.clone()
|
||||
}
|
||||
@@ -890,7 +920,6 @@ impl Room {
|
||||
|
||||
/// Return the last event in this room, if one has been cached during
|
||||
/// sliding sync.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn latest_event(&self) -> Option<LatestEvent> {
|
||||
self.inner.read().latest_event.as_deref().cloned()
|
||||
}
|
||||
@@ -899,7 +928,7 @@ impl Room {
|
||||
/// to decrypt these, the most recent relevant one will replace
|
||||
/// latest_event. (We can't tell which one is relevant until
|
||||
/// they are decrypted.)
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
|
||||
self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
|
||||
}
|
||||
@@ -914,7 +943,7 @@ impl Room {
|
||||
///
|
||||
/// It is the responsibility of the caller to apply the changes into the
|
||||
/// state store after calling this function.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) fn on_latest_event_decrypted(
|
||||
&self,
|
||||
latest_event: Box<LatestEvent>,
|
||||
@@ -1160,7 +1189,6 @@ impl Room {
|
||||
/// Returns the recency stamp of the room.
|
||||
///
|
||||
/// Please read `RoomInfo::recency_stamp` to learn more.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn recency_stamp(&self) -> Option<u64> {
|
||||
self.inner.read().recency_stamp
|
||||
}
|
||||
@@ -1207,28 +1235,61 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_seen_events_guard = self.seen_knock_request_ids_map.write().await;
|
||||
// We're not calling `get_seen_join_request_ids` here because we need to keep
|
||||
// the Mutex's guard until we've updated the data
|
||||
let mut current_seen_events = if current_seen_events_guard.is_none() {
|
||||
self.load_cached_knock_request_ids().await?
|
||||
} else {
|
||||
current_seen_events_guard.clone().unwrap()
|
||||
};
|
||||
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
|
||||
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
|
||||
|
||||
current_seen_events.extend(event_to_user_ids);
|
||||
|
||||
ObservableWriteGuard::set(
|
||||
&mut current_seen_events_guard,
|
||||
Some(current_seen_events.clone()),
|
||||
);
|
||||
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
|
||||
|
||||
self.store
|
||||
.set_kv_data(
|
||||
StateStoreDataKey::SeenKnockRequests(self.room_id()),
|
||||
StateStoreDataValue::SeenKnockRequests(current_seen_events),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the seen knock request ids that are no longer valid given the
|
||||
/// current room members.
|
||||
pub async fn remove_outdated_seen_knock_requests_ids(&self) -> StoreResult<()> {
|
||||
let current_seen_events_guard = self.get_write_guarded_current_knock_request_ids().await?;
|
||||
let mut current_seen_events = current_seen_events_guard.clone().unwrap_or_default();
|
||||
|
||||
// Get and deserialize the member events for the seen knock requests
|
||||
let keys: Vec<OwnedUserId> = current_seen_events.values().map(|id| id.to_owned()).collect();
|
||||
let raw_member_events: Vec<RawMemberEvent> =
|
||||
self.store.get_state_events_for_keys_static(self.room_id(), &keys).await?;
|
||||
let member_events = raw_member_events
|
||||
.into_iter()
|
||||
.map(|raw| raw.deserialize())
|
||||
.collect::<Result<Vec<MemberEvent>, _>>()?;
|
||||
|
||||
let mut ids_to_remove = Vec::new();
|
||||
|
||||
for (event_id, user_id) in current_seen_events.iter() {
|
||||
// Check the seen knock request ids against the current room member events for
|
||||
// the room members associated to them
|
||||
let matching_member = member_events.iter().find(|event| event.user_id() == user_id);
|
||||
|
||||
if let Some(member) = matching_member {
|
||||
let member_event_id = member.event_id();
|
||||
// If the member event is not a knock or it's different knock, it's outdated
|
||||
if *member.membership() != MembershipState::Knock
|
||||
|| member_event_id.is_some_and(|id| id != event_id)
|
||||
{
|
||||
ids_to_remove.push(event_id.to_owned());
|
||||
}
|
||||
} else {
|
||||
ids_to_remove.push(event_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no ids to remove, do nothing
|
||||
if ids_to_remove.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for event_id in ids_to_remove {
|
||||
current_seen_events.remove(&event_id);
|
||||
}
|
||||
|
||||
self.update_seen_knock_request_ids(current_seen_events_guard, current_seen_events).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1237,27 +1298,46 @@ impl Room {
|
||||
pub async fn get_seen_knock_request_ids(
|
||||
&self,
|
||||
) -> Result<BTreeMap<OwnedEventId, OwnedUserId>, StoreError> {
|
||||
let mut guard = self.seen_knock_request_ids_map.write().await;
|
||||
if guard.is_none() {
|
||||
ObservableWriteGuard::set(
|
||||
&mut guard,
|
||||
Some(self.load_cached_knock_request_ids().await?),
|
||||
);
|
||||
}
|
||||
Ok(guard.clone().unwrap_or_default())
|
||||
Ok(self.get_write_guarded_current_knock_request_ids().await?.clone().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// This loads the current list of seen knock request ids from the state
|
||||
/// store.
|
||||
async fn load_cached_knock_request_ids(
|
||||
async fn get_write_guarded_current_knock_request_ids(
|
||||
&self,
|
||||
) -> StoreResult<BTreeMap<OwnedEventId, OwnedUserId>> {
|
||||
Ok(self
|
||||
.store
|
||||
.get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
|
||||
.await?
|
||||
.and_then(|v| v.into_seen_knock_requests())
|
||||
.unwrap_or_default())
|
||||
) -> StoreResult<ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>>
|
||||
{
|
||||
let mut guard = self.seen_knock_request_ids_map.write().await;
|
||||
// If there are no loaded request ids yet
|
||||
if guard.is_none() {
|
||||
// Load the values from the store and update the shared observable contents
|
||||
let updated_seen_ids = self
|
||||
.store
|
||||
.get_kv_data(StateStoreDataKey::SeenKnockRequests(self.room_id()))
|
||||
.await?
|
||||
.and_then(|v| v.into_seen_knock_requests())
|
||||
.unwrap_or_default();
|
||||
|
||||
ObservableWriteGuard::set(&mut guard, Some(updated_seen_ids));
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
async fn update_seen_knock_request_ids(
|
||||
&self,
|
||||
mut guard: ObservableWriteGuard<'_, Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
|
||||
new_value: BTreeMap<OwnedEventId, OwnedUserId>,
|
||||
) -> StoreResult<()> {
|
||||
// Save the new values to the shared observable
|
||||
ObservableWriteGuard::set(&mut guard, Some(new_value.clone()));
|
||||
|
||||
// Save them into the store too
|
||||
self.store
|
||||
.set_kv_data(
|
||||
StateStoreDataKey::SeenKnockRequests(self.room_id()),
|
||||
StateStoreDataValue::SeenKnockRequests(new_value),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1318,7 +1398,6 @@ pub struct RoomInfo {
|
||||
pub(crate) encryption_state_synced: bool,
|
||||
|
||||
/// The last event send by sliding sync
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) latest_event: Option<Box<LatestEvent>>,
|
||||
|
||||
/// Information about read receipts for this room.
|
||||
@@ -1352,7 +1431,6 @@ pub struct RoomInfo {
|
||||
/// Sliding Sync might "ignore” some events when computing the recency
|
||||
/// stamp of the room. Thus, using this `recency_stamp` value is
|
||||
/// more accurate than relying on the latest event.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
#[serde(default)]
|
||||
pub(crate) recency_stamp: Option<u64>,
|
||||
}
|
||||
@@ -1388,14 +1466,12 @@ impl RoomInfo {
|
||||
last_prev_batch: None,
|
||||
sync_info: SyncInfo::NoState,
|
||||
encryption_state_synced: false,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: None,
|
||||
read_receipts: Default::default(),
|
||||
base_info: Box::new(BaseRoomInfo::new()),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
recency_stamp: None,
|
||||
}
|
||||
}
|
||||
@@ -1490,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.
|
||||
@@ -1500,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.
|
||||
@@ -1540,7 +1641,6 @@ impl RoomInfo {
|
||||
};
|
||||
tracing::Span::current().record("redacts", debug(redacts));
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
if let Some(latest_event) = &mut self.latest_event {
|
||||
tracing::trace!("Checking if redaction applies to latest event");
|
||||
if latest_event.event_id().as_deref() == Some(redacts) {
|
||||
@@ -1630,19 +1730,16 @@ impl RoomInfo {
|
||||
}
|
||||
|
||||
/// Updates the joined member count.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_joined_member_count(&mut self, count: u64) {
|
||||
self.summary.joined_member_count = count;
|
||||
}
|
||||
|
||||
/// Updates the invited member count.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_invited_member_count(&mut self, count: u64) {
|
||||
self.summary.invited_member_count = count;
|
||||
}
|
||||
|
||||
/// Updates the room heroes.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
|
||||
self.summary.room_heroes = heroes;
|
||||
}
|
||||
@@ -1842,7 +1939,6 @@ impl RoomInfo {
|
||||
}
|
||||
|
||||
/// Returns the latest (decrypted) event recorded for this room.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn latest_event(&self) -> Option<&LatestEvent> {
|
||||
self.latest_event.as_deref()
|
||||
}
|
||||
@@ -1850,7 +1946,6 @@ impl RoomInfo {
|
||||
/// Updates the recency stamp of this room.
|
||||
///
|
||||
/// Please read [`Self::recency_stamp`] to learn more.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
|
||||
self.recency_stamp = Some(stamp);
|
||||
}
|
||||
@@ -1936,8 +2031,9 @@ impl RoomInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn apply_redaction(
|
||||
/// Apply a redaction to the given target `event`, given the raw redaction event
|
||||
/// and the room version.
|
||||
pub fn apply_redaction(
|
||||
event: &Raw<AnySyncTimelineEvent>,
|
||||
raw_redaction: &Raw<SyncRoomRedactionEvent>,
|
||||
room_version: &RoomVersionId,
|
||||
@@ -1963,7 +2059,7 @@ fn apply_redaction(
|
||||
let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because));
|
||||
|
||||
if let Err(e) = redact_result {
|
||||
warn!("Failed to redact latest event: {e}");
|
||||
warn!("Failed to redact event: {e}");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2070,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::{
|
||||
@@ -2080,9 +2203,9 @@ mod tests {
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use assign::assign;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use matrix_sdk_test::{
|
||||
async_test,
|
||||
event_factory::EventFactory,
|
||||
@@ -2117,19 +2240,23 @@ 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};
|
||||
#[cfg(any(feature = "experimental-sliding-sync", feature = "e2e-encryption"))]
|
||||
use crate::latest_event::LatestEvent;
|
||||
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,
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn test_room_info_serialization() {
|
||||
// This test exists to make sure we don't accidentally change the
|
||||
// serialized format for `RoomInfo`.
|
||||
@@ -2161,7 +2288,7 @@ mod tests {
|
||||
last_prev_batch: Some("pb".to_owned()),
|
||||
sync_info: SyncInfo::FullySynced,
|
||||
encryption_state_synced: true,
|
||||
latest_event: Some(Box::new(LatestEvent::new(SyncTimelineEvent::new(
|
||||
latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::new(
|
||||
Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
|
||||
)))),
|
||||
base_info: Box::new(
|
||||
@@ -2316,7 +2443,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn test_room_info_deserialization() {
|
||||
use ruma::{owned_mxc_uri, owned_user_id};
|
||||
|
||||
@@ -2411,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,
|
||||
)
|
||||
@@ -2453,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);
|
||||
@@ -2475,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);
|
||||
@@ -2491,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,
|
||||
)
|
||||
@@ -2533,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);
|
||||
@@ -2555,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);
|
||||
@@ -3066,27 +3224,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
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(),
|
||||
},
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
RoomLoadSettings::default(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -3106,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.
|
||||
@@ -3134,8 +3296,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -3164,8 +3326,8 @@ mod tests {
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -3203,8 +3365,8 @@ mod tests {
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_replacing_the_newest_event_leaves_none_left() {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -3236,7 +3398,7 @@ mod tests {
|
||||
assert_eq!(enc_evs.len(), 0);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn add_encrypted_event(room: &Room, event_id: &str) {
|
||||
room.latest_encrypted_events
|
||||
.write()
|
||||
@@ -3244,9 +3406,9 @@ mod tests {
|
||||
.push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
|
||||
Box::new(LatestEvent::new(SyncTimelineEvent::new(
|
||||
Box::new(LatestEvent::new(TimelineEvent::new(
|
||||
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
|
||||
)))
|
||||
}
|
||||
@@ -3491,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);
|
||||
@@ -3513,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]
|
||||
|
||||
+529
-365
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>,
|
||||
@@ -430,7 +430,7 @@ mod test {
|
||||
assert_ambiguity!(
|
||||
[("@alice:localhost", "alice"), ("@bob:localhost", "аlice")],
|
||||
[("alice", true)],
|
||||
"Bob tries to impersonate Alice using a cyrilic а"
|
||||
"Bob tries to impersonate Alice using a cyrillic а"
|
||||
);
|
||||
|
||||
assert_ambiguity!(
|
||||
|
||||
@@ -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,20 +22,20 @@ 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,
|
||||
uint, user_id, EventId, OwnedEventId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
uint, user_id, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId,
|
||||
TransactionId, UserId,
|
||||
};
|
||||
use serde_json::{json, value::Value as JsonValue};
|
||||
|
||||
use super::{
|
||||
send_queue::SentRequestKey, DependentQueuedRequestKind, DisplayName, DynStateStore,
|
||||
ServerCapabilities,
|
||||
RoomLoadSettings, ServerCapabilities,
|
||||
};
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
@@ -46,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 {
|
||||
@@ -94,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))]
|
||||
@@ -173,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);
|
||||
@@ -267,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?
|
||||
@@ -929,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
|
||||
@@ -945,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()]);
|
||||
@@ -958,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()]);
|
||||
@@ -980,13 +984,21 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let ev =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("sup").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn.clone(), ev.into(), 0).await?;
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev.into(),
|
||||
0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add a single dependent queue request.
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn,
|
||||
ChildTransactionId::new(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await?;
|
||||
@@ -994,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!(
|
||||
@@ -1048,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(())
|
||||
}
|
||||
|
||||
@@ -1242,7 +1261,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("msg0").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn0.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event0.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Reading it will work.
|
||||
let pending = self.load_send_queue_requests(room_id).await.unwrap();
|
||||
@@ -1266,7 +1293,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.save_send_queue_request(room_id, txn, event.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn,
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Reading all the events should work.
|
||||
@@ -1364,7 +1399,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id2, txn.clone(), event.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id2,
|
||||
txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Add and remove one event for room3.
|
||||
@@ -1374,7 +1417,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("room3").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id3, txn.clone(), event.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id3,
|
||||
txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.remove_send_queue_request(room_id3, &txn).await.unwrap();
|
||||
}
|
||||
@@ -1399,21 +1450,45 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let ev0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low0").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, low0_txn.clone(), ev0.into(), 2).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
low0_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev0.into(),
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Saving one request with higher priority should work.
|
||||
let high_txn = TransactionId::new();
|
||||
let ev1 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("high").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, high_txn.clone(), ev1.into(), 10).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
high_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev1.into(),
|
||||
10,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Saving another request with the low priority should work.
|
||||
let low1_txn = TransactionId::new();
|
||||
let ev2 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("low1").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, low1_txn.clone(), ev2.into(), 2).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
low1_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
ev2.into(),
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The requests should be ordered from higher priority to lower, and when equal,
|
||||
// should use the insertion order instead.
|
||||
@@ -1453,7 +1528,15 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event0 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn0.clone(), event0.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn0.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event0.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// No dependents, to start with.
|
||||
assert!(self.load_dependent_queued_requests(room_id).await.unwrap().is_empty());
|
||||
@@ -1464,6 +1547,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room_id,
|
||||
&txn0,
|
||||
child_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
@@ -1515,12 +1599,21 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
let event1 =
|
||||
SerializableEventContent::new(&RoomMessageEventContent::text_plain("hey2").into())
|
||||
.unwrap();
|
||||
self.save_send_queue_request(room_id, txn1.clone(), event1.into(), 0).await.unwrap();
|
||||
self.save_send_queue_request(
|
||||
room_id,
|
||||
txn1.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
event1.into(),
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.save_dependent_queued_request(
|
||||
room_id,
|
||||
&txn0,
|
||||
ChildTransactionId::new(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
@@ -1531,6 +1624,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room_id,
|
||||
&txn1,
|
||||
ChildTransactionId::new(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::EditEvent {
|
||||
new_content: SerializableEventContent::new(
|
||||
&RoomMessageEventContent::text_plain("edit").into(),
|
||||
@@ -1563,6 +1657,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
room_id,
|
||||
&txn,
|
||||
child_txn.clone(),
|
||||
MilliSecondsSinceUnixEpoch::now(),
|
||||
DependentQueuedRequestKind::RedactEvent,
|
||||
)
|
||||
.await
|
||||
@@ -1598,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
|
||||
@@ -1762,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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ use ruma::{
|
||||
},
|
||||
serde::Raw,
|
||||
time::Instant,
|
||||
CanonicalJsonObject, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedTransactionId,
|
||||
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri,
|
||||
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
@@ -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(
|
||||
@@ -750,6 +760,7 @@ impl StateStore for MemoryStore {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
transaction_id: OwnedTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
kind: QueuedRequestKind,
|
||||
priority: usize,
|
||||
) -> Result<(), Self::Error> {
|
||||
@@ -759,7 +770,7 @@ impl StateStore for MemoryStore {
|
||||
.send_queue_events
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.push(QueuedRequest { kind, transaction_id, error: None, priority });
|
||||
.push(QueuedRequest { kind, transaction_id, error: None, priority, created_at });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -858,6 +869,7 @@ impl StateStore for MemoryStore {
|
||||
room: &RoomId,
|
||||
parent_transaction_id: &TransactionId,
|
||||
own_transaction_id: ChildTransactionId,
|
||||
created_at: MilliSecondsSinceUnixEpoch,
|
||||
content: DependentQueuedRequestKind,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
@@ -871,6 +883,7 @@ impl StateStore for MemoryStore {
|
||||
parent_transaction_id: parent_transaction_id.to_owned(),
|
||||
own_transaction_id,
|
||||
parent_key: None,
|
||||
created_at,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::deserialized_responses::TimelineEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
@@ -42,10 +41,9 @@ use ruma::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
deserialized_responses::SyncOrStrippedState,
|
||||
latest_event::LatestEvent,
|
||||
rooms::{
|
||||
normal::{RoomSummary, SyncInfo},
|
||||
BaseRoomInfo, RoomNotableTags,
|
||||
@@ -78,8 +76,7 @@ pub struct RoomInfoV1 {
|
||||
sync_info: SyncInfo,
|
||||
#[serde(default = "encryption_state_default")] // see fn docs for why we use this default
|
||||
encryption_state_synced: bool,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: Option<SyncTimelineEvent>,
|
||||
latest_event: Option<TimelineEvent>,
|
||||
base_info: BaseRoomInfoV1,
|
||||
}
|
||||
|
||||
@@ -106,7 +103,6 @@ impl RoomInfoV1 {
|
||||
last_prev_batch,
|
||||
sync_info,
|
||||
encryption_state_synced,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event,
|
||||
base_info,
|
||||
} = self;
|
||||
@@ -122,14 +118,12 @@ impl RoomInfoV1 {
|
||||
last_prev_batch,
|
||||
sync_info,
|
||||
encryption_state_synced,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
|
||||
read_receipts: Default::default(),
|
||||
base_info: base_info.migrate(create),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
cached_display_name: None,
|
||||
cached_user_defined_notification_mode: None,
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
recency_stamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -276,7 +312,6 @@ impl Store {
|
||||
}
|
||||
|
||||
/// Check if a room exists.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub(crate) fn room_exists(&self, room_id: &RoomId) -> bool {
|
||||
self.rooms.read().unwrap().get(room_id).is_some()
|
||||
}
|
||||
@@ -320,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)
|
||||
@@ -331,7 +366,7 @@ impl fmt::Debug for Store {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Store {
|
||||
impl Deref for BaseStateStore {
|
||||
type Target = DynStateStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -339,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 {
|
||||
@@ -480,7 +566,7 @@ impl StateChanges {
|
||||
///
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::store::StoreConfig;
|
||||
///
|
||||
/// #
|
||||
/// let store_config =
|
||||
/// StoreConfig::new("cross-process-store-locks-holder-name".to_owned());
|
||||
/// ```
|
||||
@@ -546,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ use ruma::{
|
||||
AnyMessageLikeEventContent, EventContent as _, RawExt as _,
|
||||
},
|
||||
serde::Raw,
|
||||
OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId, TransactionId, UInt,
|
||||
MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedTransactionId, OwnedUserId,
|
||||
TransactionId, UInt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -131,6 +132,9 @@ pub struct QueuedRequest {
|
||||
/// The bigger the value, the higher the priority at which this request
|
||||
/// should be handled.
|
||||
pub priority: usize,
|
||||
|
||||
/// The time that the request was originally attempted.
|
||||
pub created_at: MilliSecondsSinceUnixEpoch,
|
||||
}
|
||||
|
||||
impl QueuedRequest {
|
||||
@@ -232,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,
|
||||
@@ -371,6 +377,9 @@ pub struct DependentQueuedRequest {
|
||||
/// If the parent request has been sent, the parent's request identifier
|
||||
/// returned by the server once the local echo has been sent out.
|
||||
pub parent_key: Option<SentRequestKey>,
|
||||
|
||||
/// The time that the request was originally attempted.
|
||||
pub created_at: MilliSecondsSinceUnixEpoch,
|
||||
}
|
||||
|
||||
impl DependentQueuedRequest {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user