Compare commits
1338 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 | |||
| bb573117e1 | |||
| ff7077b742 | |||
| bb70229dd8 | |||
| 03947618ff | |||
| b18e7d71ed | |||
| 612ba6fa29 | |||
| db39c6bea6 | |||
| 5f3b56a987 | |||
| 373709fb38 | |||
| 5d8ad3a4a9 | |||
| 0ca35d6c4a | |||
| daeffc07b3 | |||
| bd15f4ecbe | |||
| f17f4e2bf6 | |||
| 177ec1216f | |||
| 512a2d2662 | |||
| 95582a6c3c | |||
| 866b5fea40 | |||
| 34ea42aec0 | |||
| cae7e43b91 | |||
| 34d15a4d37 | |||
| f6cb8186c6 | |||
| 47044b1a23 | |||
| 05d46e6027 | |||
| 338769508e | |||
| 93ebae6601 | |||
| 780c264e59 | |||
| 9a899c1cb1 | |||
| 2703f7f7d4 | |||
| 8d2e672996 | |||
| 5a25e65da3 | |||
| c197808b42 | |||
| ed34719295 | |||
| a052a79aaf | |||
| b6542477bb | |||
| a573b650c9 | |||
| 789bd317b3 | |||
| 2b39476d9b | |||
| 6dcefe49c2 | |||
| 150d9e4b05 | |||
| 54bd1d7931 | |||
| 7ae31d0cb1 | |||
| f7f58dfd71 | |||
| 780a4630e4 | |||
| 3356e0cc82 | |||
| fda374ee81 | |||
| 0264e49968 | |||
| d42c449612 | |||
| 925d10f2ff | |||
| 4402f59e74 | |||
| 20184552a8 | |||
| 832fedb05e | |||
| eeb14f6cbe | |||
| a562f73b1e | |||
| 7295f29055 | |||
| 723d7973d5 | |||
| d5e7a9c949 | |||
| 8f064581d6 | |||
| 634edf2b65 | |||
| 935e4df927 | |||
| 1d72d2774f | |||
| 1e72131e7f | |||
| e8b3949db3 | |||
| c501a39ad4 | |||
| a04f9187f8 | |||
| 32e2070f56 | |||
| 4ee96aaffc | |||
| 0783cf89ba | |||
| cf02e694f2 | |||
| cf178d603c | |||
| ee94c86164 | |||
| 3526761580 | |||
| 9a08975c8e | |||
| 6b56c9efd8 | |||
| 0f2ada0958 | |||
| 0d17ea353f | |||
| 13e26b13e7 | |||
| 72f1bd6180 | |||
| e32ea1627e | |||
| ed1f2e29ed | |||
| 92cb18207e | |||
| 80f6b8d2cd | |||
| 05969fefde | |||
| 81c962238a | |||
| 56218ee5d7 | |||
| aa9138b281 | |||
| 6f231523b3 | |||
| 943b3fbd91 | |||
| 40ff880597 | |||
| 0647be1bc3 | |||
| b069b20e18 | |||
| 91b73a2b16 | |||
| 14d0f6877a | |||
| a2210bce48 | |||
| 68cb85a2b2 | |||
| 72fcc50f80 | |||
| 5721c3622d | |||
| 50eb46dc82 | |||
| 8aae16ffd7 | |||
| e402ed4ce8 | |||
| a1a04ee513 | |||
| affdc25256 | |||
| 8db78efbbc | |||
| d8184e72eb | |||
| 3bd57d4307 | |||
| 42193f1b06 | |||
| a277e6d37f | |||
| bf6fa4cd55 | |||
| 6501a44e6a | |||
| ee30008f38 | |||
| 22cb8a1878 | |||
| 111f916a78 | |||
| a6e1f05957 | |||
| 0b64c68191 | |||
| 713039279c | |||
| d317e5d73c | |||
| ee93c278df | |||
| 1009ea86ae | |||
| 7d8e7af308 | |||
| 136522c694 | |||
| 6801811226 | |||
| a4434d79c9 | |||
| e0b1b5dc05 | |||
| 1a63d8f0b7 | |||
| 5bf3b11edf | |||
| 8f1722f2a8 | |||
| 5d95387935 | |||
| bd93a9a40e | |||
| 5cde4a6630 | |||
| de5511f009 | |||
| 9bdd9fa831 | |||
| 48bb3dbbe7 | |||
| b8bf847fc1 | |||
| 17812b6949 | |||
| bab979aaf4 | |||
| 42778dc79d | |||
| a948be9c85 | |||
| 9c381c1022 | |||
| 9002f82659 | |||
| 5f7fb4699a | |||
| 5907104e0e | |||
| d7dff5b026 | |||
| cabde8ed11 | |||
| b02fd92ad0 | |||
| 9be8578aff | |||
| 4f28dd85bf | |||
| 74119e8861 | |||
| e76b8f7e15 | |||
| 31bd5c6790 | |||
| 50f036d283 | |||
| 8c73f0c655 | |||
| 8de76deb1b | |||
| b65728d46f | |||
| 0b4b4ea791 | |||
| 552ab81739 | |||
| d49d12249a | |||
| ed1d406b72 | |||
| 80a48f53ad | |||
| 51cfaaacee | |||
| 2f9866cf04 | |||
| 7de74e2c04 | |||
| 019de4ffa0 | |||
| 9f1e3c179b | |||
| 17e17f0b9c | |||
| 5da36d13c8 | |||
| cce322f9c8 | |||
| ed3b03f454 | |||
| 27e1cded2e | |||
| ad3d1fb6b3 | |||
| d2fecb6701 | |||
| 685386df13 | |||
| f94b202341 | |||
| d1a6956e77 | |||
| 2d2215edbe | |||
| bcd0d20e2f | |||
| ba5881355d | |||
| 1072d0a019 | |||
| 783c86aa78 | |||
| 5564fe8852 | |||
| e1f0037fd5 | |||
| daa984f7de | |||
| aa0eb760de | |||
| 9ed65bc321 | |||
| ce95b6089f | |||
| c6ba71ae33 | |||
| e57d38cf57 | |||
| 9bea0cff24 | |||
| 197da2c585 | |||
| d2ecd745f6 | |||
| e99939db85 | |||
| 600a708e7b | |||
| a94a5f1716 | |||
| 46064680ce | |||
| 6fe5acfc97 | |||
| 3369903766 | |||
| a0c86d9645 | |||
| 7a454888a3 | |||
| 37f52e1c6c | |||
| 185423539e | |||
| 9e20659d5d | |||
| 7783188769 | |||
| 514af54c4c | |||
| ad615b7612 | |||
| a1b7906a7d | |||
| 79c8d2c345 | |||
| dcf6af405d | |||
| bb598b61a5 | |||
| 1c554c4912 | |||
| 21f8b7ed31 | |||
| 23ee8e25dd | |||
| 1098095846 | |||
| 3e7d7e8a31 | |||
| 2c45316bcb | |||
| 8dc7c1f876 | |||
| db84936dcd | |||
| 75d7d07013 | |||
| d4d5f45edc | |||
| d0257d1cb2 | |||
| ecf44348cf | |||
| cc8bc05537 | |||
| 728d646ce2 | |||
| ca397dca0f | |||
| 1fbe6815c3 | |||
| c61f70727f | |||
| 2abbf58825 | |||
| b979b2ea1e | |||
| 24b968ad39 | |||
| faa8aa2b9c | |||
| db9ee9d87b | |||
| 1dbb494b94 | |||
| fe52b4cb78 | |||
| 5519442ad8 | |||
| 88363d8033 | |||
| fb5d8f29ac | |||
| 912b121d27 | |||
| 2e975d9b19 | |||
| edc93e62b4 | |||
| 9d6ffa951f | |||
| 079ec023b7 | |||
| e55a1c7e00 | |||
| ddd737e4d8 | |||
| 38a15afc9c | |||
| fa93daabd2 | |||
| 6b0987385e | |||
| 48fbda844f | |||
| bc70f3c051 | |||
| d2f255d613 | |||
| bf86b168d7 | |||
| e5ca44bb04 | |||
| 1f563c964c | |||
| 9a9730d59e | |||
| af3ce4b32b | |||
| 03f0c3a001 | |||
| 639833acf1 | |||
| 60893d2797 | |||
| 9e45111d8b | |||
| 0080f17c1f | |||
| fa47af3dd6 | |||
| c4ff07124b | |||
| 900cf5d071 | |||
| 8a6ced0e8f | |||
| f20401c657 | |||
| b987fc1de2 | |||
| efeac2ef39 | |||
| 6b80055bd2 | |||
| 0af53e99ee | |||
| bc0c2a6be2 |
+8
-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]
|
||||
@@ -24,6 +23,7 @@ allow = [
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
exceptions = [
|
||||
@@ -54,8 +54,13 @@ allow-git = [
|
||||
"https://github.com/element-hq/tracing.git",
|
||||
# Sam as for the tracing dependency.
|
||||
"https://github.com/element-hq/paranoid-android.git",
|
||||
# Well, it's Ruma.
|
||||
"https://github.com/ruma/ruma",
|
||||
# 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-06-25
|
||||
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
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
test-apple:
|
||||
name: matrix-rust-components-swift
|
||||
needs: xtask
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
run: swift test
|
||||
|
||||
- name: Build Framework
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=dev
|
||||
run: target/debug/xtask swift build-framework --target=aarch64-apple-ios --profile=reldbg
|
||||
|
||||
complement-crypto:
|
||||
name: "Run Complement Crypto tests"
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
|
||||
test-crypto-apple-framework-generation:
|
||||
name: Generate Crypto FFI Apple XCFramework
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
|
||||
+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-06-25
|
||||
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.27.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-06-25
|
||||
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-06-25
|
||||
toolchain: nightly-2025-02-20
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
run:
|
||||
cargo doc --no-deps --workspace --features docsrs
|
||||
cargo doc --no-deps --workspace --features docsrs --exclude=xtask
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
os-name: 🐧
|
||||
cachekey-id: linux
|
||||
|
||||
- os: macos-14
|
||||
- os: macos-15
|
||||
os-name: 🍏
|
||||
cachekey-id: macos
|
||||
|
||||
|
||||
+72
-36
@@ -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
|
||||
@@ -45,9 +72,46 @@ that is, just the branch name.)
|
||||
|
||||
# Writing changelog entries
|
||||
|
||||
We aim to maintain clear and informative changelogs that accurately reflect the
|
||||
changes in our project. This guide will help you write useful changelog entries
|
||||
using git-cliff, which fetches changelog entries from commit messages.
|
||||
Our goal is to maintain clear, concise, and informative changelogs that
|
||||
accurately document changes in the project. Changelog entries should be written
|
||||
manually for each crate in the `/crates/$CRATE_NAME/Changelog.md` file.
|
||||
|
||||
Be sure to include a link to the pull request for additional context. A
|
||||
well-written changelog entry should be understandable even to those who may not
|
||||
be deeply familiar with the project. Provide enough context to ensure clarity
|
||||
and ease of understanding.
|
||||
|
||||
A couple of examples of bad changelog entry would look like:
|
||||
|
||||
```markdown
|
||||
- Fixed a panic.
|
||||
```
|
||||
|
||||
```markdown
|
||||
- Added the Bar function to Foo.
|
||||
```
|
||||
|
||||
A good example of a changelog entry could look like the following:
|
||||
|
||||
```markdown
|
||||
- Use the inviter's server name and the server name from the room alias as
|
||||
fallback values for the via parameter when requesting the room summary from
|
||||
the homeserver. This ensures requests succeed even when the room being
|
||||
previewed is hosted on a federated server.
|
||||
([#4357](https://github.com/matrix-org/matrix-rust-sdk/pull/4357))
|
||||
```
|
||||
|
||||
For security-related changelog entries, please include the following additional
|
||||
details alongside the pull request number:
|
||||
|
||||
* Impact: Clearly describe the issue's potential impact on users or systems.
|
||||
* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier.
|
||||
* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context.
|
||||
|
||||
```markdown
|
||||
- Use a constant-time Base64 encoder for secret key material to mitigate
|
||||
side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
|
||||
```
|
||||
|
||||
## Commit message format
|
||||
|
||||
@@ -74,45 +138,20 @@ The type of changes which will be included in changelogs is one of the following
|
||||
The scope is optional and can specify the area of the codebase affected (e.g.,
|
||||
olm, cipher).
|
||||
|
||||
### Changelog trailer
|
||||
|
||||
In addition to the Conventional Commit format, you can use the `Changelog` git
|
||||
trailer to specify the changelog message explicitly. When that trailer is
|
||||
present, its value will be used as the changelog entry instead of the commit's
|
||||
leading line. The `Breaking-Change` git trailer can be used in a similar manner
|
||||
if the changelog entry should be marked as a breaking change.
|
||||
|
||||
|
||||
#### Example commit message
|
||||
|
||||
```
|
||||
feat: Add a method to encode Ed25519 public keys to Base64
|
||||
|
||||
This patch adds the `Ed25519PublicKey::to_base64()` method, which allows us to
|
||||
stringify Ed25519 and thus present them to users. It's also commonly used when
|
||||
Ed25519 keys need to be inserted into JSON.
|
||||
|
||||
Changelog: Add the `Ed25519PublicKey::to_base64()` method which can be used to
|
||||
stringify the Ed25519 public key.
|
||||
```
|
||||
|
||||
In this commit message, the content specified in the `Changelog` trailer will be
|
||||
used for the changelog entry.
|
||||
|
||||
Be careful to add at least one whitespace after new lines to create a paragraph.
|
||||
|
||||
### Security fixes
|
||||
|
||||
Commits addressing security vulnerabilities must include specific trailers for
|
||||
vulnerability metadata. These commits are required to include at least the
|
||||
`Security-Impact` trailer to indicate that the commit is a security fix.
|
||||
vulnerability metadata, which should also be reflected in the corresponding
|
||||
changelog entry.
|
||||
|
||||
Security issues have some additional git-trailers:
|
||||
The metadata must be included in the following git-trailers:
|
||||
|
||||
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
|
||||
* `CVE`: The CVE that was assigned to this issue.
|
||||
* `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
|
||||
Please include all of the fields that are available.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
@@ -131,9 +170,6 @@ material.
|
||||
Security-Impact: Low
|
||||
CVE: CVE-2024-40640
|
||||
GitHub-Advisory: GHSA-j8cm-g7r6-hfpq
|
||||
|
||||
Changelog: Use a constant-time Base64 encoder for secret key material
|
||||
to mitigate side-channel attacks leaking secret key material.
|
||||
```
|
||||
|
||||
## Review process
|
||||
|
||||
Generated
+925
-1077
File diff suppressed because it is too large
Load Diff
+81
-50
@@ -18,34 +18,49 @@ default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.68"
|
||||
assert-json-diff = "2"
|
||||
anyhow = "1.0.95"
|
||||
aquamarine = "0.6.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
assert_matches = "1.5.0"
|
||||
assert_matches2 = "0.1.1"
|
||||
assert_matches2 = "0.1.2"
|
||||
async-rx = "0.1.3"
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.60"
|
||||
as_variant = "1.2.0"
|
||||
base64 = "0.22.0"
|
||||
byteorder = "1.4.3"
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.85"
|
||||
as_variant = "1.3.0"
|
||||
base64 = "0.22.1"
|
||||
byteorder = "1.5.0"
|
||||
chrono = "0.4.39"
|
||||
eyeball = { version = "0.8.8", features = ["tracing"] }
|
||||
eyeball-im = { version = "0.5.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.7.0"
|
||||
futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = "0.3.26"
|
||||
growable-bloom-filter = "2.1.0"
|
||||
http = "1.1.0"
|
||||
imbl = "3.0.0"
|
||||
itertools = "0.12.0"
|
||||
once_cell = "1.16.0"
|
||||
pin-project-lite = "0.2.9"
|
||||
eyeball-im = { version = "0.7.0", features = ["tracing"] }
|
||||
eyeball-im-util = "0.9.0"
|
||||
futures-core = "0.3.31"
|
||||
futures-executor = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
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.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.16"
|
||||
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", default-features = false }
|
||||
ruma = { version = "0.11.1", features = [
|
||||
reqwest = { version = "0.12.12", default-features = false }
|
||||
rmp-serde = "1.3.0"
|
||||
# Be careful to use commits from the https://github.com/ruma/ruma/tree/ruma-0.12
|
||||
# branch until a proper release with breaking changes happens.
|
||||
ruma = { version = "0.12.2", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
@@ -58,42 +73,45 @@ ruma = { version = "0.11.1", features = [
|
||||
"unstable-msc3489",
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4140",
|
||||
"unstable-msc4171",
|
||||
] }
|
||||
ruma-common = "0.14.1"
|
||||
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.5.0"
|
||||
similar-asserts = "1.6.1"
|
||||
stream_assert = "0.1.1"
|
||||
thiserror = "1.0.38"
|
||||
tokio = { version = "1.39.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"
|
||||
unicode-normalization = "0.1.24"
|
||||
uniffi = { version = "0.28.0" }
|
||||
uniffi_bindgen = { version = "0.28.0" }
|
||||
url = "2.5.0"
|
||||
vodozemac = { version = "0.8.0", features = ["insecure-pk-encryption"] }
|
||||
wiremock = "0.6.0"
|
||||
zeroize = "1.6.0"
|
||||
url = "2.5.4"
|
||||
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.8.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.8.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.8.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.8.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.8.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.8.0" }
|
||||
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.8.0", default-features = false }
|
||||
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.8.0" }
|
||||
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.7.0" }
|
||||
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.8.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`
|
||||
@@ -106,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]
|
||||
@@ -118,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" }
|
||||
@@ -131,7 +159,10 @@ paranoid-android = { git = "https://github.com/element-hq/paranoid-android.git",
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "warn"
|
||||
semicolon_in_expressions_from_macros = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(tarpaulin_include)', # Used by tarpaulin (code coverage)
|
||||
'cfg(ruma_unstable_exhaustive_types)', # Used by Ruma's EventContent derive macro
|
||||
] }
|
||||
unused_extern_crates = "warn"
|
||||
unused_import_braces = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
@@ -1,46 +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.
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.70`.
|
||||
- [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
|
||||
|
||||
|
||||
+21
-20
@@ -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`, prepend the `CHANGELOG.md`
|
||||
file using `git cliff`, and bump the version in the `Cargo.toml` file.
|
||||
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
|
||||
+10
-2
@@ -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 }
|
||||
@@ -23,12 +23,16 @@ tokio = { workspace = true, default-features = false, features = ["rt-multi-thre
|
||||
wiremock = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.13.0", features = ["flamegraph", "criterion"] }
|
||||
pprof = { version = "0.14.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
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,32 +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::{events::EventFactory, 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::{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;
|
||||
@@ -34,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);
|
||||
}
|
||||
|
||||
@@ -74,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");
|
||||
@@ -122,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);
|
||||
|
||||
@@ -146,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];
|
||||
@@ -170,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);
|
||||
@@ -184,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();
|
||||
@@ -200,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 {
|
||||
@@ -219,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);
|
||||
@@ -13,6 +13,7 @@ let package = Package(
|
||||
],
|
||||
products: [
|
||||
.library(name: "MatrixRustSDK",
|
||||
type: .dynamic,
|
||||
targets: ["MatrixRustSDK"]),
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -71,10 +71,6 @@ $ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
|
||||
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
|
||||
```
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.62`.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::{env, error::Error};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-ffi/build.rs] too!
|
||||
@@ -12,26 +17,45 @@ fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_crypto::dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
use matrix_sdk_crypto::{
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Handle;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[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)]
|
||||
@@ -22,6 +28,8 @@ pub enum DehydrationError {
|
||||
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
|
||||
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
|
||||
PickleKeyLength(usize),
|
||||
#[error(transparent)]
|
||||
Rand(#[from] rand::Error),
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
|
||||
@@ -29,10 +37,16 @@ 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)
|
||||
}
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::Store(e) => Self::Store(e),
|
||||
matrix_sdk_crypto::dehydrated_devices::DehydrationError::PickleKeyLength(l) => {
|
||||
Self::PickleKeyLength(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,14 +80,14 @@ impl DehydratedDevices {
|
||||
|
||||
pub fn rehydrate(
|
||||
&self,
|
||||
pickle_key: Vec<u8>,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
device_id: String,
|
||||
device_data: String,
|
||||
) -> Result<Arc<RehydratedDevice>, DehydrationError> {
|
||||
let device_data: Raw<_> = serde_json::from_str(&device_data)?;
|
||||
let device_id: OwnedDeviceId = device_id.into();
|
||||
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let ret = RehydratedDevice {
|
||||
runtime: self.runtime.to_owned(),
|
||||
@@ -85,10 +99,41 @@ impl DehydratedDevices {
|
||||
}
|
||||
.into();
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Get the cached dehydrated device pickle key if any.
|
||||
///
|
||||
/// None if the key was not previously cached (via
|
||||
/// [`Self::save_dehydrated_device_pickle_key`]).
|
||||
///
|
||||
/// Should be used to periodically rotate the dehydrated device to avoid
|
||||
/// OTK exhaustion and accumulation of to_device messages.
|
||||
pub fn get_dehydrated_device_key(
|
||||
&self,
|
||||
) -> Result<Option<crate::DehydratedDeviceKey>, CryptoStoreError> {
|
||||
Ok(self
|
||||
.runtime
|
||||
.block_on(self.inner.get_dehydrated_device_pickle_key())?
|
||||
.map(crate::DehydratedDeviceKey::from))
|
||||
}
|
||||
|
||||
/// Store the dehydrated device pickle key in the crypto store.
|
||||
///
|
||||
/// This is useful if the client wants to periodically rotate dehydrated
|
||||
/// devices to avoid OTK exhaustion and accumulated to_device problems.
|
||||
pub fn save_dehydrated_device_key(
|
||||
&self,
|
||||
pickle_key: &crate::DehydratedDeviceKey,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
let pickle_key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
Ok(self.runtime.block_on(self.inner.save_dehydrated_device_pickle_key(&pickle_key))?)
|
||||
}
|
||||
|
||||
/// Deletes the previously stored dehydrated device pickle key.
|
||||
pub fn delete_dehydrated_device_key(&self) -> Result<(), CryptoStoreError> {
|
||||
Ok(self.runtime.block_on(self.inner.delete_dehydrated_device_pickle_key())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -138,15 +183,13 @@ impl DehydratedDevice {
|
||||
pub fn keys_for_upload(
|
||||
&self,
|
||||
device_display_name: String,
|
||||
pickle_key: Vec<u8>,
|
||||
pickle_key: &DehydratedDeviceKey,
|
||||
) -> Result<UploadDehydratedDeviceRequest, DehydrationError> {
|
||||
let mut key = get_pickle_key(&pickle_key)?;
|
||||
let key = InnerDehydratedDeviceKey::from_slice(&pickle_key.inner)?;
|
||||
|
||||
let request =
|
||||
self.runtime.block_on(self.inner.keys_for_upload(device_display_name, &key))?;
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(request.into())
|
||||
}
|
||||
}
|
||||
@@ -177,15 +220,36 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pickle_key(pickle_key: &[u8]) -> Result<Box<[u8; 32]>, DehydrationError> {
|
||||
let pickle_key_length = pickle_key.len();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
|
||||
|
||||
if pickle_key_length == 32 {
|
||||
let mut key = Box::new([0u8; 32]);
|
||||
key.copy_from_slice(pickle_key);
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key() {
|
||||
let result = DehydratedDeviceKey::new();
|
||||
assert!(result.is_ok());
|
||||
let dehydrated_device_key = result.unwrap();
|
||||
let base_64 = dehydrated_device_key.to_base64();
|
||||
let inner_bytes = dehydrated_device_key.inner;
|
||||
|
||||
Ok(key)
|
||||
} else {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length))
|
||||
let copy = DehydratedDeviceKey::from_slice(&inner_bytes).unwrap();
|
||||
|
||||
assert_eq!(base_64, copy.to_base64());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key_failure() {
|
||||
let bytes = [0u8; 24];
|
||||
|
||||
let pickle_key = DehydratedDeviceKey::from_slice(&bytes);
|
||||
|
||||
assert!(pickle_key.is_err());
|
||||
|
||||
match pickle_key {
|
||||
Err(DehydrationError::PickleKeyLength(pickle_key_length)) => {
|
||||
assert_eq!(bytes.len(), pickle_key_length);
|
||||
}
|
||||
_ => panic!("Should have failed!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
|
||||
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
|
||||
SignatureError as InnerSignatureError,
|
||||
};
|
||||
use matrix_sdk_sqlite::OpenStoreError;
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
@@ -57,6 +58,8 @@ pub enum CryptoStoreError {
|
||||
InvalidUserId(String, IdParseError),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
#[error(transparent)]
|
||||
DehydrationError(#[from] InnerDehydrationError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
@@ -112,7 +115,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_withheld_error_mapping() {
|
||||
use matrix_sdk_crypto::types::events::room_key_withheld::WithheldCode;
|
||||
use matrix_sdk_common::deserialized_responses::WithheldCode;
|
||||
|
||||
let inner_error = MegolmError::MissingRoomKey(Some(WithheldCode::Unverified));
|
||||
|
||||
|
||||
@@ -36,7 +36,10 @@ pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
|
||||
use matrix_sdk_crypto::{
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
store::{
|
||||
Changes, CryptoStore, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
@@ -62,6 +65,8 @@ pub use verification::{
|
||||
};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use crate::dehydrated_devices::DehydrationError;
|
||||
|
||||
/// Struct collecting data that is important to migrate to the rust-sdk
|
||||
#[derive(Deserialize, Serialize, uniffi::Record)]
|
||||
pub struct MigrationData {
|
||||
@@ -502,6 +507,7 @@ fn collect_sessions(
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
shared_history: false,
|
||||
algorithm: RustEventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
};
|
||||
|
||||
@@ -675,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -822,6 +833,39 @@ impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dehydrated device key
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct DehydratedDeviceKey {
|
||||
pub(crate) inner: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DehydratedDeviceKey {
|
||||
/// Generates a new random pickle key.
|
||||
pub fn new() -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::new()?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Creates a new dehydration pickle key from the given slice.
|
||||
///
|
||||
/// Fail if the slice length is not 32.
|
||||
pub fn from_slice(slice: &[u8]) -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(slice)?;
|
||||
Ok(inner.into())
|
||||
}
|
||||
|
||||
/// Export the [`DehydratedDeviceKey`] as a base64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
let inner = InnerDehydratedDeviceKey::from_slice(&self.inner).unwrap();
|
||||
inner.to_base64()
|
||||
}
|
||||
}
|
||||
impl From<InnerDehydratedDeviceKey> for DehydratedDeviceKey {
|
||||
fn from(pickle_key: InnerDehydratedDeviceKey) -> Self {
|
||||
DehydratedDeviceKey { inner: pickle_key.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
|
||||
@@ -17,8 +17,8 @@ use matrix_sdk_crypto::{
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
types::requests::ToDeviceRequest,
|
||||
DecryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
|
||||
@@ -4,9 +4,12 @@ use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
CrossSigningBootstrapRequests, IncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
types::requests::{
|
||||
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
},
|
||||
CrossSigningBootstrapRequests,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
@@ -136,7 +139,7 @@ pub enum Request {
|
||||
|
||||
impl From<OutgoingRequest> for Request {
|
||||
fn from(r: OutgoingRequest) -> Self {
|
||||
use matrix_sdk_crypto::OutgoingRequests::*;
|
||||
use matrix_sdk_crypto::types::requests::AnyOutgoingRequest::*;
|
||||
|
||||
match r.request() {
|
||||
KeysUpload(u) => {
|
||||
@@ -338,16 +341,16 @@ impl From<RoomMessageResponse> for OwnedResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
impl<'a> From<&'a OwnedResponse> for AnyIncomingResponse<'a> {
|
||||
fn from(r: &'a OwnedResponse) -> Self {
|
||||
match r {
|
||||
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
|
||||
OwnedResponse::KeysClaim(r) => AnyIncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => AnyIncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => AnyIncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => AnyIncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => AnyIncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => AnyIncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => AnyIncomingResponse::RoomMessage(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +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,10 +1,15 @@
|
||||
use std::{env, error::Error};
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
/// Adds a temporary workaround for an issue with the Rust compiler and Android
|
||||
/// in x86_64 devices: https://github.com/rust-lang/rust/issues/109717.
|
||||
/// The workaround comes from: https://github.com/mozilla/application-services/pull/5442
|
||||
/// The workaround is based on: https://github.com/mozilla/application-services/pull/5442
|
||||
///
|
||||
/// IMPORTANT: if you modify this, make sure to modify
|
||||
/// [../matrix-sdk-crypto-ffi/build.rs] too!
|
||||
@@ -12,26 +17,45 @@ fn setup_x86_64_android_workaround() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set");
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH not set");
|
||||
if target_arch == "x86_64" && target_os == "android" {
|
||||
let android_ndk_home = env::var("ANDROID_NDK_HOME").expect("ANDROID_NDK_HOME not set");
|
||||
let build_os = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"windows" => "windows",
|
||||
_ => panic!(
|
||||
"Unsupported OS. You must use either Linux, MacOS or Windows to build the crate."
|
||||
),
|
||||
};
|
||||
const DEFAULT_CLANG_VERSION: &str = "18";
|
||||
let clang_version =
|
||||
env::var("NDK_CLANG_VERSION").unwrap_or_else(|_| DEFAULT_CLANG_VERSION.to_owned());
|
||||
let linux_x86_64_lib_dir = format!(
|
||||
"toolchains/llvm/prebuilt/{build_os}-x86_64/lib/clang/{clang_version}/lib/linux/"
|
||||
// Configure rust to statically link against the `libclang_rt.builtins` supplied
|
||||
// with clang.
|
||||
|
||||
// cargo-ndk sets CC_x86_64-linux-android to the path to `clang`, within the
|
||||
// Android NDK.
|
||||
let clang_path = PathBuf::from(
|
||||
env::var("CC_x86_64-linux-android").expect("CC_x86_64-linux-android not set"),
|
||||
);
|
||||
println!("cargo:rustc-link-search={android_ndk_home}/{linux_x86_64_lib_dir}");
|
||||
|
||||
// clang_path should now look something like
|
||||
// `.../sdk/ndk/28.0.12674087/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`.
|
||||
// We strip `/bin/clang` from the end to get the toolchain path.
|
||||
let toolchain_path = clang_path
|
||||
.ancestors()
|
||||
.nth(2)
|
||||
.expect("could not find NDK toolchain path")
|
||||
.to_str()
|
||||
.expect("NDK toolchain path is not valid UTF-8");
|
||||
|
||||
let clang_version = get_clang_major_version(&clang_path);
|
||||
|
||||
println!("cargo:rustc-link-search={toolchain_path}/lib/clang/{clang_version}/lib/linux/");
|
||||
println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the clang binary at `clang_path`, and return its major version number
|
||||
fn get_clang_major_version(clang_path: &Path) -> String {
|
||||
let clang_output =
|
||||
Command::new(clang_path).arg("-dumpversion").output().expect("failed to start clang");
|
||||
|
||||
if !clang_output.status.success() {
|
||||
panic!("failed to run clang: {}", String::from_utf8_lossy(&clang_output.stderr));
|
||||
}
|
||||
|
||||
let clang_version = String::from_utf8(clang_output.stdout).expect("clang output is not utf8");
|
||||
clang_version.split('.').next().expect("could not parse clang output").to_owned()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
|
||||
|
||||
@@ -8,15 +8,3 @@ dictionary Mentions {
|
||||
interface RoomMessageEventContentWithoutRelation {
|
||||
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface ClientError {
|
||||
Generic(string msg);
|
||||
};
|
||||
|
||||
interface MediaSource {
|
||||
[Name=from_json, Throws=ClientError]
|
||||
constructor(string json);
|
||||
string to_json();
|
||||
string url();
|
||||
};
|
||||
|
||||
@@ -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)?, []))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,15 @@
|
||||
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,
|
||||
},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
reqwest::Certificate,
|
||||
ruma::{ServerName, UserId},
|
||||
sliding_sync::{
|
||||
@@ -15,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,
|
||||
@@ -103,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,
|
||||
@@ -152,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,
|
||||
@@ -202,6 +203,8 @@ pub enum ClientBuildError {
|
||||
SlidingSyncVersion(VersionBuilderError),
|
||||
#[error(transparent)]
|
||||
Sdk(MatrixClientBuildError),
|
||||
#[error(transparent)]
|
||||
EventCache(#[from] EventCacheError),
|
||||
#[error("Failed to build the client: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
@@ -252,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>,
|
||||
@@ -269,6 +276,10 @@ pub struct ClientBuilder {
|
||||
room_key_recipient_strategy: CollectStrategy,
|
||||
decryption_trust_requirement: TrustRequirement,
|
||||
request_config: Option<RequestConfig>,
|
||||
|
||||
/// Whether to enable use of the event cache store, for reloading events
|
||||
/// when building timelines et al.
|
||||
use_event_cache_persistent_storage: bool,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
@@ -277,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,
|
||||
@@ -299,9 +314,27 @@ impl ClientBuilder {
|
||||
room_key_recipient_strategy: Default::default(),
|
||||
decryption_trust_requirement: TrustRequirement::Untrusted,
|
||||
request_config: Default::default(),
|
||||
use_event_cache_persistent_storage: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether to use the event cache persistent storage or not.
|
||||
///
|
||||
/// This is a temporary feature flag, for testing the event cache's
|
||||
/// persistent storage. Follow new developments in https://github.com/matrix-org/matrix-rust-sdk/issues/3280.
|
||||
///
|
||||
/// This is disabled by default. When disabled, a one-time cleanup is
|
||||
/// performed when creating the client, and it will clear all the events
|
||||
/// previously stored in the event cache.
|
||||
///
|
||||
/// When enabled, it will attempt to store events in the event cache as
|
||||
/// they're received, and reuse them when reconstructing timelines.
|
||||
pub fn use_event_cache_persistent_storage(self: Arc<Self>, value: bool) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.use_event_cache_persistent_storage = value;
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn cross_process_store_locks_holder_name(
|
||||
self: Arc<Self>,
|
||||
holder_name: String,
|
||||
@@ -338,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);
|
||||
@@ -362,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);
|
||||
@@ -484,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(),
|
||||
@@ -493,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.");
|
||||
}
|
||||
@@ -579,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)
|
||||
@@ -604,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));
|
||||
@@ -616,14 +718,28 @@ 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);
|
||||
}
|
||||
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
if builder.use_event_cache_persistent_storage {
|
||||
// Enable the persistent storage \o/
|
||||
sdk_client.event_cache().enable_storage()?;
|
||||
} else {
|
||||
// Get rid of all the previous events, if any.
|
||||
let store = sdk_client
|
||||
.event_cache_store()
|
||||
.lock()
|
||||
.await
|
||||
.map_err(EventCacheError::LockingStorage)?;
|
||||
store.clear_all_rooms_chunks().await.map_err(EventCacheError::Storage)?;
|
||||
}
|
||||
|
||||
Ok(Arc::new(
|
||||
Client::new(sdk_client, builder.enable_oidc_refresh_lock, builder.session_delegate)
|
||||
.await?,
|
||||
@@ -635,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.
|
||||
@@ -662,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());
|
||||
}
|
||||
@@ -684,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,
|
||||
@@ -704,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());
|
||||
@@ -254,7 +254,7 @@ impl Encryption {
|
||||
/// Therefore it is necessary to poll the server for an answer every time
|
||||
/// you want to differentiate between those two states.
|
||||
pub async fn backup_exists_on_server(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.backups().exists_on_server().await?)
|
||||
Ok(self.inner.backups().fetch_exists_on_server().await?)
|
||||
}
|
||||
|
||||
pub fn recovery_state(&self) -> RecoveryState {
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{message::Relation, redaction::SyncRoomRedactionEvent},
|
||||
room::{
|
||||
message::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
@@ -14,6 +17,7 @@ use ruma::{
|
||||
use crate::{
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, NotifyType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
@@ -30,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> {
|
||||
@@ -202,7 +206,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
|
||||
_ => None,
|
||||
});
|
||||
MessageLikeEventContent::RoomMessage {
|
||||
message_type: original_content.msgtype.into(),
|
||||
message_type: original_content.msgtype.try_into()?,
|
||||
in_reply_to_event_id,
|
||||
}
|
||||
}
|
||||
@@ -356,6 +360,39 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, uniffi::Enum)]
|
||||
pub enum RoomMessageEventMessageType {
|
||||
Audio,
|
||||
Emote,
|
||||
File,
|
||||
Image,
|
||||
Location,
|
||||
Notice,
|
||||
ServerNotice,
|
||||
Text,
|
||||
Video,
|
||||
VerificationRequest,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for RoomMessageEventMessageType {
|
||||
fn from(val: ruma::events::room::message::MessageType) -> Self {
|
||||
match val {
|
||||
RumaMessageType::Audio { .. } => Self::Audio,
|
||||
RumaMessageType::Emote { .. } => Self::Emote,
|
||||
RumaMessageType::File { .. } => Self::File,
|
||||
RumaMessageType::Image { .. } => Self::Image,
|
||||
RumaMessageType::Location { .. } => Self::Location,
|
||||
RumaMessageType::Notice { .. } => Self::Notice,
|
||||
RumaMessageType::ServerNotice { .. } => Self::ServerNotice,
|
||||
RumaMessageType::Text { .. } => Self::Text,
|
||||
RumaMessageType::Video { .. } => Self::Video,
|
||||
RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the 2 possible identifiers of an event, either it has a remote
|
||||
/// event id or a local transaction id, never both or none.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// TODO: target-os conditional would be good.
|
||||
|
||||
#![allow(unused_qualifications, clippy::new_without_default)]
|
||||
#![allow(clippy::empty_line_after_doc_comments)] // Needed because uniffi macros contain empty
|
||||
// lines after docs.
|
||||
|
||||
mod authentication;
|
||||
mod chunk_iterator;
|
||||
@@ -12,6 +14,7 @@ mod error;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod identity_status_change;
|
||||
mod live_location_share;
|
||||
mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
@@ -27,19 +30,15 @@ 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, MediaSource,
|
||||
};
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContentWithoutRelation;
|
||||
|
||||
use self::{
|
||||
error::ClientError,
|
||||
ruma::{MediaSourceExt, Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
ruma::{Mentions, RoomMessageEventContentWithoutRelationExt},
|
||||
task_handle::TaskHandle,
|
||||
};
|
||||
|
||||
|
||||
@@ -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("")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+515
-126
@@ -1,45 +1,50 @@
|
||||
use std::{collections::HashMap, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::StreamExt;
|
||||
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::{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,
|
||||
},
|
||||
TimelineEventType,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent, TimelineEventType,
|
||||
},
|
||||
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},
|
||||
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::{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,
|
||||
};
|
||||
@@ -50,6 +55,7 @@ pub enum Membership {
|
||||
Joined,
|
||||
Left,
|
||||
Knocked,
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl From<RoomState> for Membership {
|
||||
@@ -59,6 +65,7 @@ impl From<RoomState> for Membership {
|
||||
RoomState::Joined => Membership::Joined,
|
||||
RoomState::Left => Membership::Left,
|
||||
RoomState::Knocked => Membership::Knocked,
|
||||
RoomState::Banned => Membership::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,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.
|
||||
@@ -107,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 {
|
||||
@@ -158,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.
|
||||
///
|
||||
@@ -196,72 +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,
|
||||
configuration: TimelineConfiguration,
|
||||
) -> Result<Arc<Timeline>, ClientError> {
|
||||
let room = &self.inner;
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner);
|
||||
|
||||
let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room);
|
||||
builder = builder
|
||||
.with_focus(configuration.focus.try_into()?)
|
||||
.with_date_divider_mode(configuration.date_divider_mode.into());
|
||||
|
||||
if let Some(internal_id_prefix) = internal_id_prefix {
|
||||
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);
|
||||
}
|
||||
|
||||
let timeline = builder
|
||||
.with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests })
|
||||
.build()
|
||||
.await?;
|
||||
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> {
|
||||
@@ -275,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)
|
||||
@@ -291,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(
|
||||
@@ -306,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),
|
||||
@@ -336,6 +338,22 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a raw event to the room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_type` - The type of the event to send.
|
||||
///
|
||||
/// * `content` - The content of the event to send encoded as JSON string.
|
||||
pub async fn send_raw(&self, event_type: String, content: String) -> Result<(), ClientError> {
|
||||
let content_json: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| ClientError::Generic { msg: format!("Failed to parse JSON: {e}") })?;
|
||||
|
||||
self.inner.send_raw(&event_type, content_json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -382,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(())
|
||||
}
|
||||
|
||||
@@ -579,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 {
|
||||
@@ -590,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
|
||||
@@ -840,6 +871,305 @@ impl Room {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the event cache storage for the current room.
|
||||
///
|
||||
/// This will remove all the information related to the event cache, in
|
||||
/// memory and in the persisted storage, if enabled.
|
||||
pub async fn clear_event_cache_storage(&self) -> Result<(), ClientError> {
|
||||
let (room_event_cache, _drop_handles) = self.inner.event_cache().await?;
|
||||
room_event_cache.clear().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subscribes to requests to join this room (knock member events), using a
|
||||
/// `listener` to be notified of the changes.
|
||||
///
|
||||
/// The current requests to join the room will be emitted immediately
|
||||
/// when subscribing, along with a [`TaskHandle`] to cancel the
|
||||
/// subscription.
|
||||
pub async fn subscribe_to_knock_requests(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn KnockRequestsListener>,
|
||||
) -> Result<Arc<TaskHandle>, ClientError> {
|
||||
let (stream, seen_ids_cleanup_handle) = self.inner.subscribe_to_knock_requests().await?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Return a debug representation for the internal room events data
|
||||
/// structure, one line per entry in the resulting vector.
|
||||
pub async fn room_events_debug_string(&self) -> Result<Vec<String>, ClientError> {
|
||||
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 {
|
||||
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
|
||||
Self {
|
||||
event_id: request.event_id.to_string(),
|
||||
user_id: request.member_info.user_id.to_string(),
|
||||
room_id: request.room_id().to_string(),
|
||||
display_name: request.member_info.display_name.clone(),
|
||||
avatar_url: request.member_info.avatar_url.as_ref().map(|url| url.to_string()),
|
||||
reason: request.member_info.reason.clone(),
|
||||
timestamp: request.timestamp.map(|ts| ts.into()),
|
||||
is_seen: request.is_seen,
|
||||
actions: Arc::new(KnockRequestActions { inner: request }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new requests to a join a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait KnockRequestsListener: Send + Sync {
|
||||
fn call(&self, join_requests: Vec<KnockRequest>);
|
||||
}
|
||||
|
||||
/// An FFI representation of a request to join a room.
|
||||
#[derive(Debug, Clone, uniffi::Record)]
|
||||
pub struct KnockRequest {
|
||||
/// The event id of the event that contains the `knock` membership change.
|
||||
pub event_id: String,
|
||||
/// The user id of the user who's requesting to join the room.
|
||||
pub user_id: String,
|
||||
/// The room id of the room whose access was requested.
|
||||
pub room_id: String,
|
||||
/// The optional display name of the user who's requesting to join the room.
|
||||
pub display_name: Option<String>,
|
||||
/// The optional avatar url of the user who's requesting to join the room.
|
||||
pub avatar_url: Option<String>,
|
||||
/// An optional reason why the user wants join the room.
|
||||
pub reason: Option<String>,
|
||||
/// The timestamp when this request was created.
|
||||
pub timestamp: Option<u64>,
|
||||
/// Whether the knock request has been marked as `seen` so it can be
|
||||
/// filtered by the client.
|
||||
pub is_seen: bool,
|
||||
/// A set of actions to perform for this knock request.
|
||||
pub actions: Arc<KnockRequestActions>,
|
||||
}
|
||||
|
||||
/// A set of actions to perform for a knock request.
|
||||
#[derive(Debug, Clone, uniffi::Object)]
|
||||
pub struct KnockRequestActions {
|
||||
inner: matrix_sdk::room::knock_requests::KnockRequest,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl KnockRequestActions {
|
||||
/// Accepts the knock request by inviting the user to the room.
|
||||
pub async fn accept(&self) -> Result<(), ClientError> {
|
||||
self.inner.accept().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Declines the knock request by kicking the user from the room with an
|
||||
/// optional reason.
|
||||
pub async fn decline(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.decline(reason.as_deref()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Declines the knock request by banning the user from the room with an
|
||||
/// optional reason.
|
||||
pub async fn decline_and_ban(&self, reason: Option<String>) -> Result<(), ClientError> {
|
||||
self.inner.decline_and_ban(reason.as_deref()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Marks the knock request as 'seen'.
|
||||
///
|
||||
/// **IMPORTANT**: this won't update the current reference to this request,
|
||||
/// a new one with the updated value should be emitted instead.
|
||||
pub async fn mark_as_seen(&self) -> Result<(), ClientError> {
|
||||
self.inner.mark_as_seen().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a `matrix.to` permalink to the given room alias.
|
||||
@@ -973,7 +1303,7 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
|
||||
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
|
||||
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
|
||||
match media_source.as_ref() {
|
||||
match &media_source.as_ref().media_source {
|
||||
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
|
||||
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
|
||||
}
|
||||
@@ -1073,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,16 +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.
|
||||
@@ -54,12 +58,16 @@ pub struct RoomInfo {
|
||||
/// Events causing mentions/highlights for the user, according to their
|
||||
/// notification settings.
|
||||
num_unread_mentions: u64,
|
||||
/// The currently pinned event ids
|
||||
/// The currently pinned event ids.
|
||||
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;
|
||||
@@ -70,8 +78,14 @@ impl RoomInfo {
|
||||
let pinned_event_ids =
|
||||
room.pinned_event_ids().unwrap_or_default().iter().map(|id| id.to_string()).collect();
|
||||
|
||||
let join_rule = room.join_rule().try_into();
|
||||
if let Err(e) = &join_rule {
|
||||
warn!("Failed to parse join rule: {:?}", e);
|
||||
}
|
||||
|
||||
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(),
|
||||
@@ -118,6 +132,8 @@ impl RoomInfo {
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
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,19 +583,10 @@ 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, server_names) = if let Some(alias) = self.inner.canonical_alias() {
|
||||
let (room_or_alias_id, mut server_names) = if let Some(alias) = self.inner.canonical_alias()
|
||||
{
|
||||
let room_or_alias_id: OwnedRoomOrAliasId = alias.into();
|
||||
(room_or_alias_id, Vec::new())
|
||||
} else {
|
||||
@@ -624,6 +594,16 @@ impl RoomListItem {
|
||||
(room_or_alias_id, server_names)
|
||||
};
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
|
||||
Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview)))
|
||||
@@ -700,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()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ use ruma::{room::RoomType as RumaRoomType, space::SpaceRoomJoinRule};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
client::JoinRule, error::ClientError, room::Membership, room_member::RoomMember,
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
utils::AsyncRuntimeDropped,
|
||||
};
|
||||
|
||||
@@ -38,17 +41,33 @@ impl RoomPreview {
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("unhandled SpaceRoomJoinRule kind"))?,
|
||||
is_direct: info.is_direct,
|
||||
heroes: info
|
||||
.heroes
|
||||
.as_ref()
|
||||
.map(|heroes| heroes.iter().map(|h| h.to_owned().into()).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -57,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 {
|
||||
@@ -85,13 +118,15 @@ pub struct RoomPreviewInfo {
|
||||
/// The room type (space, custom) or nothing, if it's a regular room.
|
||||
pub room_type: RoomType,
|
||||
/// Is the history world-readable for this room?
|
||||
pub is_history_world_readable: bool,
|
||||
pub is_history_world_readable: Option<bool>,
|
||||
/// The membership state for the current user, if known.
|
||||
pub membership: Option<Membership>,
|
||||
/// The join rule for this room (private, public, knock, etc.).
|
||||
pub join_rule: JoinRule,
|
||||
/// Whether the room is direct or not, if known.
|
||||
pub is_direct: Option<bool>,
|
||||
/// Room heroes.
|
||||
pub heroes: Option<Vec<RoomHero>>,
|
||||
}
|
||||
|
||||
impl TryFrom<SpaceRoomJoinRule> for JoinRule {
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
use std::{collections::BTreeSet, sync::Arc, time::Duration};
|
||||
|
||||
use extension_trait::extension_trait;
|
||||
use matrix_sdk::attachment::{
|
||||
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
|
||||
};
|
||||
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
@@ -36,13 +34,14 @@ 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,
|
||||
VideoMessageEventContent as RumaVideoMessageEventContent,
|
||||
},
|
||||
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
@@ -154,11 +153,6 @@ impl From<&RumaMatrixId> for MatrixId {
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub fn message_event_content_new(
|
||||
msgtype: MessageType,
|
||||
@@ -200,21 +194,84 @@ pub fn message_event_content_from_html_as_emote(
|
||||
)))
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub impl MediaSourceExt for MediaSource {
|
||||
fn from_json(json: String) -> Result<MediaSource, ClientError> {
|
||||
let res = serde_json::from_str(&json)?;
|
||||
Ok(res)
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct MediaSource {
|
||||
pub(crate) media_source: RumaMediaSource,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl MediaSource {
|
||||
#[uniffi::constructor]
|
||||
pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
|
||||
let media_source = RumaMediaSource::Plain(url.into());
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).expect("Media source should always be serializable ")
|
||||
pub fn url(&self) -> String {
|
||||
self.media_source.url()
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
#[uniffi::constructor]
|
||||
pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
|
||||
let media_source: RumaMediaSource = serde_json::from_str(&json)?;
|
||||
media_source.verify()?;
|
||||
|
||||
Ok(Arc::new(MediaSource { media_source }))
|
||||
}
|
||||
|
||||
// Used on Element X Android
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(&self.media_source)
|
||||
.expect("Media source should always be serializable ")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&RumaMediaSource> for MediaSource {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
|
||||
value.verify()?;
|
||||
Ok(Self { media_source: value.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaSource> for RumaMediaSource {
|
||||
fn from(value: MediaSource) -> Self {
|
||||
value.media_source
|
||||
}
|
||||
}
|
||||
|
||||
#[extension_trait]
|
||||
pub(crate) impl MediaSourceExt for RumaMediaSource {
|
||||
fn verify(&self) -> Result<(), ClientError> {
|
||||
match self {
|
||||
RumaMediaSource::Plain(url) => {
|
||||
url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
RumaMediaSource::Encrypted(file) => {
|
||||
file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
match self {
|
||||
MediaSource::Plain(url) => url.to_string(),
|
||||
MediaSource::Encrypted(file) => file.url.to_string(),
|
||||
RumaMediaSource::Plain(url) => url.to_string(),
|
||||
RumaMediaSource::Encrypted(file) => file.url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +337,7 @@ fn get_body_and_filename(filename: String, caption: Option<String>) -> (String,
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
type Error = serde_json::Error;
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: MessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
@@ -292,7 +349,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::Image { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaImageMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaImageMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -301,16 +358,18 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::Audio { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaAudioMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaAudioMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.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 } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaVideoMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaVideoMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -319,7 +378,7 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
MessageType::File { content } => {
|
||||
let (body, filename) = get_body_and_filename(content.filename, content.caption);
|
||||
let mut event_content =
|
||||
RumaFileMessageEventContent::new(body, (*content.source).clone())
|
||||
RumaFileMessageEventContent::new(body, (*content.source).clone().into())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted_caption.map(Into::into);
|
||||
event_content.filename = filename;
|
||||
@@ -345,9 +404,11 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaMessageType> for MessageType {
|
||||
fn from(value: RumaMessageType) -> Self {
|
||||
match value {
|
||||
impl TryFrom<RumaMessageType> for MessageType {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
RumaMessageType::Emote(c) => MessageType::Emote {
|
||||
content: EmoteMessageContent {
|
||||
body: c.body.clone(),
|
||||
@@ -359,16 +420,17 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
voice: c.voice.map(Into::into),
|
||||
@@ -379,8 +441,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
@@ -388,8 +450,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
filename: c.filename().to_owned(),
|
||||
caption: c.caption().map(ToString::to_string),
|
||||
formatted_caption: c.formatted_caption().map(Into::into),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
source: Arc::new(c.source.try_into()?),
|
||||
info: c.info.as_deref().map(TryInto::try_into).transpose()?,
|
||||
},
|
||||
},
|
||||
RumaMessageType::Notice(c) => MessageType::Notice {
|
||||
@@ -425,7 +487,7 @@ impl From<RumaMessageType> for MessageType {
|
||||
msgtype: value.msgtype().to_owned(),
|
||||
body: value.body().to_owned(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,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 {
|
||||
@@ -520,8 +583,9 @@ impl From<ImageInfo> for RumaImageInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -543,6 +607,7 @@ impl TryFrom<&ImageInfo> for BaseImageInfo {
|
||||
width: Some(width),
|
||||
size: Some(size),
|
||||
blurhash: Some(blurhash),
|
||||
is_animated: value.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -595,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 {}
|
||||
|
||||
@@ -604,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>,
|
||||
@@ -625,7 +705,7 @@ impl From<VideoInfo> for RumaVideoInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
blurhash: value.blurhash,
|
||||
})
|
||||
}
|
||||
@@ -668,7 +748,7 @@ impl From<FileInfo> for RumaFileInfo {
|
||||
mimetype: value.mimetype,
|
||||
size: value.size.map(u64_to_uint),
|
||||
thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone()),
|
||||
thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -703,21 +783,6 @@ impl From<ThumbnailInfo> for RumaThumbnailInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
fn try_from(value: &ThumbnailInfo) -> Result<Self, MediaInfoError> {
|
||||
let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
|
||||
.map_err(|_| MediaInfoError::InvalidField)?;
|
||||
|
||||
Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct NoticeMessageContent {
|
||||
pub body: String,
|
||||
@@ -790,8 +855,10 @@ pub enum MessageFormat {
|
||||
Unknown { format: String },
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
fn from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Self {
|
||||
impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -799,15 +866,21 @@ impl From<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
is_animated: info.is_animated,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,8 +894,10 @@ impl From<&RumaAudioInfo> for AudioInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaVideoInfo> for VideoInfo {
|
||||
fn from(info: &RumaVideoInfo) -> Self {
|
||||
impl TryFrom<&RumaVideoInfo> for VideoInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -830,21 +905,28 @@ impl From<&RumaVideoInfo> for VideoInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
duration: info.duration,
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
blurhash: info.blurhash.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RumaFileInfo> for FileInfo {
|
||||
fn from(info: &RumaFileInfo) -> Self {
|
||||
impl TryFrom<&RumaFileInfo> for FileInfo {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
|
||||
let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
|
||||
height: info.height.map(Into::into),
|
||||
width: info.width.map(Into::into),
|
||||
@@ -852,12 +934,17 @@ impl From<&RumaFileInfo> for FileInfo {
|
||||
size: info.size.map(Into::into),
|
||||
});
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
mimetype: info.mimetype.clone(),
|
||||
size: info.size.map(Into::into),
|
||||
thumbnail_info,
|
||||
thumbnail_source: info.thumbnail_source.clone().map(Arc::new),
|
||||
}
|
||||
thumbnail_source: info
|
||||
.thumbnail_source
|
||||
.as_ref()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.map(Arc::new),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
@@ -201,6 +210,22 @@ pub struct UnableToDecryptInfo {
|
||||
/// What we know about what caused this UTD. E.g. was this event sent when
|
||||
/// we were not a member of this room?
|
||||
pub cause: UtdCause,
|
||||
|
||||
/// The difference between the event creation time (`origin_server_ts`) and
|
||||
/// the time our device was created. If negative, this event was sent
|
||||
/// *before* our device was created.
|
||||
pub event_local_age_millis: i64,
|
||||
|
||||
/// Whether the user had verified their own identity at the point they
|
||||
/// received the UTD event.
|
||||
pub user_trusts_own_identity: bool,
|
||||
|
||||
/// The homeserver of the user that sent the undecryptable event.
|
||||
pub sender_homeserver: String,
|
||||
|
||||
/// Our local user's own homeserver, or `None` if the client is not logged
|
||||
/// in.
|
||||
pub own_homeserver: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
@@ -209,6 +234,10 @@ impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
cause: value.cause,
|
||||
event_local_age_millis: value.event_local_age_millis,
|
||||
user_trusts_own_identity: value.user_trusts_own_identity,
|
||||
sender_homeserver: value.sender_homeserver.to_string(),
|
||||
own_homeserver: value.own_homeserver.map(String::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,43 +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, 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::ruma::{ImageInfo, 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) => TimelineItemContent::Message { content: message.into() },
|
||||
|
||||
Content::RedactedMessage => TimelineItemContent::RedactedMessage,
|
||||
|
||||
Content::Sticker(sticker) => {
|
||||
let content = sticker.content();
|
||||
TimelineItemContent::Sticker {
|
||||
body: content.body.clone(),
|
||||
info: (&content.info).into(),
|
||||
source: Arc::new(MediaSource::from(content.source.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
@@ -107,63 +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 From<matrix_sdk_ui::timeline::Message> for MessageContent {
|
||||
fn from(value: matrix_sdk_ui::timeline::Message) -> Self {
|
||||
Self {
|
||||
msg_type: value.msgtype().clone().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>,
|
||||
@@ -191,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,
|
||||
@@ -288,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)]
|
||||
@@ -431,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,
|
||||
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
|
||||
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,12 +50,12 @@ use ruma::{
|
||||
},
|
||||
receipt::ReceiptThread,
|
||||
room::message::{
|
||||
ForwardThread, LocationMessageEventContent, MessageType,
|
||||
LocationMessageEventContent, MessageType, ReplyWithinThread,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId,
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
@@ -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,91 +104,168 @@ 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 base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
let height = thumbnail_info
|
||||
.height
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
let width = thumbnail_info
|
||||
.width
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
let size = thumbnail_info
|
||||
.size
|
||||
.and_then(|u| UInt::try_from(u).ok())
|
||||
.ok_or(RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let mime_str =
|
||||
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
|
||||
let mime_type =
|
||||
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
|
||||
|
||||
let thumbnail = Thumbnail {
|
||||
Ok(Some(Thumbnail {
|
||||
data: thumbnail_data,
|
||||
content_type: mime_type,
|
||||
info: Some(base_thumbnail_info),
|
||||
};
|
||||
|
||||
Ok(AttachmentConfig::with_thumbnail(thumbnail))
|
||||
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
|
||||
@@ -195,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;
|
||||
});
|
||||
}
|
||||
@@ -214,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);
|
||||
}
|
||||
@@ -226,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(
|
||||
@@ -279,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(
|
||||
@@ -488,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,
|
||||
@@ -498,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(())
|
||||
@@ -545,6 +537,7 @@ impl Timeline {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
|
||||
Err(timeline::Error::EventNotInTimeline(_)) => {
|
||||
// If we couldn't edit, assume it was an (remote) event that wasn't in the
|
||||
// timeline, and try to edit it via the room itself.
|
||||
@@ -560,7 +553,8 @@ impl Timeline {
|
||||
room.send_queue().send(edit_event).await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err)?,
|
||||
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,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(
|
||||
@@ -802,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)]
|
||||
@@ -977,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::DayDivider(ts) => Some(VirtualTimelineItem::DayDivider { ts: ts.0.into() }),
|
||||
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),
|
||||
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),
|
||||
VItem::TimelineStart => Some(VirtualTimelineItem::TimelineStart),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1072,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,
|
||||
@@ -1083,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 =
|
||||
@@ -1109,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(),
|
||||
@@ -1122,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()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1246,15 +1220,19 @@ impl SendAttachmentJoinHandle {
|
||||
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VirtualTimelineItem {
|
||||
/// A divider between messages of two days.
|
||||
DayDivider {
|
||||
/// A divider between messages of different day or month depending on
|
||||
/// timeline settings.
|
||||
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.
|
||||
@@ -1277,17 +1255,34 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum EditedContent {
|
||||
RoomMessage { content: Arc<RoomMessageEventContentWithoutRelation> },
|
||||
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, mentions } => {
|
||||
Ok(SdkEditedContent::MediaCaption {
|
||||
caption,
|
||||
formatted_caption: formatted_caption.map(Into::into),
|
||||
mentions: mentions.map(Into::into),
|
||||
})
|
||||
}
|
||||
EditedContent::PollStart { poll_data } => {
|
||||
let block: UnstablePollStartContentBlock = poll_data.clone().try_into()?;
|
||||
Ok(SdkEditedContent::PollStart {
|
||||
@@ -1299,6 +1294,25 @@ impl TryFrom<EditedContent> for SdkEditedContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a caption edit.
|
||||
///
|
||||
/// If no `formatted_caption` is provided, then it's assumed the `caption`
|
||||
/// represents valid Markdown that can be used as the formatted caption.
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to retrieve some timeline item info lazily.
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineItem>);
|
||||
@@ -1324,4 +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)))
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# This git-cliff configuration file is used to generate weekly reports for This
|
||||
# Week in Matrix amongst others.
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# This Week in the Matrix Rust SDK ({{ now() | date(format="%Y-%m-%d") }})
|
||||
"""
|
||||
body = """
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {{ commit_message | upper_first }}
|
||||
{% endfor %}
|
||||
"""
|
||||
trim = true
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
filter_commits = true
|
||||
tag_pattern = "[0-9]*"
|
||||
skip_tags = ""
|
||||
ignore_tags = ""
|
||||
date_order = false
|
||||
sort_commits = "newest"
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
# This git-cliff configuration file is used to generate release reports.
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
{% set_global commit_message = commit.message -%}
|
||||
{% set_global breaking = commit.breaking -%}
|
||||
{% for footer in commit.footers -%}
|
||||
{% if footer.token | lower == "changelog" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "breaking-change" -%}
|
||||
{% set_global commit_message = footer.value -%}
|
||||
{% elif footer.token | lower == "security-impact" -%}
|
||||
{% set_global security_impact = footer.value -%}
|
||||
{% elif footer.token | lower == "cve" -%}
|
||||
{% set_global cve = footer.value -%}
|
||||
{% elif footer.token | lower == "github-advisory" -%}
|
||||
{% set_global github_advisory = footer.value -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
- {% if breaking %}[**breaking**] {% endif %}{{ commit_message | upper_first }}
|
||||
{% if security_impact -%}
|
||||
(\
|
||||
*{{ security_impact | upper_first }}*\
|
||||
{% if cve -%}, [{{ cve | upper }}](https://www.cve.org/CVERecord?id={{ cve }}){% endif -%}\
|
||||
{% if github_advisory -%}, [{{ github_advisory | upper }}](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/{{ github_advisory }}){% endif -%}
|
||||
)
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/matrix-org/matrix-rust-sdk/pull/${2}))"},
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ footer = "Security-Impact:", group = "Security" },
|
||||
{ footer = "CVE:", group = "Security" },
|
||||
{ footer = "GitHub-Advisory:", group = "Security" },
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^style", group = "Styling", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
]
|
||||
# forbid parsers from skipping breaking changes
|
||||
protect_breaking_commits = true
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = true
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = ""
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags chronologically
|
||||
date_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
@@ -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 |
@@ -2,6 +2,94 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
<!-- next-header -->
|
||||
|
||||
## [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
|
||||
|
||||
- Introduced support for
|
||||
[MSC4171](https://github.com/matrix-org/matrix-rust-sdk/pull/4335), enabling
|
||||
the designation of certain users as service members. These flagged users are
|
||||
excluded from the room display name calculation.
|
||||
([#4335](https://github.com/matrix-org/matrix-rust-sdk/pull/4335))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix an off-by-one error in the `ObservableMap` when the `remove()` method is
|
||||
called. Previously, items following the removed item were not shifted left by
|
||||
one position, leaving them at incorrect indices.
|
||||
([#4346](https://github.com/matrix-org/matrix-rust-sdk/pull/4346))
|
||||
|
||||
## [0.8.0] - 2024-11-19
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -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.8.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 = []
|
||||
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,9 +48,9 @@ as_variant = { workspace = true }
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = { version = "2.4.0", features = ["serde"] }
|
||||
decancer = "3.2.4"
|
||||
eyeball = { workspace = true }
|
||||
bitflags = { version = "2.8.0", features = ["serde"] }
|
||||
decancer = "3.2.8"
|
||||
eyeball = { workspace = true, features = ["async-lock"] }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
growable-bloom-filter = { workspace = true }
|
||||
@@ -61,9 +60,15 @@ matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = "1.11.0"
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867", "rand"] }
|
||||
unicode-normalization = "0.1.24"
|
||||
regex = "1.11.1"
|
||||
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 }
|
||||
tokio = { workspace = true }
|
||||
@@ -85,7 +90,7 @@ similar-asserts = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
wasm-bindgen-test = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ use ruma::{
|
||||
pub struct DebugListOfRawEventsNoId<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEventsNoId<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEventNoId));
|
||||
@@ -41,7 +41,7 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
pub struct DebugInvitedRoom<'a>(pub &'a InvitedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
impl fmt::Debug for DebugInvitedRoom<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InvitedRoom")
|
||||
.field("invite_state", &DebugListOfRawEvents(&self.0.invite_state.events))
|
||||
@@ -55,7 +55,7 @@ impl<'a> fmt::Debug for DebugInvitedRoom<'a> {
|
||||
pub struct DebugKnockedRoom<'a>(pub &'a KnockedRoom);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
impl fmt::Debug for DebugKnockedRoom<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("KnockedRoom")
|
||||
.field("knock_state", &DebugListOfRawEvents(&self.0.knock_state.events))
|
||||
@@ -66,7 +66,7 @@ impl<'a> fmt::Debug for DebugKnockedRoom<'a> {
|
||||
pub(crate) struct DebugListOfRawEvents<'a, T>(pub &'a [Raw<T>]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a, T> fmt::Debug for DebugListOfRawEvents<'a, T> {
|
||||
impl<T> fmt::Debug for DebugListOfRawEvents<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugRawEvent));
|
||||
|
||||
@@ -30,7 +30,7 @@ use ruma::{
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
@@ -160,12 +160,12 @@ impl PartialEq for DisplayName {
|
||||
|
||||
impl DisplayName {
|
||||
/// Regex pattern matching an MXID.
|
||||
const MXID_PATTERN: &str = "@.+[:.].+";
|
||||
const MXID_PATTERN: &'static str = "@.+[:.].+";
|
||||
|
||||
/// Regex pattern matching some left-to-right formatting marks:
|
||||
/// * LTR and RTL marks U+200E and U+200F
|
||||
/// * LTR/RTL and other directional formatting marks U+202A - U+202F
|
||||
const LEFT_TO_RIGHT_PATTERN: &str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
|
||||
const LEFT_TO_RIGHT_PATTERN: &'static str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
|
||||
|
||||
/// Regex pattern matching bunch of unicode control characters and otherwise
|
||||
/// misleading/invisible characters.
|
||||
@@ -176,7 +176,7 @@ impl DisplayName {
|
||||
/// * Blank/invisible characters (U2800, U2062-U2063)
|
||||
/// * Arabic Letter RTL mark U+061C
|
||||
/// * Zero width no-break space (BOM) U+FEFF
|
||||
const HIDDEN_CHARACTERS_PATTERN: &str =
|
||||
const HIDDEN_CHARACTERS_PATTERN: &'static str =
|
||||
"[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
|
||||
|
||||
/// Creates a new [`DisplayName`] from the given raw string.
|
||||
@@ -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 {
|
||||
@@ -476,6 +484,23 @@ impl MemberEvent {
|
||||
.unwrap_or_else(|| self.user_id().localpart()),
|
||||
)
|
||||
}
|
||||
|
||||
/// The optional reason why the membership changed.
|
||||
pub fn reason(&self) -> Option<&str> {
|
||||
match self {
|
||||
MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
|
||||
MemberEvent::Stripped(e) => e.content.reason.as_deref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The optional timestamp for this member event.
|
||||
pub fn timestamp(&self) -> Option<UInt> {
|
||||
match self {
|
||||
MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
|
||||
@@ -585,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)]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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,35 +12,93 @@
|
||||
// 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::{
|
||||
ring_buffer::RingBuffer, store_locks::memory_store_helper::try_take_leased_lock,
|
||||
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};
|
||||
use ruma::{
|
||||
events::relation::RelationType,
|
||||
time::{Instant, SystemTime},
|
||||
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use super::{EventCacheStore, EventCacheStoreError, Result};
|
||||
use crate::media::{MediaRequestParameters, UniqueKey as _};
|
||||
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 _},
|
||||
};
|
||||
|
||||
/// 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 {
|
||||
media: StdRwLock<RingBuffer<(OwnedMxcUri, String /* unique key */, Vec<u8>)>>,
|
||||
leases: StdRwLock<HashMap<String, (String, Instant)>>,
|
||||
inner: Arc<StdRwLock<MemoryStoreInner>>,
|
||||
media_service: MediaService,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
|
||||
#[derive(Debug)]
|
||||
struct MemoryStoreInner {
|
||||
media: RingBuffer<MediaContent>,
|
||||
leases: HashMap<String, (String, Instant)>,
|
||||
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
|
||||
media_retention_policy: Option<MediaRetentionPolicy>,
|
||||
last_media_cleanup_time: SystemTime,
|
||||
}
|
||||
|
||||
/// A media content in the `MemoryStore`.
|
||||
#[derive(Debug)]
|
||||
struct MediaContent {
|
||||
/// The URI of the content.
|
||||
uri: OwnedMxcUri,
|
||||
|
||||
/// The unique key of the content.
|
||||
key: String,
|
||||
|
||||
/// The bytes of the content.
|
||||
data: Vec<u8>,
|
||||
|
||||
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
|
||||
ignore_policy: bool,
|
||||
|
||||
/// The time of the last access of the content.
|
||||
last_access: SystemTime,
|
||||
}
|
||||
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
// Given that the store is empty, we won't need to clean it up right away.
|
||||
let last_media_cleanup_time = SystemTime::now();
|
||||
let media_service = MediaService::new();
|
||||
media_service.restore(None, Some(last_media_cleanup_time));
|
||||
|
||||
Self {
|
||||
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
|
||||
leases: Default::default(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,20 +121,159 @@ impl EventCacheStore for MemoryStore {
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
Ok(try_take_leased_lock(&self.leases, lease_duration_ms, key, holder))
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.events.apply_updates(room_id, updates);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
inner
|
||||
.events
|
||||
.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 })
|
||||
}
|
||||
|
||||
async fn clear_all_rooms_chunks(&self) -> Result<(), Self::Error> {
|
||||
self.inner.write().unwrap().events.clear();
|
||||
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.
|
||||
self.media.write().unwrap().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(
|
||||
@@ -86,68 +283,302 @@ impl EventCacheStore for MemoryStore {
|
||||
) -> Result<(), Self::Error> {
|
||||
let expected_key = from.unique_key();
|
||||
|
||||
let mut medias = self.media.write().unwrap();
|
||||
if let Some((mxc, key, _)) = medias.iter_mut().find(|(_, key, _)| *key == expected_key) {
|
||||
*mxc = to.uri().to_owned();
|
||||
*key = to.unique_key();
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
if let Some(media_content) =
|
||||
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
|
||||
{
|
||||
media_content.uri = to.uri().to_owned();
|
||||
media_content.key = to.unique_key();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let media = self.media.read().unwrap();
|
||||
Ok(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<()> {
|
||||
let expected_key = request.unique_key();
|
||||
|
||||
let mut media = self.media.write().unwrap();
|
||||
let Some(index) = media
|
||||
.iter()
|
||||
.position(|(_media_uri, media_key, _media_content)| media_key == &expected_key)
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
let Some(index) =
|
||||
inner.media.iter().position(|media_content| media_content.key == expected_key)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
media.remove(index);
|
||||
inner.media.remove(index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.media_service.get_media_content_for_uri(self, uri).await
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media = self.media.write().unwrap();
|
||||
let expected_key = uri.to_owned();
|
||||
let positions = media
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
|
||||
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.
|
||||
for position in positions.into_iter().rev() {
|
||||
media.remove(position);
|
||||
inner.media.remove(position);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_media_retention_policy(self, policy).await
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.media_service.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.media_service.clean_up_media_cache(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_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,19 +32,25 @@ 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;
|
||||
pub use self::{
|
||||
memory_store::MemoryStore,
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore},
|
||||
traits::{DynEventCacheStore, EventCacheStore, IntoEventCacheStore, DEFAULT_CHUNK_CAPACITY},
|
||||
};
|
||||
|
||||
/// The high-level public type to represent an `EventCacheStore` lock.
|
||||
#[derive(Clone)]
|
||||
pub struct EventCacheStoreLock {
|
||||
/// The inner cross process lock that is used to lock the `EventCacheStore`.
|
||||
cross_process_lock: CrossProcessStoreLock<LockableEventCacheStore>,
|
||||
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
|
||||
|
||||
/// The store itself.
|
||||
///
|
||||
@@ -70,11 +77,11 @@ impl EventCacheStoreLock {
|
||||
let store = store.into_event_cache_store();
|
||||
|
||||
Self {
|
||||
cross_process_lock: CrossProcessStoreLock::new(
|
||||
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
|
||||
LockableEventCacheStore(store.clone()),
|
||||
"default".to_owned(),
|
||||
holder,
|
||||
),
|
||||
)),
|
||||
store,
|
||||
}
|
||||
}
|
||||
@@ -100,13 +107,13 @@ pub struct EventCacheStoreLockGuard<'a> {
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for EventCacheStoreLockGuard<'a> {
|
||||
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EventCacheStoreLockGuard<'a> {
|
||||
impl Deref for EventCacheStoreLockGuard<'_> {
|
||||
type Target = DynEventCacheStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -138,12 +145,23 @@ pub enum EventCacheStoreError {
|
||||
#[error("Error encoding or decoding data from the event cache store: {0}")]
|
||||
Codec(#[from] Utf8Error),
|
||||
|
||||
/// The store failed to serialize or deserialize some data.
|
||||
#[error("Error serializing or deserializing data from the event cache store: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
/// The database format has changed in a backwards incompatible way.
|
||||
#[error(
|
||||
"The database format of the event cache store changed in an incompatible way, \
|
||||
current version: {0}, latest version: {1}"
|
||||
)]
|
||||
UnsupportedDatabaseVersion(usize, usize),
|
||||
|
||||
/// The store contains invalid data.
|
||||
#[error("The store contains invalid data: {details}")]
|
||||
InvalidData {
|
||||
/// Details why the data contained in the store was invalid.
|
||||
details: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventCacheStoreError {
|
||||
@@ -181,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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,11 +15,25 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::AsyncTraitDeps;
|
||||
use ruma::MxcUri;
|
||||
use matrix_sdk_common::{
|
||||
linked_chunk::{ChunkIdentifier, ChunkIdentifierGenerator, Position, RawChunk, Update},
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
|
||||
|
||||
use super::EventCacheStoreError;
|
||||
use crate::media::MediaRequestParameters;
|
||||
use super::{
|
||||
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
|
||||
EventCacheStoreError,
|
||||
};
|
||||
use crate::{
|
||||
event_cache::{Event, Gap},
|
||||
media::MediaRequestParameters,
|
||||
};
|
||||
|
||||
/// A default capacity for linked chunks, when manipulating in conjunction with
|
||||
/// an `EventCacheStore` implementation.
|
||||
// TODO: move back?
|
||||
pub const DEFAULT_CHUNK_CAPACITY: usize = 128;
|
||||
|
||||
/// An abstract trait that can be used to implement different store backends
|
||||
/// for the event cache of the SDK.
|
||||
@@ -37,6 +51,98 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error>;
|
||||
|
||||
/// An [`Update`] reflects an operation that has happened inside a linked
|
||||
/// chunk. The linked chunk is used by the event cache to store the events
|
||||
/// in-memory. This method aims at forwarding this update inside this store.
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
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.
|
||||
#[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. 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
|
||||
@@ -48,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.
|
||||
@@ -95,6 +202,23 @@ pub trait EventCacheStore: AsyncTraitDeps {
|
||||
request: &MediaRequestParameters,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get a media file's content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// In theory, there could be several files stored using the same URI and a
|
||||
/// different `MediaFormat`. This API is meant to be used with a media file
|
||||
/// that has only been stored with a single format.
|
||||
///
|
||||
/// If there are several media files for a given URI in different formats,
|
||||
/// this API will only return one of them. Which one is left as an
|
||||
/// implementation detail.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media file.
|
||||
async fn get_media_content_for_uri(&self, uri: &MxcUri)
|
||||
-> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Remove all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
@@ -105,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)]
|
||||
@@ -131,12 +291,76 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn handle_linked_chunk_updates(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
updates: Vec<Update<Event, Gap>>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.handle_linked_chunk_updates(room_id, updates).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn load_all_chunks(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
|
||||
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(
|
||||
@@ -161,9 +385,39 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
|
||||
self.0.remove_media_content(request).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_media_content_for_uri(
|
||||
&self,
|
||||
uri: &MxcUri,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
|
||||
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn set_media_retention_policy(
|
||||
&self,
|
||||
policy: MediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn media_retention_policy(&self) -> MediaRetentionPolicy {
|
||||
self.0.media_retention_policy()
|
||||
}
|
||||
|
||||
async fn set_ignore_media_retention_policy(
|
||||
&self,
|
||||
request: &MediaRequestParameters,
|
||||
ignore_policy: IgnoreMediaRetentionPolicy,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
|
||||
self.0.clean_up_media_cache().await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,10 +67,15 @@ 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().map_or(false, |relates_to| {
|
||||
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
relation_type == RelationType::Replacement
|
||||
} else {
|
||||
@@ -83,12 +84,13 @@ pub fn is_suitable_for_latest_event<'a>(
|
||||
});
|
||||
|
||||
if is_replacement {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
} else {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
}
|
||||
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
|
||||
@@ -167,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")]
|
||||
@@ -181,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")]
|
||||
@@ -214,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,
|
||||
@@ -233,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 {
|
||||
@@ -247,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
|
||||
}
|
||||
|
||||
@@ -297,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::{
|
||||
@@ -339,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(
|
||||
@@ -371,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(
|
||||
@@ -394,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(
|
||||
@@ -416,6 +427,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_call_notifications_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
|
||||
@@ -438,6 +450,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_stickers_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
@@ -460,6 +473,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event =
|
||||
@@ -482,6 +496,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
@@ -510,6 +525,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
@@ -533,6 +549,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[test]
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
@@ -552,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!");
|
||||
@@ -576,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)]
|
||||
@@ -583,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,13 +435,13 @@ 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
|
||||
.iter()
|
||||
.any(|ev| ev.event_id().map_or(false, |event_id| previous_events_ids.contains(&event_id)))
|
||||
.any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id)))
|
||||
}
|
||||
|
||||
/// Given a set of events coming from sync, for a room, update the
|
||||
@@ -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(),
|
||||
|
||||
+51
-38
@@ -18,42 +18,31 @@ use std::{
|
||||
};
|
||||
|
||||
use ruma::{
|
||||
events::{AnyGlobalAccountDataEvent, GlobalAccountDataEventType},
|
||||
events::{
|
||||
direct::OwnedDirectUserIdentifier, AnyGlobalAccountDataEvent, GlobalAccountDataEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
OwnedUserId, RoomId,
|
||||
RoomId,
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -85,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<OwnedUserId>>::new();
|
||||
for (user_id, rooms) in direct_event.content.iter() {
|
||||
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_id.clone());
|
||||
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| {
|
||||
@@ -118,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;
|
||||
});
|
||||
}
|
||||
@@ -126,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
|
||||
@@ -145,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(())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#![allow(clippy::assign_op_pattern)] // triggered by bitflags! usage
|
||||
#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
|
||||
|
||||
mod members;
|
||||
pub(crate) mod normal;
|
||||
@@ -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::{
|
||||
@@ -21,6 +21,7 @@ use ruma::{
|
||||
events::{
|
||||
beacon_info::BeaconInfoEventContent,
|
||||
call::member::{CallMemberEventContent, CallMemberStateKey},
|
||||
direct::OwnedDirectUserIdentifier,
|
||||
macros::EventContent,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
@@ -127,7 +128,7 @@ pub struct BaseRoomInfo {
|
||||
pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
|
||||
/// A list of user ids this room is considered as direct message, if this
|
||||
/// room is a DM.
|
||||
pub(crate) dm_targets: HashSet<OwnedUserId>,
|
||||
pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
|
||||
/// The `m.room.encryption` event content that enabled E2EE in this room.
|
||||
pub(crate) encryption: Option<RoomEncryptionEventContent>,
|
||||
/// The guest access policy of this room.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user