Compare commits
679 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7329c71bb | |||
| b977a239c3 | |||
| 3aa62a265d | |||
| fc4cd530fb | |||
| 7c68096237 | |||
| e12f917559 | |||
| 20f0346733 | |||
| 8e98252be3 | |||
| 1053bc9148 | |||
| 1dd497e9de | |||
| 81f1292660 | |||
| 5671121b21 | |||
| 88c4dec35f | |||
| 6d9aa14ccd | |||
| 16551feea3 | |||
| 1e7182e820 | |||
| a843125fa4 | |||
| fcfdaadb25 | |||
| 222f969e2f | |||
| 406fd011ff | |||
| b903ee4b42 | |||
| b9b5286f8a | |||
| 2f5d8c212a | |||
| 797532815a | |||
| f11aeafd58 | |||
| 327e0aef99 | |||
| 742700b7fa | |||
| 03f4a56bff | |||
| d8b0b9e2f4 | |||
| dbb9c60d09 | |||
| b6db3af882 | |||
| 8a8ad22961 | |||
| b78bbc01a0 | |||
| f51865e1ae | |||
| 682c17c9d8 | |||
| 4da1c01963 | |||
| 96a4b06ca6 | |||
| ef5b12035d | |||
| 38978dacd7 | |||
| 74ea661438 | |||
| da2abccc0d | |||
| 6731c52b12 | |||
| a3ca28f1a5 | |||
| 450ceaa241 | |||
| 6cd655ba7c | |||
| f9ab073adf | |||
| b5d7c40029 | |||
| 29f7f88a2c | |||
| db37a1feb5 | |||
| 901024bccc | |||
| 5d1fa986f3 | |||
| aa99c7bd38 | |||
| 76d484541f | |||
| 8482d181b5 | |||
| 650f210da7 | |||
| 191350a290 | |||
| 95baf17c36 | |||
| 1c1053afe6 | |||
| 7ac153fd67 | |||
| 31131146a6 | |||
| ac0bc95c25 | |||
| 11c3799fa2 | |||
| f11cf87326 | |||
| 7b092fd174 | |||
| 1fa4bb4cfa | |||
| dabc7c512c | |||
| 8fe27ab582 | |||
| 8c1d3f4f60 | |||
| a623215257 | |||
| f61de718b8 | |||
| fa5bbadf57 | |||
| 9319f4fcff | |||
| 25fb9ee47d | |||
| 85538dc3ed | |||
| 5bb2511914 | |||
| 667ada88e6 | |||
| 022b8a0f38 | |||
| 29caa02ef0 | |||
| 9102a9c841 | |||
| 99e47ed5d7 | |||
| 54729ce32b | |||
| 36e199c31e | |||
| ab9e4f73b1 | |||
| ce7143b833 | |||
| 9480450410 | |||
| 4744a994b4 | |||
| 1fd5b34fd0 | |||
| 1255027d6e | |||
| baac38fec5 | |||
| 94c0322fbe | |||
| e9a4389a12 | |||
| 67615fec3a | |||
| 95a471b0d2 | |||
| 56f4b3e70e | |||
| 6aee1f62bd | |||
| 8d968604e9 | |||
| 3a7b8fc6a5 | |||
| 7de5d295b6 | |||
| 6b394d96bd | |||
| 97959bbcd0 | |||
| 2883685bcc | |||
| eef61f87c1 | |||
| 2c9a088a36 | |||
| 8912761eb7 | |||
| 3323f37efc | |||
| 515aaf0a8a | |||
| c172ad9191 | |||
| 601dce76ef | |||
| f704066fbe | |||
| b8174c437f | |||
| a7cda30f6a | |||
| 88cd2557f3 | |||
| bd33c336e7 | |||
| 82bcf48c88 | |||
| daaf17198c | |||
| fe39ca47d6 | |||
| f42c8937da | |||
| 199275ff89 | |||
| 7df31406dc | |||
| 1edfc6cb5e | |||
| d4c1b9b8ad | |||
| d447f63e33 | |||
| c13fb7e19f | |||
| 17805cbcd8 | |||
| d2c9ca455d | |||
| 36c39b837a | |||
| b83a644260 | |||
| 7c4d180297 | |||
| b2e7ae4310 | |||
| 5d5a3044c8 | |||
| ae170362a5 | |||
| 01c5412951 | |||
| ab2b5bfa23 | |||
| c120da79d1 | |||
| 962c0bf4fd | |||
| 57b68614af | |||
| 8eafaa58fb | |||
| 96c7b3fc52 | |||
| 0a02a41a14 | |||
| a248ec75e2 | |||
| 3aa0a905b2 | |||
| 44443d3b46 | |||
| 40d96dbf27 | |||
| b2dc21d7d0 | |||
| fa5ce1d462 | |||
| e264482954 | |||
| a1c1b0e157 | |||
| 3ac123db29 | |||
| 9caec95c5e | |||
| 4bdcedbc66 | |||
| 3ccd2e9f8f | |||
| 099c855049 | |||
| 1e35188aec | |||
| c6da40cf55 | |||
| 0ff9e066fb | |||
| c59465c54c | |||
| 0c4b62f664 | |||
| 8c2831a5da | |||
| 4e8cee162a | |||
| 1e11ac406f | |||
| 10069fbead | |||
| 56da4a31a4 | |||
| ffacbe8666 | |||
| ca13be020c | |||
| baf97c69b1 | |||
| 628374b8d8 | |||
| 454d49aa64 | |||
| 06e212c11d | |||
| 213dac2d30 | |||
| 6b754acd81 | |||
| 88f75a85bb | |||
| a8e522c164 | |||
| 9dcab4ed30 | |||
| 4774cc8e65 | |||
| 9c4318d191 | |||
| 2bb07d6a4e | |||
| 44029009e4 | |||
| b2c96b72b0 | |||
| 9d281937d5 | |||
| 818a435f9e | |||
| 486b6d6e2b | |||
| 32edfb1a9f | |||
| 9159a5983b | |||
| 69ac7e07e6 | |||
| ee23839259 | |||
| 555dfe0e77 | |||
| 7f7d9b8175 | |||
| 57f6715784 | |||
| b587c064d7 | |||
| 52dc64e0db | |||
| 7b7ee980e8 | |||
| 5591be9a8e | |||
| 876d3237eb | |||
| a308d34d09 | |||
| cabab289c9 | |||
| a98779dfbb | |||
| 182e84cd3d | |||
| 5c049d6e2e | |||
| 057bca070c | |||
| 75871216d2 | |||
| 4661ca810a | |||
| f3687dc4c7 | |||
| 694fb57c17 | |||
| e1b9fe266d | |||
| 35a1596755 | |||
| 117307eaff | |||
| 52a5a86cf9 | |||
| 9faa839f56 | |||
| 5f960d889e | |||
| 655ac0725c | |||
| 229105536b | |||
| 4b85a81347 | |||
| a328d8787a | |||
| d1e92ece42 | |||
| ff4a8f0ba5 | |||
| 0a7e28f681 | |||
| e8cf6dcde6 | |||
| 505fb682af | |||
| 2520804a60 | |||
| 73684ab57c | |||
| 7718f90428 | |||
| 5e692931dd | |||
| 5e10ccc248 | |||
| 73b01743a5 | |||
| 2f9b9942c3 | |||
| a52a2329a1 | |||
| 6f9147de86 | |||
| 6a67ff9acf | |||
| 4eb3da6be7 | |||
| 78889aec8c | |||
| b0880996fc | |||
| 88a8a7007c | |||
| ff1555ed40 | |||
| 5c29c08941 | |||
| 3fb8a46c95 | |||
| 31d985813a | |||
| eea475854c | |||
| a6c2719976 | |||
| 2f58cb1620 | |||
| 0947349ae0 | |||
| 2f3b85d244 | |||
| 919d58f94b | |||
| 16dcfb2e84 | |||
| 9ef78a484c | |||
| 3edaff1364 | |||
| 45055d80cd | |||
| 1ea163271b | |||
| 899e4db8d0 | |||
| e9cca7f68d | |||
| fd709b9d52 | |||
| 4ad79d6d44 | |||
| e57a02fd91 | |||
| c41f7975b3 | |||
| cb6b420ad0 | |||
| 724d133cce | |||
| b7d6fd08f1 | |||
| 0469c27b91 | |||
| f14c00db82 | |||
| a204b2994d | |||
| 3f627f4125 | |||
| 98a68632df | |||
| 0c98e26a05 | |||
| 4b56ca1841 | |||
| 8d2e790bca | |||
| e4be216731 | |||
| 74727e5f84 | |||
| 4b711e4b37 | |||
| 4db69647ce | |||
| e922a58cc3 | |||
| eb0ddbc063 | |||
| 9fbc2ab07c | |||
| 2f7b2f0451 | |||
| ad1623da58 | |||
| 2abe3aba4a | |||
| 82684d64d4 | |||
| 7b40daa3cf | |||
| 5ee3897f7e | |||
| 370f4735f7 | |||
| 77ba3010cc | |||
| b2b9b5fa12 | |||
| c251f16292 | |||
| 8890bf3cee | |||
| 2e3ced1fb2 | |||
| 2163ab03ec | |||
| 4b1eefca80 | |||
| 4dd7c3093c | |||
| 70466aafb4 | |||
| 3e35f163b7 | |||
| 690ed4611d | |||
| 8392ef07cd | |||
| 9c33540af8 | |||
| 0401b995b7 | |||
| 0521d23e94 | |||
| 37d95571e9 | |||
| caa9a7d8be | |||
| d9231be1ba | |||
| 2068e7f266 | |||
| 26b0b32e55 | |||
| c7f3e2ad1d | |||
| a79c5286d7 | |||
| 8ac6845607 | |||
| 9f75552c9b | |||
| 3ef5214587 | |||
| 30640ebb65 | |||
| 4f3cdfacaa | |||
| b68bcf9cff | |||
| 8e64341176 | |||
| 40ba98b95e | |||
| 3541d205e0 | |||
| 2a201d4218 | |||
| 43129441db | |||
| 5ae2d83457 | |||
| 193f3331e8 | |||
| 65fe3c8b5b | |||
| 85e8771b5a | |||
| d27bfca5e4 | |||
| 77bf972b3f | |||
| 307063e571 | |||
| 18c155beb5 | |||
| 0c1b6e45d5 | |||
| b5bda577dd | |||
| 6593e32582 | |||
| d3612ce35b | |||
| bd6d0e959a | |||
| cced512ad4 | |||
| 06359b1166 | |||
| 10098d20c5 | |||
| e4c8d6b708 | |||
| 6c6a8e2e77 | |||
| dcf0069753 | |||
| f0354d1fc5 | |||
| 6634735065 | |||
| 59c468c758 | |||
| c6e93b06a3 | |||
| 89033cd13a | |||
| 88a70f472f | |||
| fafd1a403c | |||
| 371cc24031 | |||
| 2538ba68c5 | |||
| 14e93e8c0c | |||
| 54bdb7791c | |||
| 2f97bc2bae | |||
| 7e3e8fff55 | |||
| 8d878b6785 | |||
| add06bf897 | |||
| e5b07aa827 | |||
| 814d78708e | |||
| 88d7a2fe28 | |||
| 5386e9e838 | |||
| 9e6252cb2d | |||
| 12d5f51051 | |||
| c7d34bd65e | |||
| 900a6d1382 | |||
| 9228ad2f59 | |||
| 7d9ee71245 | |||
| c1c8bfda4e | |||
| 1fb717968e | |||
| e69591dad3 | |||
| ee8e9ef528 | |||
| 7cbc3e587d | |||
| aeba46a0eb | |||
| 11074d8f4d | |||
| a6c133369f | |||
| 6a81ceced0 | |||
| ffc7648ce6 | |||
| fd395a82c5 | |||
| 15afd1f690 | |||
| 1e24fbc72d | |||
| 32afc56005 | |||
| 7e61a6dd31 | |||
| cae3b38c35 | |||
| c36cb1b424 | |||
| 315a29f568 | |||
| 74931768e5 | |||
| 7eb3c30a3c | |||
| 7e99e812dd | |||
| e97b7838c5 | |||
| 8eb3fdc9e4 | |||
| 8536e2b2a3 | |||
| 060eaeffc0 | |||
| b177bd9783 | |||
| 6fa487fad8 | |||
| 12444d3dc1 | |||
| ed5d97e052 | |||
| cff844ac74 | |||
| 70e38755fa | |||
| c20e6aeca7 | |||
| 53d723d149 | |||
| 5cb587a60b | |||
| 354f9de257 | |||
| 9d04b23f45 | |||
| 73edf6b734 | |||
| 5114474829 | |||
| 91331bea51 | |||
| 79f97504f8 | |||
| 50ed681a4e | |||
| 910887fbc6 | |||
| 304bd910f0 | |||
| e87a7954a0 | |||
| d2b02ec2e8 | |||
| d84387d12e | |||
| 368b585124 | |||
| 7bbd07cc77 | |||
| 008330a744 | |||
| 1052c8db15 | |||
| 3e04590ded | |||
| ce2d8212eb | |||
| 5ad9090dc8 | |||
| b15829a9fd | |||
| bebb733607 | |||
| e872babd40 | |||
| bcb125a09b | |||
| 65774aa90b | |||
| 868bb9a8d9 | |||
| 61c7a96e36 | |||
| e6dadf2b0d | |||
| 75454de284 | |||
| 8b298dfd2f | |||
| 31ba7b82d8 | |||
| 71f4af9cdd | |||
| fd716bcd81 | |||
| 385e6933d2 | |||
| fab1c1c299 | |||
| 9bf48ef041 | |||
| 2e9f362ae4 | |||
| fce1140ad1 | |||
| 0c1d90d901 | |||
| 717dc1184b | |||
| 99e0593f90 | |||
| f6f6cfd844 | |||
| d905dcc476 | |||
| 790fe5f6db | |||
| f2652ee9f2 | |||
| 099bf9c929 | |||
| 5957d9603b | |||
| 3d60ac36d2 | |||
| 787d04190e | |||
| e33c44266e | |||
| 17e8109ab6 | |||
| 14246c7094 | |||
| 6a34f54753 | |||
| 78e3350b17 | |||
| e3c7e9db9b | |||
| dc89c00a8c | |||
| 5baf078c4b | |||
| f950e67b39 | |||
| 2c688fd40f | |||
| e94fd2a7df | |||
| d699b2fa70 | |||
| 61d3f8d1d9 | |||
| 435d74a67a | |||
| fe2ca34179 | |||
| 3b068b592d | |||
| 344a96a80f | |||
| 4e8c63e4e0 | |||
| 452cf0f269 | |||
| e78c4b89b4 | |||
| bcf8cc1c57 | |||
| f47e7b3427 | |||
| 8252379f76 | |||
| 2ef1e32f9a | |||
| 47a4f473a6 | |||
| 9d6d2c3b3c | |||
| bd38dc971b | |||
| 4712c318cc | |||
| a92140f3ac | |||
| 88d10210ad | |||
| de9adc8d3d | |||
| 51704fa093 | |||
| 0e91c085f9 | |||
| 49630fdd9f | |||
| a1f1992d7e | |||
| d64a5e4dcc | |||
| f0c97d94b2 | |||
| 5d0c6a608a | |||
| 3dbfdcddf4 | |||
| e9b11408bb | |||
| d26b8cca1c | |||
| 6c5400896a | |||
| 8b7ebece49 | |||
| 4def306eb8 | |||
| 3d4db7e220 | |||
| 6fe98c3f43 | |||
| 375c6c33c6 | |||
| edf23dbd2e | |||
| 8d87e32f8b | |||
| 2f6d0450a7 | |||
| e07ed14d20 | |||
| ae97772909 | |||
| 8975e6ab5d | |||
| 32aa784c5f | |||
| 56e155300c | |||
| df61037ac5 | |||
| 471b470c01 | |||
| 56d9f9d9a8 | |||
| 6e685e2e09 | |||
| ad5c1202b5 | |||
| 87a07d9ee3 | |||
| e0bda80e7b | |||
| b9ef5d6270 | |||
| fa26499a39 | |||
| 916e85f79e | |||
| f2b61eb5e2 | |||
| 40e006658c | |||
| ac2fce8220 | |||
| 296d613138 | |||
| 43bed281b1 | |||
| b951d0d785 | |||
| a802b733f8 | |||
| 6acd68cf00 | |||
| 75365a7774 | |||
| 61a5f1b3cf | |||
| 44e438b21b | |||
| 7b49283394 | |||
| e0384494b6 | |||
| cc236e39d5 | |||
| c4d5657162 | |||
| 59f9f43f15 | |||
| 261eb99614 | |||
| 5ae638047e | |||
| 8ee0021641 | |||
| 39241c5e18 | |||
| 9ef9251936 | |||
| 90f1a34855 | |||
| e00532f5d2 | |||
| 8cef7a54d9 | |||
| f64af126f1 | |||
| e3da325f29 | |||
| 860fe4afe0 | |||
| e3ba3366f1 | |||
| e604277e4c | |||
| a356a20c3d | |||
| 1c1ecf0ab0 | |||
| fd26cbcfcb | |||
| 3a543f188b | |||
| 06fe8a8f32 | |||
| 6ef33619bf | |||
| cde6a559ad | |||
| 3b20cc4444 | |||
| 3599e578e2 | |||
| bc6c458371 | |||
| f0d722099f | |||
| bcf1ee408b | |||
| ef322cc39b | |||
| f5f8f47667 | |||
| c418f0e6ba | |||
| cde7dd2ea8 | |||
| debc28fa47 | |||
| 6e3892528c | |||
| fca455c0bf | |||
| cd90265eab | |||
| 25b2b191bb | |||
| 3ade16a07d | |||
| 411dd29244 | |||
| 1e74be2b2d | |||
| a77b67eecd | |||
| 18eefcffdb | |||
| 2a5dcd562c | |||
| a3b8ec3640 | |||
| 1fa488ec04 | |||
| 25ecf089aa | |||
| 8573417537 | |||
| f20c92a323 | |||
| be8be9ef04 | |||
| eccfab8b27 | |||
| 25e4c8d722 | |||
| a9d6ab7313 | |||
| 4d2c3e33fc | |||
| 0be2747ccf | |||
| 0b01a2a53a | |||
| d85c7b213f | |||
| 52b10e776c | |||
| d88970155d | |||
| 1139538c76 | |||
| a1a93afbe2 | |||
| 7a107d8fa2 | |||
| 5117325ef4 | |||
| 1085b85dfc | |||
| 7a7e624e7b | |||
| 384d8b0e19 | |||
| 68c9e5e98d | |||
| ff89315b6e | |||
| eec52d7977 | |||
| 8515def61b | |||
| 75ea0ad283 | |||
| ac24f62d8c | |||
| 76cd7ab649 | |||
| eab35c1289 | |||
| 1d6c01fb34 | |||
| b680c50035 | |||
| 20b8c41727 | |||
| e9d2b1f27a | |||
| f90d1678e2 | |||
| 92b7598e4b | |||
| 10779481b7 | |||
| 2314a74cdb | |||
| 5e0e752fcd | |||
| b4b460ad27 | |||
| 6d5f617355 | |||
| 9f0a1e0ce7 | |||
| d6484a012c | |||
| f7cb253b1c | |||
| 2dbd4a28a1 | |||
| 5c3edfcf6a | |||
| 20b97fc716 | |||
| b3f4e658c5 | |||
| a3fe9c357b | |||
| 2fcd28ab0c | |||
| 5e029aa7a0 | |||
| 95edb9f864 | |||
| 7116ed4848 | |||
| c00f9e9312 | |||
| 26896599de | |||
| 3b5238135d | |||
| 0e5cc44ffa | |||
| ca47e6d5f9 | |||
| 75f1aaced6 | |||
| de316acc11 | |||
| 706e477e02 | |||
| 6716ad6374 | |||
| 92df885474 | |||
| ddb44d35cd | |||
| 8de6345d0f | |||
| e6d33eaf90 | |||
| fa74602c65 | |||
| 5cbc34811f | |||
| 2d60611a44 | |||
| ff387609c3 | |||
| b05a0ecb3b | |||
| 784c7459fe | |||
| 18065cb42e | |||
| 01d11888f4 | |||
| 8aef687dcb | |||
| 79bb716298 | |||
| 8ef09b4044 | |||
| 5f435a86ee | |||
| 58be7b0f3d | |||
| 7034104ef1 | |||
| c4113060b2 | |||
| 3d1de1f583 | |||
| 147f1d3f6c | |||
| e3af74739d | |||
| 8d0867a1a8 | |||
| cfe3bb7cef | |||
| 95b95aae5f | |||
| 83d7cd5430 | |||
| d0e5d84bff | |||
| fb4b5ea48f | |||
| 985f9f9b07 | |||
| c7f7828368 | |||
| 3fea9ae51a | |||
| 3c4b892129 | |||
| a7d2ff327d | |||
| 90c1858389 | |||
| 289268a60b | |||
| 04d1fd4b8a | |||
| 97b2a20338 | |||
| 62641adfeb | |||
| 6e407989e8 | |||
| 1521ec6cb2 | |||
| b3ada6cb37 | |||
| 21b7e54a59 | |||
| 463b0dfb91 | |||
| f48695cc95 | |||
| 395a39e039 | |||
| e37f5e5ac3 | |||
| 1bc921f373 | |||
| 36e69f30ec | |||
| 82c60116ef | |||
| 840cb97c21 | |||
| ae15595af1 | |||
| f89ed01182 | |||
| 2e803c3a3d | |||
| 7a3a3edc9d | |||
| 13718c6d49 | |||
| 767b45dcf7 | |||
| b690db1e05 | |||
| 5ab397e8d9 | |||
| 0d938d94aa |
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-02-06
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Benchmarks
|
||||
|
||||
@@ -210,11 +210,8 @@ jobs:
|
||||
- name: '[m]-common'
|
||||
cmd: matrix-sdk-common
|
||||
|
||||
- name: '[m]-indexeddb, no crypto'
|
||||
cmd: indexeddb-no-crypto
|
||||
|
||||
- name: '[m]-indexeddb, with crypto'
|
||||
cmd: indexeddb-with-crypto
|
||||
- name: '[m]-indexeddb'
|
||||
cmd: indexeddb
|
||||
|
||||
- name: '[m], no-default, wasm-flags'
|
||||
cmd: matrix-sdk-no-default
|
||||
@@ -281,7 +278,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-02-06
|
||||
components: rustfmt
|
||||
|
||||
- name: Cargo fmt
|
||||
@@ -318,7 +315,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-02-06
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
|
||||
@@ -105,13 +105,13 @@ jobs:
|
||||
--features experimental-widgets,testing
|
||||
env:
|
||||
CARGO_PROFILE_COV_INHERITS: 'dev'
|
||||
CARGO_PROFILE_COV_DEBUG: 'false'
|
||||
CARGO_PROFILE_COV_DEBUG: 1
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
SLIDING_SYNC_PROXY_URL: "http://localhost:8118"
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# Work around frequent upload errors, for runs inside the main repo (not PRs from forks).
|
||||
# Otherwise not required for public repos.
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2023-11-08
|
||||
toolchain: nightly-2024-02-06
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Rust version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
|
||||
@@ -7,6 +7,8 @@ emsdk-*
|
||||
.build
|
||||
.swiftpm
|
||||
/Package.swift
|
||||
# code coverage report
|
||||
cobertura.xml
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
+5
-3
@@ -22,12 +22,14 @@ sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
Nd = "Nd"
|
||||
ratatui = "ratatui"
|
||||
|
||||
[files]
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
|
||||
# automatically ignored, we ignore them here.
|
||||
extend-exclude = [
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys.
|
||||
"*.json",
|
||||
# We are using some fuzzy match patterns that can be understood as typos confusingly.
|
||||
# Fuzzy match patterns that can be understood as typos confusingly.
|
||||
"crates/matrix-sdk-ui/tests/integration/room_list_service.rs",
|
||||
# Hand-crafted base64 session keys that are understood as typos.
|
||||
"crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs",
|
||||
]
|
||||
|
||||
@@ -29,6 +29,44 @@ integration tests that need a running synapse instance. These tests reside in
|
||||
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
|
||||
synapse for testing purposes.
|
||||
|
||||
## Commit messages and PR title guidelines
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and each commit
|
||||
should have a *good commit message*.
|
||||
|
||||
An *atomic logical commit* is one that is ideally small, can be compiled in isolation, and passes
|
||||
tests. This is useful to make the review process easier (help your reviewer), but also when running
|
||||
bisections, helping identifying which commit introduced a regression.
|
||||
|
||||
A *good commit message* should be composed of:
|
||||
|
||||
- a prefix to indicate which area/feature is related by the commit
|
||||
- a short description that would give sufficient context for a reviewer to guess what the commit is
|
||||
about.
|
||||
|
||||
Examples of commit messages that aren't so useful:
|
||||
|
||||
- “add new method“
|
||||
- “enhance performance“
|
||||
- “fix receipts“
|
||||
|
||||
Examples of good commit messages:
|
||||
|
||||
- “ffi: Add new method `frobnicate_the_foos`”
|
||||
- “indexeddb: Break up the request inside `get_inbound_group_sessions`”
|
||||
- “read_receipts: Store receipts locally, fixing #12345”
|
||||
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR, following the
|
||||
same guidelines of a good commit message, including the area/feature prefix. Something like
|
||||
`FFI: Allow logs files to be pruned.` would be a good PR title.
|
||||
|
||||
(An additional bad example of a bad PR title would be `mynickname/branch name`, that is, just the
|
||||
branch name.)
|
||||
|
||||
Having good commit messages and PR titles also helps with reviews, scanning the `git log` of
|
||||
the project, and writing the [*This week in
|
||||
Matrix*](https://matrix.org/category/this-week-in-matrix/) updates for the SDK.
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
|
||||
Generated
+987
-586
File diff suppressed because it is too large
Load Diff
+23
-8
@@ -6,15 +6,16 @@ members = [
|
||||
"crates/*",
|
||||
"testing/*",
|
||||
"examples/*",
|
||||
"labs/*",
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
]
|
||||
# xtask, testing and the bindings should only be built when invoked explicitly.
|
||||
default-members = ["benchmarks", "crates/*"]
|
||||
default-members = ["benchmarks", "crates/*", "labs/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
rust-version = "1.70"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.68"
|
||||
@@ -32,11 +33,21 @@ eyeball-im = { version = "0.4.1", features = ["tracing"] }
|
||||
eyeball-im-util = "0.5.1"
|
||||
futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.26", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
http = "0.2.6"
|
||||
imbl = "2.0.0"
|
||||
itertools = "0.12.0"
|
||||
ruma = { version = "0.9.3", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = "0.12.0"
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "4c00bd010dbdca6005bd599b52e90a0b7015d056", features = [
|
||||
"client-api-c",
|
||||
"compat-upload-signatures",
|
||||
"compat-user-id",
|
||||
"compat-arbitrary-length-ids",
|
||||
"compat-tag-info",
|
||||
"unstable-msc3401",
|
||||
] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "4c00bd010dbdca6005bd599b52e90a0b7015d056" }
|
||||
once_cell = "1.16.0"
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.151"
|
||||
@@ -49,9 +60,10 @@ tokio = { version = "1.30.0", default-features = false, features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tracing = { version = "0.1.40", default-features = false, features = ["std"] }
|
||||
tracing-core = "0.1.32"
|
||||
uniffi = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
uniffi_bindgen = { version = "0.25.3", git = "https://github.com/mozilla/uniffi-rs", rev = "0d58c94cbd2ef63554f3388d03d55984be76bb1f" }
|
||||
vodozemac = "0.5.0"
|
||||
uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "789a9023b522562a95618443cee5a0d4f111c4c7" }
|
||||
uniffi_bindgen = { git = "https://github.com/mozilla/uniffi-rs", rev = "789a9023b522562a95618443cee5a0d4f111c4c7" }
|
||||
vodozemac = { git="https://github.com/matrix-org/vodozemac", rev = "0c75746fc8a5eda4a0e490d345d1798b4c6cbd67" }
|
||||
wiremock = "0.5.21"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.7.0", default-features = false }
|
||||
@@ -95,3 +107,6 @@ opt-level = 3
|
||||
[patch.crates-io]
|
||||
async-compat = { git = "https://github.com/jplatte/async-compat", rev = "16dc8597ec09a6102d58d4e7b67714a35dd0ecb8" }
|
||||
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
assigning_clones = "allow"
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Releasing `matrix-rust-sdk`
|
||||
|
||||
- Make sure to bump all the crates to *the same version number*, and commit that (along with the
|
||||
changes to the `Cargo.lock` file).
|
||||
- Create a `git tag` for the current version, following the format `major.minor.patch`, e.g. `0.7.0`.
|
||||
- Push the tag: `git push origin 0.7.0`
|
||||
- Publish all the crates, in topological order of the dependency tree:
|
||||
|
||||
```
|
||||
cargo publish -p matrix-sdk-test-macros
|
||||
cargo publish -p matrix-sdk-test
|
||||
cargo publish -p matrix-sdk-common
|
||||
cargo publish -p matrix-sdk-qrcode
|
||||
cargo publish -p matrix-sdk-store-encryption
|
||||
cargo publish -p matrix-sdk-crypto
|
||||
cargo publish -p matrix-sdk-base
|
||||
cargo publish -p matrix-sdk-sqlite
|
||||
cargo publish -p matrix-sdk-indexeddb
|
||||
cargo publish -p matrix-sdk
|
||||
cargo publish -p matrix-sdk-ui
|
||||
```
|
||||
@@ -15,6 +15,7 @@ matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
matrix-sdk = { workspace = true }
|
||||
ruma = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.24.2", default-features = false, features = ["rt-multi-thread"] }
|
||||
@@ -29,3 +30,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "store_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "room_bench"
|
||||
harness = false
|
||||
|
||||
@@ -194,7 +194,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -225,7 +225,7 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use matrix_sdk::utils::IntoRawStateEventContent;
|
||||
use matrix_sdk_base::{
|
||||
store::StoreConfig, BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteStateStore;
|
||||
use matrix_sdk_test::EventBuilder;
|
||||
use ruma::{
|
||||
api::client::membership::get_member_events,
|
||||
device_id,
|
||||
events::room::member::{RoomMemberEvent, RoomMemberEventContent},
|
||||
owned_room_id,
|
||||
serde::Raw,
|
||||
user_id, OwnedUserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
pub fn receive_all_members_benchmark(c: &mut Criterion) {
|
||||
const MEMBERS_IN_ROOM: usize = 100000;
|
||||
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let room_id = owned_room_id!("!room:example.com");
|
||||
|
||||
let ev_builder = EventBuilder::new();
|
||||
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();
|
||||
member_events.push(event);
|
||||
}
|
||||
|
||||
// Create a fake list of changes, and a session to recover from.
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
|
||||
for member_event in member_events.iter() {
|
||||
let event = member_event.clone().cast();
|
||||
changes.add_state_event(&room_id, event.deserialize().unwrap(), event);
|
||||
}
|
||||
|
||||
// Sqlite
|
||||
let sqlite_dir = tempfile::tempdir().unwrap();
|
||||
let sqlite_store = runtime.block_on(SqliteStateStore::open(sqlite_dir.path(), None)).unwrap();
|
||||
runtime
|
||||
.block_on(sqlite_store.save_changes(&changes))
|
||||
.expect("initial filling of sqlite failed");
|
||||
|
||||
let base_client = BaseClient::with_store_config(StoreConfig::new().state_store(sqlite_store));
|
||||
runtime
|
||||
.block_on(base_client.set_session_meta(SessionMeta {
|
||||
user_id: user_id!("@somebody:example.com").to_owned(),
|
||||
device_id: device_id!("DEVICE_ID").to_owned(),
|
||||
}))
|
||||
.expect("Could not set session meta");
|
||||
base_client.get_or_create_room(&room_id, RoomState::Joined);
|
||||
|
||||
let request = get_member_events::v3::Request::new(room_id.clone());
|
||||
let response = get_member_events::v3::Response::new(member_events);
|
||||
|
||||
let count = MEMBERS_IN_ROOM;
|
||||
let name = format!("{count} members");
|
||||
let mut group = c.benchmark_group("Test");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.sample_size(50);
|
||||
|
||||
group.bench_function(BenchmarkId::new("receive_members", name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
base_client.receive_all_members(&room_id, &request, &response).await.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let _guard = runtime.enter();
|
||||
drop(base_client);
|
||||
}
|
||||
|
||||
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 = room;
|
||||
config = criterion();
|
||||
targets = receive_all_members_benchmark,
|
||||
}
|
||||
criterion_main!(room);
|
||||
@@ -2,9 +2,9 @@ import XCTest
|
||||
@testable import MatrixRustSDK
|
||||
|
||||
final class ClientTests: XCTestCase {
|
||||
func testBuildingWithHomeserverURL() {
|
||||
func testBuildingWithHomeserverURL() async {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
_ = try await ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.build()
|
||||
} catch {
|
||||
@@ -12,9 +12,9 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithHomeserverURLAndUserAgent() {
|
||||
func testBuildingWithHomeserverURLAndUserAgent() async {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
_ = try await ClientBuilder()
|
||||
.homeserverUrl(url: "https://localhost:8008")
|
||||
.userAgent(userAgent: "golden-eye/007")
|
||||
.build()
|
||||
@@ -23,24 +23,14 @@ final class ClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithUsername() {
|
||||
func testBuildingWithInvalidUsername() async {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
.username(username: "@test:matrix.org")
|
||||
.build()
|
||||
} catch {
|
||||
XCTFail("The client should build successfully when given a username.")
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildingWithInvalidUsername() {
|
||||
do {
|
||||
_ = try ClientBuilder()
|
||||
_ = try await ClientBuilder()
|
||||
.username(username: "@test:invalid")
|
||||
.build()
|
||||
|
||||
XCTFail("The client should not build when given an invalid username.")
|
||||
} catch ClientError.Generic(let message) {
|
||||
} catch ClientBuildError.Sdk(let message) {
|
||||
XCTAssertTrue(message.contains(".well-known"), "The client should fail to do the well-known lookup.")
|
||||
} catch {
|
||||
XCTFail("Not expecting any other kind of exception")
|
||||
|
||||
@@ -41,7 +41,7 @@ features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
workspace = true
|
||||
features = ["qrcode", "automatic-room-key-forwarding"]
|
||||
features = ["qrcode", "automatic-room-key-forwarding", "uniffi"]
|
||||
|
||||
[dependencies.matrix-sdk-sqlite]
|
||||
workspace = true
|
||||
@@ -53,9 +53,11 @@ default_features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
vergen = { version = "8.2.5", features = ["build", "git", "gitcl"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.8.0"
|
||||
assert_matches2 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -31,7 +31,6 @@ fn setup_x86_64_android_workaround() {
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
setup_x86_64_android_workaround();
|
||||
uniffi::generate_scaffolding("./src/olm.udl")?;
|
||||
|
||||
EmitBuilder::builder().git_sha(true).git_describe(true, false, None).emit()?;
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ pub struct Device {
|
||||
/// Is our cross signing identity trusted and does the identity trust the
|
||||
/// device.
|
||||
pub cross_signing_trusted: bool,
|
||||
/// The first time this device was seen in local timestamp, seconds since
|
||||
/// epoch.
|
||||
/// The first time this device was seen in local timestamp, milliseconds
|
||||
/// since epoch.
|
||||
pub first_time_seen_ts: u64,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ mod responses;
|
||||
mod users;
|
||||
mod verification;
|
||||
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Context as _;
|
||||
pub use backup_recovery_key::{
|
||||
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
|
||||
};
|
||||
@@ -31,11 +31,10 @@ pub use logger::{set_logger, Logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::ShieldState as RustShieldState;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::SignatureState,
|
||||
olm::{IdentityKeys, InboundGroupSession, Session},
|
||||
store::{Changes, CryptoStore, PendingChanges, RoomSettings as RustRoomSettings},
|
||||
types::{EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey},
|
||||
EncryptionSettings as RustEncryptionSettings, LocalTrust,
|
||||
EncryptionSettings as RustEncryptionSettings,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
pub use responses::{
|
||||
@@ -129,10 +128,10 @@ pub struct PickledSession {
|
||||
pub sender_key: String,
|
||||
/// Was the session created using a fallback key.
|
||||
pub created_using_fallback_key: bool,
|
||||
/// The Unix timestamp when the session was created.
|
||||
pub creation_time: String,
|
||||
/// The Unix timestamp when the session was last used.
|
||||
pub last_use_time: String,
|
||||
/// Unix timestamp (in seconds) when the session was created.
|
||||
pub creation_time: u64,
|
||||
/// Unix timestamp (in seconds) when the session was last used.
|
||||
pub last_use_time: u64,
|
||||
}
|
||||
|
||||
/// A pickled version of an `InboundGroupSession`.
|
||||
@@ -244,9 +243,11 @@ async fn migrate_data(
|
||||
user_id: parse_user_id(&data.account.user_id)?,
|
||||
device_id: device_id.clone(),
|
||||
pickle,
|
||||
dehydrated: false, // dehydrated devices are never involved in migration
|
||||
shared: data.account.shared,
|
||||
uploaded_signed_key_count: data.account.uploaded_signed_key_count as u64,
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch(UInt::default()),
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
|
||||
fallback_key_creation_timestamp: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
};
|
||||
let account = matrix_sdk_crypto::olm::Account::from_pickle(pickled_account)?;
|
||||
|
||||
@@ -423,8 +424,12 @@ fn collect_sessions(
|
||||
vodozemac::olm::Session::from_libolm_pickle(&session_pickle.pickle, pickle_key)?
|
||||
.pickle();
|
||||
|
||||
let creation_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.creation_time)?);
|
||||
let last_use_time = SecondsSinceUnixEpoch(UInt::from_str(&session_pickle.last_use_time)?);
|
||||
let creation_time = SecondsSinceUnixEpoch(
|
||||
UInt::new(session_pickle.creation_time).context("invalid creation timestamp")?,
|
||||
);
|
||||
let last_use_time = SecondsSinceUnixEpoch(
|
||||
UInt::new(session_pickle.last_use_time).context("invalid last use timestamp")?,
|
||||
);
|
||||
|
||||
let pickle = matrix_sdk_crypto::olm::PickledSession {
|
||||
pickle,
|
||||
@@ -839,6 +844,7 @@ impl From<RoomSettings> for RustRoomSettings {
|
||||
Self {
|
||||
algorithm: value.algorithm.into(),
|
||||
only_allow_trusted_devices: value.only_allow_trusted_devices,
|
||||
..RustRoomSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -881,7 +887,7 @@ fn vodozemac_version() -> String {
|
||||
vodozemac::VERSION.to_owned()
|
||||
}
|
||||
|
||||
uniffi::include_scaffolding!("olm");
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -901,36 +907,35 @@ mod tests {
|
||||
"pickle":"FFGTGho89T3Xgd56l+EedOPV37s09RR8aYnS9305qPKF66LG+ly29YpCibjJOvkwm0dZwN9A2bOH/z7WscriqwZn/p0GE6YSNwLzffCy5iROzYzpYzFe0HtiyJmCQWCezvLc5lHV8YsfD00C1pKGX2R9M1wwp3/n4/3VjtTyPsdnmtwAPu4WdcPSkisCaQ3a6JaSKqv8zYzUjnpzgcpXHvPUR5d5+TzXgrVz3BeCOe8NEOWIW6xYUxFtGteYP0BczOkkJ22t7Css0tSMSrYgCll4zZUGNrd6D9b/z7KwcDnb978epsZ16DcZ/aaTxPdM5uDIkHgF/qHWerfxcaqsqs4EQfJdSgOTeqhjHBw1k0uWF2bByJLK+n7sGkYXEAuTzc4+0XvSFvu3Qp+1bHZuT7QejngRZzyxznORyBxd8la3/JjeJlehSK80OL7zSmohoYZD59S6i3tFWfopjQThJ0/eIyVOhEN/c3tfIcVr3lFEQeokgpCRNOVldhPcQWq994NHaL7jtb6yhUqT1gShY4zYayFL/VRz6nBSXXYwzrC9jho67knqXSri3lIKYevP9aOi384IvzbkinQdumc804dYwiCbs5hZppfEnfhfgiDDm+kVrJ9WaPRF4SySCTlS8jdGmBeL2CfCQ5IcZ5nK6X7tZM3tmtYwva0RuQiTNltp3XTfbMa0EoaEBximv25165hFTpzrWgoszBTpZPfgsMuWENWBcIX4AcLSk0CJ0qzPDeUwvmRcFStstGYV4drs5u5HEqovFSI48CoHPSEZfwwERCI4c/0efZ0CVEfnm8VcMv3AbnAfedD7v3QNdVwWOEhz/fGR76BQi2WjZP4MWvYRJ/vsLO5hcVWUvaJGQs5kANUFZMWuJQeJv3DmkV9kKKXnyfFUerlQ4Uk/5tp2mXiG+adHjuRp/Eeh5V/biCcIaX3rNuIY6MJaPz6SOwlFe79MMBaNwaS3j4Kh/Aq9BRw0QXdjO4CqMI4p2xCE1N5QTPdeaRTHTZ3r7mLkHX3FpZMxitc8vDl9L2FRoSOMMh/sRD1boBCkjrsty9rvTUGYY3li05jBuTXnYMjA4zj79dC9TGo4g+/wi+h537EhtP5+170LwqnIzfHt8yfjbsMMC7iwLpC1C57sTwxpMkNo3nQEvZOfqCxjq+ihiGuL9iN5lSstu9/C4qP2tQll86ASXf1axxRZQlUB0hlLHbEW6/7O7xOU6FTs4yXAZC04souRkggmfhDzZ9kQmN/zRTbqlATFI7l9/0VGxwLOVnCMUhgiDX5yL8CYK9I4ENMLf5zOuO6P3GbYISjEoHC7fUOzQ6OwGgLyI0wCEVdSJzQcdKh+W15VV+eDjhE/qEJHQWx024hTQFTKYHlDn95+lMmRI9BJLP1HU2JW6onVWsTsE5zSYu9jLj739EKfV4gS/pWzoQDRa7a9ZG6+m+RrwyJhCso3gkUekDNobhFlDX6YeH+Btj91N0uS3F9qr8lbo491s/z2fNV42zT4NYObzgrAYDQAV/2WYF8tXtxLV/Jzk8AMmyr/cfNaT2dXxVJKWq+nN2BYHBmg9CCWPJ2aB/1WWIcHfcDOlngtH991gP6246f/DEaVC/Ayxz7bPtSH5tlZ4Xbpc2P4BYxaRp/yxhhQ2C9H2I/PTt3mnNNgky/t8PZrN3W5+eiSVE9sONF8G3mYsa4XFqM+KxfbPUqsrEnrRBmvmJ250hpTPkFcIF775RvvRRKALXdlTKs+S4HKDW7KoP0Dm9+r4RlO0UHpWND9w0WSMItvWQyo0VViXJgZfBjYtWDoO0Ud+Kc7PLWNX6RUKY7RlDjXadJTC4adH6CN3UBC/ouqqfTrYvPOkyd2oKf4RLjEVcFAUIftFbLy+WBcWv8072nnAFJIlm3CxGq++80TyjqFR45P+qfIJavxQNIt5zhHPfMgHjX27OA3+l7rHDxqfMLBPxhtARwlyF+qx1IJiSWbmlHkdz2ylD9unoLSpf+DmmFvvgTj+3EEP4bY2jA/t91XFeG3uaTQSy3ryDvhbX21U7G2HGOEl9rCkmz+hG0YRB/6KxZZ0eMIDr7OWfpPEuHV8oYwDNYbsT9zCGsR1hHxBJtdo60b36mjMemtf761DhJ/oQZ4eU738yzx1hvVS3aCJsfyp70H5u+pUjgrA565uG2lEMNLu4T4NFVw0UdrVudyrhmT8P7vF4v+mR4pp+OzRbLf8AtZrKmHlMqRst+/wOHUHug/Tpz6EwZPDWGiQyFyPUkjHWW7ACouegBFOWFabsk+zCDhyxoSNrSMCtdB1L+qK72jRPGOvXk8p/1kBOIJfAjaK1ZWz8hTc30hOSWYxkRP296zPHiQF0ibNYSPNZ9tNxgq9nV/cEQ68TsNr3SULfDr0TSjCPf4AfmJ0k1k5xphSYv/TtGIbjg/9yGVFqclg4Y/6rrfkApbx36PQEBNxLiRsZ4hGpCfVU6h0jOekk8TV6CAguXVX/G31UqsAEa4sOD2g10Ir+5JD7bdd3JE/999kHGdiCqc0DNcgSqWYbq2QYwrN/mb+mMUbiQSNMcc34kK1n+7dGxppnt7YN7UsJqBWJdH0Lw1Epxi11ViTeVma9bqioJYXi6N5exdpZTT7KmcGYFsoTqO958EX6AppgcML7N9oP3TO8qSgCpV3Bbbemq4bvjV43aM6Rdx17pC4GZo0jjU97p4K8jE4PvgoHlYkuPwSJDOSAdnYPh+Inq/vCk48UfIlup0ATJFVUXD7uf84v9roZSwZPXZ5j/88+MkHBIJwPv8cugmz5uN2EuBW5IScMuEqG7Cmk72SU3/QA39G79S0Gpw7iPhTos5LXxhfvohGcnSaNEvfNeecQf7fpVciTdHwuvcgqJizUKpSFg2P+LDBiO44mJD15RNAaT37Rrj5P06YITO4PDj+FMdc6gx+JQUFbcSRhScE/0gfsVm0P1BYIH5q0k/QDgEVoerf/n19lITTzPib1F2OHP4hyF3BEq1pd9NwuPhhsVVqTVTK5MzFwFIOH7cwJyY7aBykmsWBavdb2J7UA5wjKqMHl1auUGPlNL+lZjqG4tw05bchtFAF+PGWQXJhJCtRSkkzTOCrLRyYyyI9mWYEjoc23cGLanlIs7WA1Nd0Jz+5RSNlf9Gtnd65yQp/W1eqY6yzURPHUUa7FrynyORmjaR9adT9utSQkXy8++IeDNzhMtFr+SqQ/gKECLe0GeuyTs6E5bImUtqpN+xopBXnEeq8wp+bvLf76d98qPE5ibTRwlsSyCE4c1Y7vrJrlc15Yc2R9ciIuKUS8rUKLSdGBFe/TD4R3cPhCKAnnRLGWnJiPPgxoTVwHVZMISdsAjNaWblBmiAOzFcu7443d3PCLyXVcfR9xgvW51HTumo91t5Qyx4HIXGoZxayZYFm2hrhSlieUqLnDL2j2gYgGU5NGoQl4OnEY2QqobpRUF4xJ4HhLzYbLrBeXmTDPvj0MasC3kKsRlm/HrsRRWZ2iPSMw9601tLvDfyjG53ddPISiVNnkdXcaAN5np7dwipdBOC1s4a0sEmKakNbkkDb8LsGBNte/g6UYs5yYaKr0bnXlDjMCznHQa7pypBjE7S55T3UeRpwo3IvZ1tfIGdb+z9RIA/PDvUksxJ3Xq3lqtZzkZJF5aeedfIOekGS/G0LiCSYsELgRceH5veknHqoGoL6xi4Q6/VjmfpZVXT19bDcTNtaR9Dlaq4LDjpQl9rl5C3O/X1hgADvJUuINCiLrD114sLY1DG/TDXE0sp+TK7utnjLAoHuAuj+6anY5vN66CSbwyUNmvo+m8li/AMkRYdtSDoPWkV7Y1ixMBPcua0Llwn2HSKKwnCjvhDIDIIVwbWwb1s6b9cztH81WF5RWUgFujewPvTElM1Sy10y7BcZohKw28uLRFVsKunc9yX2PiQoTSB4PHBHRA4U5dEQV3GHQJ93nee7VT3oeQPMVebWhuhOhi34Z33LQajzpCF3OjIbJb0tOPP6L6N/ODqkNsYViI3kgCnkNhexadOuGFWIqen2Q8iv2uOZWbPirt0YEeKZIk2dpND07L8Q3OsoQCk2rjpnw9LuFrjgu7gN9gFyPq25HJRBn7PM/lS60DF+xVkJq94PwN+CiZWC43SVcBGx65DFZIs/N78MZCUzZbFlsS7FsIrDJt878cp9eZdq/Ai4LZhL8QYHpVUrQxRxZGSqooA755N6nOxw66JkA1VPnjECCMgoNNtWox0JzhMe8PBdh2ZliXf8yQ6/eTvsG6FD84F+49pc7m0L99pfWHb9ClyO3KRHscp/MOIC1MJmqoB4dNxV20U+z8/lSTIvcmM8DiaAZj/yxlst90drlGydlyPjQzYd/XtIYcO5gHoeD1KUCZRapE5dkyk5vh97WZJn/JkR8hsslU3D6x3rNGwJbQVRu0IiA3PpeAQNZBNAJHHfv8IzIYxPhMJdYq0YqLIGSUYu87D04cDOxJY7hgawYs+ExOWb7XkbpuRoITQd8zpwVDFlSCS+wFO+qah3Vn8RBTc6cXHO5xRWfUNj+NrEtPdVmax+9EXqXtHQyFpxaauvL96RH+mGwpKHOk3aisXbZ6gLE2mF4egGjjJOIJdHyb2ZR+kj+4GIvkoBwipDgUfr4UBXY8pvFxQOxRgtI4LgOY9Z1Aco7Mwp6qi1KoMFJW8d+gJwsgM3cMsyEeYH1n/mdpJW6VDbIWzOHkP5n+OKKNm2vJTkQFFwF9eOtGy9fNBtS4qo4jvOUJnnAPsrPbGMbBYd1dMC3daHLEwvIKCAVBn7q1Z2c4zAD5eEoY0EwZj/j8x8lGQ8TswFT81ZotW7ZBDai/YtV8mkGfuaWJRI5yHc/bV7GWLF+yrMji/jicBF5jy2UoqwxseqjgTut49FRgBH3h1qwnfYbXD3FvQljyAAgBCiZV726pFRG+sZv0FjDbq0iCKILVSEUDZgmQ",
|
||||
"shared":true,
|
||||
"uploaded_signed_key_count":50
|
||||
|
||||
},
|
||||
"sessions":[
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4s+SdrKUYAMUdGcYD7QukrPklEOy7fJho9YGK/jV04QdA8JABiOfD+ngJTR4V8eZdmDuG08+Q5EL79V81hQwU2fKndP0y/9nAXPUIADYq0Zrg4EsOnXz7aE+hAeBAm0IBog1s8RYUvynZ15uwjbd/OTLP+gpqpX33DwVg2leiBkQetiUSpOpZCuQ8CcZwIA0MoGCqvaT7h76VHX9JxJx+2fCMhsJMx1nhd99WJH1W9ge5CtdbC4KUP92OSxIrPOnMrNcOPJPp/paZP+HFNQ3PDL+z8pGKXmCnrXGSbd7iPHurPYESrVkBzr",
|
||||
"sender_key":"WJ6Ce7U67a6jqkHYHd8o0+5H4bqdi9hInZdk0+swuXs",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649425011424",
|
||||
"last_use_time":"1649425011424"
|
||||
"creation_time": 1649425011424u64,
|
||||
"last_use_time": 1649425011424u64
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4t2W/lowyrV6SXVZp+uG59im0AAfNSKjhjZuiOpQlX7MS+AOJkCNvyujJ2g3KSjLZ94IkoHxkBDHLWSjwaLPu40rfOzJPDpm0XZsR6bQrsxKOmXLGEw2qw5jOTouzMVL2gvuuTix97nSYSU8j3XvTMRUoh0AF/tUpRLcvEFZeGrdUYmTMlyTv4na+FVUalUZ+jrk8t1/sM99JNq3SY1IBSjrBq/0rCOHieiippz0sw2fe2b87id4rqj1g3R9w2MWTWEdOz3ugjMGYF1YDBQZA1tJZ/hmgppk2AU2xKQXE2X3DgSC6fC66D4",
|
||||
"sender_key":"RzRROfmHNlBfzxnNCUYBfn/5oZNQ11XYjDg59hS+mV0",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649425011503",
|
||||
"last_use_time":"1649425011503"
|
||||
"creation_time": 1649425011503u64,
|
||||
"last_use_time": 1649425011503u64
|
||||
},
|
||||
{
|
||||
"pickle":"cryZlFaQv0hwWe6tTgv75RExFKGnC8tMHBXJYMHOw4titbL3SS12PYHpcBPJc6hXnOnZXqrjtjYOD545fck+3utEo8cqqwWubc9tsvxGW3tOWPttLBdAW30Vn8V1M8ebqVCNVWEAb1GKjV4ni8xG7G9SlEcCjLjnF4lJpddSZkqVMFoN0ITr9aSz/eJwXpc3HLreUFXwc8LuQp7krQ4Vt1e5EE/klduqsdurZf5V14RHsmWz2lKjt7nVgtIz/dhtF5F/sGJdg8kCGaHIMSbGAPuPPpa4/Laicb/5otrYt4pg4W4KdFpSGJIcvUQNjXaOZMx3cu/RPJIOyNhx7whG1QiYAUBqAJvr",
|
||||
"sender_key":"IXSZugAHig1v8MowE1jxi2wDDDfuZBeJynHlegJVwUc",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649425011566",
|
||||
"last_use_time":"1649425011566"
|
||||
"creation_time": 1649425011566u64,
|
||||
"last_use_time": 1649425011566u64
|
||||
},
|
||||
{
|
||||
"pickle":"SmkDiFZjNukiarQ7XHQo25FILHsuhNOnxy56cMSQU/Y71jaGbJes4YrvN4Dfy4RSONfejEDXDkbW2JudlHHRP/rWEmnfJiGbK6ArbrG2puqIZgOecPnOUgPfCisr49p1Gmf36dPaO5lm/ZSrngfSoxahoeJJE/CcJN98sYM15XytRk2LBwc+CyYDqr4V1qxfsBt6tzJ4+tsAZeRdD0UtipQgysgH56o8N7nKTCkaZz5lfpYCl3FEgwXpLJ0MGQvtQmbORFvOLqR1jZ/EbmNGKiqDDIYsqG0sf78ii1jqfpLDBXLuYDccsg",
|
||||
"sender_key":"EB9SC4jVAydKhM6/GcwMc9biKwVNywqW3TerNTrtb1M",
|
||||
"created_using_fallback_key":false,
|
||||
"creation_time":"1649542063182",
|
||||
"last_use_time":"1649542063182"
|
||||
"creation_time": 1649542063182u64,
|
||||
"last_use_time": 1649542063182u64
|
||||
}
|
||||
],
|
||||
"inbound_group_sessions":[
|
||||
@@ -1010,7 +1015,8 @@ mod tests {
|
||||
"JGgPQRuYj3ScMdPS+A0P+k/1qS9Hr3qeKXLscI+hS78"
|
||||
);
|
||||
|
||||
let room_keys = machine.runtime.block_on(machine.inner.export_room_keys(|_| true))?;
|
||||
let room_keys =
|
||||
machine.runtime.block_on(machine.inner.store().export_room_keys(|_| true))?;
|
||||
assert_eq!(room_keys.len(), 2);
|
||||
|
||||
let cross_signing_status = machine.cross_signing_status();
|
||||
|
||||
@@ -17,7 +17,7 @@ use matrix_sdk_crypto::{
|
||||
decrypt_room_key_export, encrypt_room_key_export,
|
||||
olm::ExportedRoomKey,
|
||||
store::{BackupDecryptionKey, Changes},
|
||||
LocalTrust, OlmMachine as InnerMachine, UserIdentities,
|
||||
LocalTrust, OlmMachine as InnerMachine, ToDeviceRequest, UserIdentities,
|
||||
};
|
||||
use ruma::{
|
||||
api::{
|
||||
@@ -40,6 +40,7 @@ use ruma::{
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -800,6 +801,53 @@ impl OlmMachine {
|
||||
Ok(serde_json::to_string(&encrypted_content)?)
|
||||
}
|
||||
|
||||
/// Encrypt the given event with the given type and content for the given
|
||||
/// device. This method is used to send an event to a specific device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user who owns the target device.
|
||||
/// * `device_id` - The ID of the device to which the message will be sent.
|
||||
/// * `event_type` - The event type.
|
||||
/// * `content` - The serialized content of the event.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result` containing the request to be sent out if the encryption was
|
||||
/// successful. If the device is not found, the result will be `Ok(None)`.
|
||||
///
|
||||
/// The caller should ensure that there is an olm session (see
|
||||
/// `get_missing_sessions`) with the target device before calling this
|
||||
/// method.
|
||||
pub fn create_encrypted_to_device_request(
|
||||
&self,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
event_type: String,
|
||||
content: String,
|
||||
) -> Result<Option<Request>, CryptoStoreError> {
|
||||
let user_id = parse_user_id(&user_id)?;
|
||||
let device_id = device_id.as_str().into();
|
||||
let content = serde_json::from_str(&content)?;
|
||||
|
||||
let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id, None))?;
|
||||
|
||||
if let Some(device) = device {
|
||||
let encrypted_content =
|
||||
self.runtime.block_on(device.encrypt_event_raw(&event_type, &content))?;
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
user_id.as_ref(),
|
||||
DeviceIdOrAllDevices::DeviceId(device_id.to_owned()),
|
||||
"m.room.encrypted",
|
||||
encrypted_content.cast(),
|
||||
);
|
||||
|
||||
Ok(Some(request.into()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt the given event that was sent in the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -917,7 +965,7 @@ impl OlmMachine {
|
||||
passphrase: String,
|
||||
rounds: i32,
|
||||
) -> Result<String, CryptoStoreError> {
|
||||
let keys = self.runtime.block_on(self.inner.export_room_keys(|_| true))?;
|
||||
let keys = self.runtime.block_on(self.inner.store().export_room_keys(|_| true))?;
|
||||
|
||||
let encrypted = encrypt_room_key_export(&keys, &passphrase, rounds as u32)
|
||||
.map_err(CryptoStoreError::Serialization)?;
|
||||
@@ -976,7 +1024,7 @@ impl OlmMachine {
|
||||
pub fn discard_room_key(&self, room_id: String) -> Result<(), CryptoStoreError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?;
|
||||
self.runtime.block_on(self.inner.discard_room_key(&room_id))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace matrix_sdk_crypto_ffi {};
|
||||
|
||||
enum LocalTrust {
|
||||
"Verified",
|
||||
"BlackListed",
|
||||
"Ignored",
|
||||
"Unset",
|
||||
};
|
||||
|
||||
enum SignatureState {
|
||||
"Missing",
|
||||
"Invalid",
|
||||
"ValidButNotTrusted",
|
||||
"ValidAndTrusted",
|
||||
};
|
||||
@@ -164,7 +164,6 @@ impl From<OutgoingRequest> for Request {
|
||||
},
|
||||
RoomMessage(r) => Request::from(r),
|
||||
KeysClaim(c) => (r.request_id().to_owned(), c.clone()).into(),
|
||||
KeysBackup(b) => (r.request_id().to_owned(), b.clone()).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi", "experimental-room-list-with-unified-invites"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
opentelemetry = "0.21.0"
|
||||
opentelemetry_sdk = { version = "0.21.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.14.0", features = ["tokio", "reqwest-client", "http-proto"] }
|
||||
opentelemetry = "0.22.0"
|
||||
opentelemetry_sdk = { version = "0.22.0", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.15.0", features = ["tokio", "reqwest-client", "http-proto"] }
|
||||
ruma = { workspace = true, features = ["html", "unstable-unspecified", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat"] }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde = { workspace = true }
|
||||
@@ -42,7 +42,7 @@ serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-core = { workspace = true }
|
||||
tracing-opentelemetry = "0.22.0"
|
||||
tracing-opentelemetry = "0.23.0"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
@@ -69,6 +69,7 @@ features = [
|
||||
"rustls-tls", # note: differ from block below
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies.matrix-sdk]
|
||||
@@ -83,4 +84,8 @@ features = [
|
||||
"native-tls", # note: differ from block above
|
||||
"socks",
|
||||
"sqlite",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
oidc::{
|
||||
registrations::{ClientId, OidcRegistrations, OidcRegistrationsError},
|
||||
types::{
|
||||
client_credentials::ClientCredentials,
|
||||
errors::ClientErrorCode::AccessDenied,
|
||||
@@ -15,30 +16,39 @@ use matrix_sdk::{
|
||||
},
|
||||
AuthorizationResponse, Oidc, OidcError,
|
||||
},
|
||||
AuthSession,
|
||||
AuthSession, ClientBuildError as MatrixClientBuildError, HttpError, RumaApiError,
|
||||
};
|
||||
use matrix_sdk_ui::authentication::oidc::{ClientId, OidcRegistrations, OidcRegistrationsError};
|
||||
use ruma::{
|
||||
api::client::discovery::discover_homeserver::AuthenticationServerInfo, IdParseError,
|
||||
api::{
|
||||
client::discovery::discover_homeserver::AuthenticationServerInfo,
|
||||
error::{DeserializationError, FromHttpResponseError},
|
||||
},
|
||||
OwnedUserId,
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
use url::Url;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::{client::Client, client_builder::ClientBuilder, RUNTIME};
|
||||
use crate::{client::ClientSessionDelegate, client_builder::UrlScheme, error::ClientError};
|
||||
use super::{client::Client, client_builder::ClientBuilder};
|
||||
use crate::{
|
||||
client::ClientSessionDelegate,
|
||||
client_builder::{CertificateBytes, ClientBuildError},
|
||||
error::ClientError,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct AuthenticationService {
|
||||
base_path: String,
|
||||
passphrase: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
client: RwLock<Option<Arc<Client>>>,
|
||||
homeserver_details: RwLock<Option<Arc<HomeserverLoginDetails>>>,
|
||||
client: AsyncRwLock<Option<Client>>,
|
||||
homeserver_details: StdRwLock<Option<Arc<HomeserverLoginDetails>>>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: RwLock<Option<String>>,
|
||||
custom_sliding_sync_proxy: StdRwLock<Option<String>>,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<CertificateBytes>,
|
||||
proxy: Option<String>,
|
||||
}
|
||||
|
||||
impl Drop for AuthenticationService {
|
||||
@@ -52,14 +62,23 @@ impl Drop for AuthenticationService {
|
||||
pub enum AuthenticationError {
|
||||
#[error("A successful call to configure_homeserver must be made first.")]
|
||||
ClientMissing,
|
||||
#[error("{message}")]
|
||||
InvalidServerName { message: String },
|
||||
|
||||
#[error("The supplied server name is invalid.")]
|
||||
InvalidServerName,
|
||||
#[error(transparent)]
|
||||
ServerUnreachable(HttpError),
|
||||
#[error(transparent)]
|
||||
WellKnownLookupFailed(RumaApiError),
|
||||
#[error(transparent)]
|
||||
WellKnownDeserializationError(DeserializationError),
|
||||
#[error("The homeserver doesn't provide a trusted sliding sync proxy in its well-known configuration.")]
|
||||
SlidingSyncNotAvailable,
|
||||
|
||||
#[error("Login was successful but is missing a valid Session to configure the file store.")]
|
||||
SessionMissing,
|
||||
#[error("Failed to use the supplied base path.")]
|
||||
InvalidBasePath,
|
||||
|
||||
#[error(
|
||||
"The homeserver doesn't provide an authentication issuer in its well-known configuration."
|
||||
)]
|
||||
@@ -74,6 +93,7 @@ pub enum AuthenticationError {
|
||||
OidcCancelled,
|
||||
#[error("An error occurred with OIDC: {message}")]
|
||||
OidcError { message: String },
|
||||
|
||||
#[error("An error occurred: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
@@ -84,9 +104,27 @@ impl From<anyhow::Error> for AuthenticationError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdParseError> for AuthenticationError {
|
||||
fn from(e: IdParseError) -> AuthenticationError {
|
||||
AuthenticationError::InvalidServerName { message: e.to_string() }
|
||||
impl From<ClientBuildError> for AuthenticationError {
|
||||
fn from(e: ClientBuildError) -> AuthenticationError {
|
||||
match e {
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::InvalidServerName) => {
|
||||
AuthenticationError::InvalidServerName
|
||||
}
|
||||
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::Http(e)) => {
|
||||
AuthenticationError::ServerUnreachable(e)
|
||||
}
|
||||
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
|
||||
FromHttpResponseError::Server(e),
|
||||
)) => AuthenticationError::WellKnownLookupFailed(e),
|
||||
|
||||
ClientBuildError::Sdk(MatrixClientBuildError::AutoDiscovery(
|
||||
FromHttpResponseError::Deserialization(e),
|
||||
)) => AuthenticationError::WellKnownDeserializationError(e),
|
||||
|
||||
_ => AuthenticationError::Generic { message: e.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +188,7 @@ impl OidcAuthenticationData {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverLoginDetails {
|
||||
url: String,
|
||||
sliding_sync_proxy: Option<String>,
|
||||
supports_oidc_login: bool,
|
||||
supports_password_login: bool,
|
||||
}
|
||||
@@ -161,6 +200,12 @@ impl HomeserverLoginDetails {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
/// The URL of the discovered or manually set sliding sync proxy,
|
||||
/// if any.
|
||||
pub fn sliding_sync_proxy(&self) -> Option<String> {
|
||||
self.sliding_sync_proxy.clone()
|
||||
}
|
||||
|
||||
/// Whether the current homeserver supports login using OIDC.
|
||||
pub fn supports_oidc_login(&self) -> bool {
|
||||
self.supports_oidc_login
|
||||
@@ -172,14 +217,20 @@ impl HomeserverLoginDetails {
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl AuthenticationService {
|
||||
/// Creates a new service to authenticate a user with.
|
||||
#[uniffi::constructor]
|
||||
// TODO: This has too many arguments, even clippy agrees. Many of these methods are the same as
|
||||
// for the `ClientBuilder`. We should let people pass in a `ClientBuilder` and possibly convert
|
||||
// this to a builder pattern as well.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
base_path: String,
|
||||
passphrase: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
proxy: Option<String>,
|
||||
oidc_configuration: Option<OidcConfiguration>,
|
||||
custom_sliding_sync_proxy: Option<String>,
|
||||
session_delegate: Option<Box<dyn ClientSessionDelegate>>,
|
||||
@@ -189,12 +240,14 @@ impl AuthenticationService {
|
||||
base_path,
|
||||
passphrase,
|
||||
user_agent,
|
||||
client: RwLock::new(None),
|
||||
homeserver_details: RwLock::new(None),
|
||||
client: AsyncRwLock::new(None),
|
||||
homeserver_details: StdRwLock::new(None),
|
||||
oidc_configuration,
|
||||
custom_sliding_sync_proxy: RwLock::new(custom_sliding_sync_proxy),
|
||||
custom_sliding_sync_proxy: StdRwLock::new(custom_sliding_sync_proxy),
|
||||
session_delegate: session_delegate.map(Into::into),
|
||||
cross_process_refresh_lock_id,
|
||||
additional_root_certificates,
|
||||
proxy,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,93 +257,65 @@ impl AuthenticationService {
|
||||
|
||||
/// Updates the service to authenticate with the homeserver for the
|
||||
/// specified address.
|
||||
pub fn configure_homeserver(
|
||||
pub async fn configure_homeserver(
|
||||
&self,
|
||||
server_name_or_homeserver_url: String,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
let mut builder = self.new_client_builder();
|
||||
builder = builder.server_name_or_homeserver_url(server_name_or_homeserver_url);
|
||||
|
||||
// Attempt discovery as a server name first.
|
||||
let result = matrix_sdk::sanitize_server_name(&server_name_or_homeserver_url);
|
||||
let client = builder.build_inner().await?;
|
||||
let details = self.details_from_client(&client).await?;
|
||||
|
||||
match result {
|
||||
Ok(server_name) => {
|
||||
let protocol = if server_name_or_homeserver_url.starts_with("http://") {
|
||||
UrlScheme::Http
|
||||
} else {
|
||||
UrlScheme::Https
|
||||
};
|
||||
builder = builder.server_name_with_protocol(server_name.to_string(), protocol);
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
// When the input isn't a valid server name check it is a URL.
|
||||
// If this is the case, build the client with a homeserver URL.
|
||||
if Url::parse(&server_name_or_homeserver_url).is_ok() {
|
||||
builder = builder.homeserver_url(server_name_or_homeserver_url.clone());
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client = builder.build_inner().or_else(|e| {
|
||||
if !server_name_or_homeserver_url.starts_with("http://")
|
||||
&& !server_name_or_homeserver_url.starts_with("https://")
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
// When discovery fails, fallback to the homeserver URL if supplied.
|
||||
let mut builder = self.new_client_builder();
|
||||
builder = builder.homeserver_url(server_name_or_homeserver_url);
|
||||
builder.build_inner()
|
||||
})?;
|
||||
|
||||
let details = RUNTIME.block_on(self.details_from_client(&client))?;
|
||||
|
||||
// Now we've verified that it's a valid homeserver, make sure
|
||||
// there's a sliding sync proxy available one way or another.
|
||||
// Make sure there's a sliding sync proxy available.
|
||||
if self.custom_sliding_sync_proxy.read().unwrap().is_none()
|
||||
&& client.discovered_sliding_sync_proxy().is_none()
|
||||
&& details.sliding_sync_proxy().is_none()
|
||||
{
|
||||
return Err(AuthenticationError::SlidingSyncNotAvailable);
|
||||
}
|
||||
|
||||
*self.client.write().unwrap() = Some(client);
|
||||
*self.client.write().await = Some(client);
|
||||
*self.homeserver_details.write().unwrap() = Some(Arc::new(details));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs a password login using the current homeserver.
|
||||
pub fn login(
|
||||
pub async fn login(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
initial_device_name: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
let client_guard = self.client.read().await;
|
||||
let Some(client) = client_guard.as_ref() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
// Login and ask the server for the full user ID as this could be different from
|
||||
// the username that was entered.
|
||||
client.login(username, password, initial_device_name, device_id).map_err(|e| match e {
|
||||
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
|
||||
})?;
|
||||
let whoami = client.whoami()?;
|
||||
client.login(username, password, initial_device_name, device_id).await.map_err(
|
||||
|e| match e {
|
||||
ClientError::Generic { msg } => AuthenticationError::Generic { message: msg },
|
||||
},
|
||||
)?;
|
||||
let whoami = client.whoami().await?;
|
||||
let session =
|
||||
client.inner.matrix_auth().session().ok_or(AuthenticationError::SessionMissing)?;
|
||||
|
||||
self.finalize_client(client, session, whoami.user_id)
|
||||
drop(client_guard);
|
||||
self.finalize_client(session, whoami.user_id).await
|
||||
}
|
||||
|
||||
/// Requests the URL needed for login in a web view using OIDC. Once the web
|
||||
/// view has succeeded, call `login_with_oidc_callback` with the callback it
|
||||
/// returns.
|
||||
pub fn url_for_oidc_login(&self) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
pub async fn url_for_oidc_login(
|
||||
&self,
|
||||
) -> Result<Arc<OidcAuthenticationData>, AuthenticationError> {
|
||||
let client_guard = self.client.read().await;
|
||||
let Some(client) = client_guard.as_ref() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
@@ -307,25 +332,24 @@ impl AuthenticationService {
|
||||
|
||||
let oidc = client.inner.oidc();
|
||||
|
||||
RUNTIME.block_on(async {
|
||||
self.configure_oidc(&oidc, authentication_server, oidc_configuration).await?;
|
||||
self.configure_oidc(&oidc, authentication_server, oidc_configuration).await?;
|
||||
|
||||
let mut data_builder = oidc.login(redirect_url, None)?;
|
||||
// TODO: Add a check for the Consent prompt when MAS is updated.
|
||||
data_builder = data_builder.prompt(vec![Prompt::Consent]);
|
||||
let data = data_builder.build().await?;
|
||||
let mut data_builder = oidc.login(redirect_url, None)?;
|
||||
// TODO: Add a check for the Consent prompt when MAS is updated.
|
||||
data_builder = data_builder.prompt(vec![Prompt::Consent]);
|
||||
let data = data_builder.build().await?;
|
||||
|
||||
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
|
||||
})
|
||||
Ok(Arc::new(OidcAuthenticationData { url: data.url, state: data.state }))
|
||||
}
|
||||
|
||||
/// Completes the OIDC login process.
|
||||
pub fn login_with_oidc_callback(
|
||||
pub async fn login_with_oidc_callback(
|
||||
&self,
|
||||
authentication_data: Arc<OidcAuthenticationData>,
|
||||
callback_url: String,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
let Some(client) = self.client.read().unwrap().clone() else {
|
||||
let client_guard = self.client.read().await;
|
||||
let Some(client) = client_guard.as_ref() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
@@ -354,18 +378,18 @@ impl AuthenticationService {
|
||||
return Err(AuthenticationError::OidcCallbackUrlInvalid);
|
||||
};
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
oidc.finish_authorization(code).await?;
|
||||
oidc.finish_authorization(code).await?;
|
||||
|
||||
oidc.finish_login()
|
||||
.await
|
||||
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })
|
||||
})?;
|
||||
oidc.finish_login()
|
||||
.await
|
||||
.map_err(|e| AuthenticationError::OidcError { message: e.to_string() })?;
|
||||
|
||||
let user_id = client.inner.user_id().unwrap().to_owned();
|
||||
let session =
|
||||
client.inner.oidc().full_session().ok_or(AuthenticationError::SessionMissing)?;
|
||||
self.finalize_client(client, session, user_id)
|
||||
|
||||
drop(client_guard);
|
||||
self.finalize_client(session, user_id).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,19 +403,31 @@ impl AuthenticationService {
|
||||
builder = builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
if let Some(proxy) = &self.proxy {
|
||||
builder = builder.proxy(proxy.to_owned())
|
||||
}
|
||||
|
||||
builder = builder.add_root_certificates(self.additional_root_certificates.clone());
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
/// Get the homeserver login details from a client.
|
||||
async fn details_from_client(
|
||||
&self,
|
||||
client: &Arc<Client>,
|
||||
client: &Client,
|
||||
) -> Result<HomeserverLoginDetails, AuthenticationError> {
|
||||
let supports_oidc_login = client.discovered_authentication_server().is_some();
|
||||
let supports_password_login = client.supports_password_login().await.ok().unwrap_or(false);
|
||||
let sliding_sync_proxy = client.sliding_sync_proxy().map(|proxy_url| proxy_url.to_string());
|
||||
let url = client.homeserver();
|
||||
|
||||
Ok(HomeserverLoginDetails { url, supports_oidc_login, supports_password_login })
|
||||
Ok(HomeserverLoginDetails {
|
||||
url,
|
||||
sliding_sync_proxy,
|
||||
supports_oidc_login,
|
||||
supports_password_login,
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle any necessary configuration in order for login via OIDC to
|
||||
@@ -550,30 +586,53 @@ impl AuthenticationService {
|
||||
}
|
||||
|
||||
/// Creates a new client to setup the store path now the user ID is known.
|
||||
fn finalize_client(
|
||||
async fn finalize_client(
|
||||
&self,
|
||||
client: Arc<Client>,
|
||||
session: impl Into<AuthSession>,
|
||||
user_id: OwnedUserId,
|
||||
) -> Result<Arc<Client>, AuthenticationError> {
|
||||
// Take ownership of the client. This means that further attempts to
|
||||
// `finalize_client` may fail, but we want to make sure that there
|
||||
// aren't two clients at any point later.
|
||||
let Some(client) = self.client.write().await.take() else {
|
||||
return Err(AuthenticationError::ClientMissing);
|
||||
};
|
||||
|
||||
let homeserver_url = client.homeserver();
|
||||
|
||||
let sliding_sync_proxy: Option<String>;
|
||||
if let Some(custom_proxy) = self.custom_sliding_sync_proxy.read().unwrap().clone() {
|
||||
sliding_sync_proxy = Some(custom_proxy);
|
||||
} else if let Some(discovered_proxy) = client.discovered_sliding_sync_proxy() {
|
||||
sliding_sync_proxy = Some(discovered_proxy.to_string());
|
||||
} else {
|
||||
sliding_sync_proxy = None;
|
||||
}
|
||||
let sliding_sync_proxy = self
|
||||
.custom_sliding_sync_proxy
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.or_else(|| client.sliding_sync_proxy().map(|url| url.to_string()));
|
||||
|
||||
// Wait for the parent client to finish running its initialization tasks.
|
||||
client.inner.encryption().wait_for_e2ee_initialization_tasks().await;
|
||||
|
||||
// Drop the parent client. Both clients shouldn't be alive at the same time, or
|
||||
// it may cause issues (when trying to initialize encryption-related tasks at
|
||||
// the same time).
|
||||
drop(client);
|
||||
|
||||
// Construct the final client.
|
||||
let mut client = self
|
||||
.new_client_builder()
|
||||
.passphrase(self.passphrase.clone())
|
||||
.homeserver_url(homeserver_url)
|
||||
.sliding_sync_proxy(sliding_sync_proxy)
|
||||
.with_encryption_settings(matrix_sdk::encryption::EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
backup_download_strategy:
|
||||
matrix_sdk::encryption::BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
auto_enable_backups: true,
|
||||
})
|
||||
.username(user_id.to_string());
|
||||
|
||||
if let Some(proxy) = &self.proxy {
|
||||
client = client.proxy(proxy.to_owned())
|
||||
}
|
||||
|
||||
if let Some(id) = &self.cross_process_refresh_lock_id {
|
||||
let Some(ref session_delegate) = self.session_delegate else {
|
||||
return Err(AuthenticationError::OidcError {
|
||||
@@ -586,12 +645,12 @@ impl AuthenticationService {
|
||||
client = client.set_session_delegate_inner(session_delegate.clone());
|
||||
}
|
||||
|
||||
let client = client.build_inner()?;
|
||||
let client = client.build_inner().await?;
|
||||
|
||||
// Restore the client using the session from the login request.
|
||||
client.restore_session_inner(session)?;
|
||||
|
||||
Ok(client)
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ use matrix_sdk::{
|
||||
AnyInitialStateEvent, AnyToDeviceEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventEncryptionAlgorithm, TransactionId, UInt, UserId,
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
|
||||
};
|
||||
@@ -40,7 +40,10 @@ use matrix_sdk_ui::notification_client::NotificationProcessSetup as MatrixNotifi
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::discovery::discover_homeserver::AuthenticationServerInfo,
|
||||
events::room::power_levels::RoomPowerLevelsEventContent,
|
||||
events::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
room::power_levels::RoomPowerLevelsEventContent, GlobalAccountDataEventType,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -55,6 +58,7 @@ use crate::{
|
||||
encryption::Encryption,
|
||||
notification::NotificationClientBuilder,
|
||||
notification_settings::NotificationSettings,
|
||||
room_directory_search::RoomDirectorySearch,
|
||||
sync_service::{SyncService, SyncServiceBuilder},
|
||||
task_handle::TaskHandle,
|
||||
ClientError,
|
||||
@@ -179,7 +183,7 @@ impl Client {
|
||||
sdk_client: MatrixClient,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
) -> Result<Arc<Self>, ClientError> {
|
||||
) -> Result<Self, ClientError> {
|
||||
let session_verification_controller: Arc<
|
||||
tokio::sync::RwLock<Option<SessionVerificationController>>,
|
||||
> = Default::default();
|
||||
@@ -193,11 +197,11 @@ impl Client {
|
||||
}
|
||||
});
|
||||
|
||||
let client = Arc::new(Client {
|
||||
let client = Client {
|
||||
inner: ManuallyDrop::new(sdk_client),
|
||||
delegate: RwLock::new(None),
|
||||
session_verification_controller,
|
||||
});
|
||||
};
|
||||
|
||||
if let Some(process_id) = cross_process_refresh_lock_id {
|
||||
if session_delegate.is_none() {
|
||||
@@ -239,24 +243,22 @@ impl Client {
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Client {
|
||||
/// Login using a username and password.
|
||||
pub fn login(
|
||||
pub async fn login(
|
||||
&self,
|
||||
username: String,
|
||||
password: String,
|
||||
initial_device_name: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
}
|
||||
if let Some(device_id) = device_id.as_ref() {
|
||||
builder = builder.device_id(device_id);
|
||||
}
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
})
|
||||
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
}
|
||||
if let Some(device_id) = device_id.as_ref() {
|
||||
builder = builder.device_id(device_id);
|
||||
}
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_media_file(
|
||||
@@ -325,9 +327,10 @@ impl Client {
|
||||
self.inner.oidc().authentication_server_info().cloned()
|
||||
}
|
||||
|
||||
/// The sliding sync proxy that is trusted by the homeserver. `None` when
|
||||
/// not configured.
|
||||
pub fn discovered_sliding_sync_proxy(&self) -> Option<Url> {
|
||||
/// The sliding sync proxy of the homeserver. It is either set automatically
|
||||
/// during discovery or manually via `set_sliding_sync_proxy` or `None`
|
||||
/// when not configured.
|
||||
pub fn sliding_sync_proxy(&self) -> Option<Url> {
|
||||
self.inner.sliding_sync_proxy()
|
||||
}
|
||||
|
||||
@@ -342,9 +345,8 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Gets information about the owner of a given access token.
|
||||
pub(crate) fn whoami(&self) -> anyhow::Result<whoami::v3::Response> {
|
||||
RUNTIME
|
||||
.block_on(async move { self.inner.whoami().await.map_err(|e| anyhow!(e.to_string())) })
|
||||
pub(crate) async fn whoami(&self) -> anyhow::Result<whoami::v3::Response> {
|
||||
Ok(self.inner.whoami().await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,7 +606,7 @@ impl Client {
|
||||
}
|
||||
|
||||
/// Registers a pusher with given parameters
|
||||
pub fn set_pusher(
|
||||
pub async fn set_pusher(
|
||||
&self,
|
||||
identifiers: PusherIdentifiers,
|
||||
kind: PusherKind,
|
||||
@@ -613,20 +615,24 @@ impl Client {
|
||||
profile_tag: Option<String>,
|
||||
lang: String,
|
||||
) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let ids = identifiers.into();
|
||||
let ids = identifiers.into();
|
||||
|
||||
let pusher_init = PusherInit {
|
||||
ids,
|
||||
kind: kind.try_into()?,
|
||||
app_display_name,
|
||||
device_display_name,
|
||||
profile_tag,
|
||||
lang,
|
||||
};
|
||||
self.inner.set_pusher(pusher_init.into()).await?;
|
||||
Ok(())
|
||||
})
|
||||
let pusher_init = PusherInit {
|
||||
ids,
|
||||
kind: kind.try_into()?,
|
||||
app_display_name,
|
||||
device_display_name,
|
||||
profile_tag,
|
||||
lang,
|
||||
};
|
||||
self.inner.pusher().set(pusher_init.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a pusher of given pusher ids
|
||||
pub async fn delete_pusher(&self, identifiers: PusherIdentifiers) -> Result<(), ClientError> {
|
||||
self.inner.pusher().delete(identifiers.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The homeserver this client is configured to use.
|
||||
@@ -645,22 +651,6 @@ impl Client {
|
||||
Ok(dm)
|
||||
}
|
||||
|
||||
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().unignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search_users(
|
||||
&self,
|
||||
search_term: String,
|
||||
@@ -675,7 +665,7 @@ impl Client {
|
||||
pub fn get_profile(&self, user_id: String) -> Result<UserProfile, ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let owned_user_id = UserId::parse(user_id.clone())?;
|
||||
let response = self.inner.get_profile(&owned_user_id).await?;
|
||||
let response = self.inner.account().fetch_user_profile_of(&owned_user_id).await?;
|
||||
|
||||
let user_profile = UserProfile {
|
||||
user_id,
|
||||
@@ -710,6 +700,75 @@ impl Client {
|
||||
pub fn encryption(&self) -> Arc<Encryption> {
|
||||
Arc::new(self.inner.encryption().into())
|
||||
}
|
||||
|
||||
// Ignored users
|
||||
|
||||
pub async fn ignored_users(&self) -> Result<Vec<String>, ClientError> {
|
||||
if let Some(raw_content) = self
|
||||
.inner
|
||||
.account()
|
||||
.fetch_account_data(GlobalAccountDataEventType::IgnoredUserList)
|
||||
.await?
|
||||
{
|
||||
let content = raw_content.deserialize_as::<IgnoredUserListEventContent>()?;
|
||||
let user_ids: Vec<String> =
|
||||
content.ignored_users.keys().map(|id| id.to_string()).collect();
|
||||
|
||||
return Ok(user_ids);
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.account().unignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn subscribe_to_ignored_users(
|
||||
&self,
|
||||
listener: Box<dyn IgnoredUsersListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.subscribe_to_ignore_user_list_changes();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(user_ids) = subscriber.next().await {
|
||||
listener.call(user_ids);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn room_directory_search(&self) -> Arc<RoomDirectorySearch> {
|
||||
Arc::new(RoomDirectorySearch::new(
|
||||
matrix_sdk::room_directory_search::RoomDirectorySearch::new((*self.inner).clone()),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn join_room_by_id(&self, room_id: String) -> Result<Arc<Room>, ClientError> {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
let room = self.inner.join_room_by_id(room_id.as_ref()).await?;
|
||||
Ok(Arc::new(Room::new(room)))
|
||||
}
|
||||
|
||||
pub async fn get_recently_visited_rooms(&self) -> Result<Vec<String>, ClientError> {
|
||||
Ok(self.inner.account().get_recently_visited_rooms().await?)
|
||||
}
|
||||
|
||||
pub async fn track_recently_visited_room(&self, room: String) -> Result<(), ClientError> {
|
||||
self.inner.account().track_recently_visited_room(room).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait IgnoredUsersListener: Sync + Send {
|
||||
fn call(&self, ignored_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -766,6 +825,7 @@ impl From<&search_users::v3::User> for UserProfile {
|
||||
impl Client {
|
||||
fn process_session_change(&self, session_change: SessionChange) {
|
||||
if let Some(delegate) = self.delegate.read().unwrap().clone() {
|
||||
debug!("Applying session change: {session_change:?}");
|
||||
RUNTIME.spawn_blocking(move || match session_change {
|
||||
SessionChange::UnknownToken { soft_logout } => {
|
||||
delegate.did_receive_auth_error(soft_logout);
|
||||
@@ -774,6 +834,10 @@ impl Client {
|
||||
delegate.did_refresh_tokens();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug!(
|
||||
"No client delegate found, session change couldn't be applied: {session_change:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,31 +2,69 @@ use std::{fs, path::PathBuf, sync::Arc};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
reqwest::Certificate,
|
||||
ruma::{
|
||||
api::{error::UnknownVersionError, MatrixVersion},
|
||||
ServerName, UserId,
|
||||
},
|
||||
Client as MatrixClient, ClientBuilder as MatrixClientBuilder,
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError,
|
||||
ClientBuilder as MatrixClientBuilder, IdParseError,
|
||||
};
|
||||
use sanitize_filename_reader_friendly::sanitize;
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::{client::Client, RUNTIME};
|
||||
use super::client::Client;
|
||||
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum UrlScheme {
|
||||
Http,
|
||||
Https,
|
||||
/// A list of bytes containing a certificate in DER or PEM form.
|
||||
pub type CertificateBytes = Vec<u8>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum HomeserverConfig {
|
||||
Url(String),
|
||||
ServerName(String),
|
||||
ServerNameOrUrl(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum ClientBuildError {
|
||||
#[error(transparent)]
|
||||
Sdk(#[from] MatrixClientBuildError),
|
||||
#[error("Failed to build the client: {message}")]
|
||||
Generic { message: String },
|
||||
}
|
||||
|
||||
impl From<IdParseError> for ClientBuildError {
|
||||
fn from(e: IdParseError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ClientBuildError {
|
||||
fn from(e: std::io::Error) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for ClientBuildError {
|
||||
fn from(e: url::ParseError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientError> for ClientBuildError {
|
||||
fn from(e: ClientError) -> ClientBuildError {
|
||||
ClientBuildError::Generic { message: format!("{e:#}") }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct ClientBuilder {
|
||||
base_path: Option<String>,
|
||||
username: Option<String>,
|
||||
server_name: Option<(String, UrlScheme)>,
|
||||
homeserver_url: Option<String>,
|
||||
homeserver_cfg: Option<HomeserverConfig>,
|
||||
server_versions: Option<Vec<String>>,
|
||||
passphrase: Zeroizing<Option<String>>,
|
||||
user_agent: Option<String>,
|
||||
@@ -37,13 +75,33 @@ pub struct ClientBuilder {
|
||||
inner: MatrixClientBuilder,
|
||||
cross_process_refresh_lock_id: Option<String>,
|
||||
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
|
||||
additional_root_certificates: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl ClientBuilder {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self::default())
|
||||
Arc::new(Self {
|
||||
base_path: None,
|
||||
username: None,
|
||||
homeserver_cfg: None,
|
||||
server_versions: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
sliding_sync_proxy: None,
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
inner: MatrixClient::builder().with_encryption_settings(EncryptionSettings {
|
||||
auto_enable_cross_signing: false,
|
||||
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
auto_enable_backups: false,
|
||||
}),
|
||||
cross_process_refresh_lock_id: None,
|
||||
session_delegate: None,
|
||||
additional_root_certificates: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable_cross_process_refresh_lock(
|
||||
@@ -83,14 +141,19 @@ impl ClientBuilder {
|
||||
|
||||
pub fn server_name(self: Arc<Self>, server_name: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
// Assume HTTPS if no protocol is provided.
|
||||
builder.server_name = Some((server_name, UrlScheme::Https));
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::ServerName(server_name));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn homeserver_url(self: Arc<Self>, url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_url = Some(url);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::Url(url));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn server_name_or_homeserver_url(self: Arc<Self>, server_name_or_url: String) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.homeserver_cfg = Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
@@ -130,12 +193,31 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientError> {
|
||||
Ok(self.build_inner()?)
|
||||
pub fn add_root_certificates(
|
||||
self: Arc<Self>,
|
||||
certificates: Vec<CertificateBytes>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.additional_root_certificates = certificates;
|
||||
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
Ok(Arc::new(self.build_inner().await?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
pub(crate) fn with_encryption_settings(
|
||||
self: Arc<Self>,
|
||||
settings: EncryptionSettings,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.inner = builder.inner.with_encryption_settings(settings);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub(crate) fn enable_cross_process_refresh_lock_inner(
|
||||
self: Arc<Self>,
|
||||
process_id: String,
|
||||
@@ -156,17 +238,7 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub(crate) fn server_name_with_protocol(
|
||||
self: Arc<Self>,
|
||||
server_name: String,
|
||||
protocol: UrlScheme,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
builder.server_name = Some((server_name, protocol));
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub(crate) fn build_inner(self: Arc<Self>) -> anyhow::Result<Arc<Client>> {
|
||||
pub(crate) async fn build_inner(self: Arc<Self>) -> Result<Client, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = builder.inner;
|
||||
|
||||
@@ -179,23 +251,45 @@ impl ClientBuilder {
|
||||
}
|
||||
|
||||
// Determine server either from URL, server name or user ID.
|
||||
if let Some(homeserver_url) = builder.homeserver_url {
|
||||
inner_builder = inner_builder.homeserver_url(homeserver_url);
|
||||
} else if let Some((server_name, protocol)) = builder.server_name {
|
||||
let server_name = ServerName::parse(server_name)?;
|
||||
inner_builder = match protocol {
|
||||
UrlScheme::Http => inner_builder.insecure_server_name_no_tls(&server_name),
|
||||
UrlScheme::Https => inner_builder.server_name(&server_name),
|
||||
};
|
||||
} else if let Some(username) = builder.username {
|
||||
let user = UserId::parse(username)?;
|
||||
inner_builder = inner_builder.server_name(user.server_name());
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Failed to build: One of homeserver_url, server_name or username must be called."
|
||||
);
|
||||
inner_builder = match builder.homeserver_cfg {
|
||||
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
|
||||
Some(HomeserverConfig::ServerName(server_name)) => {
|
||||
let server_name = ServerName::parse(server_name)?;
|
||||
inner_builder.server_name(&server_name)
|
||||
}
|
||||
Some(HomeserverConfig::ServerNameOrUrl(server_name_or_url)) => {
|
||||
inner_builder.server_name_or_homeserver_url(server_name_or_url)
|
||||
}
|
||||
None => {
|
||||
if let Some(username) = builder.username {
|
||||
let user = UserId::parse(username)?;
|
||||
inner_builder.server_name(user.server_name())
|
||||
} else {
|
||||
return Err(ClientBuildError::Generic {
|
||||
message: "Failed to build: One of homeserver_url, server_name, server_name_or_homeserver_url or username must be called.".to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut certificates = Vec::new();
|
||||
|
||||
for certificate in builder.additional_root_certificates {
|
||||
// We don't really know what type of certificate we may get here, so let's try
|
||||
// first one type, then the other.
|
||||
if let Ok(cert) = Certificate::from_der(&certificate) {
|
||||
certificates.push(cert);
|
||||
} else {
|
||||
let cert =
|
||||
Certificate::from_pem(&certificate).map_err(|e| ClientBuildError::Generic {
|
||||
message: format!("Failed to add a root certificate {e:?}"),
|
||||
})?;
|
||||
certificates.push(cert);
|
||||
}
|
||||
}
|
||||
|
||||
inner_builder = inner_builder.add_root_certificates(certificates);
|
||||
|
||||
if let Some(proxy) = builder.proxy {
|
||||
inner_builder = inner_builder.proxy(proxy);
|
||||
}
|
||||
@@ -217,11 +311,12 @@ impl ClientBuilder {
|
||||
server_versions
|
||||
.iter()
|
||||
.map(|s| MatrixVersion::try_from(s.as_str()))
|
||||
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()?,
|
||||
.collect::<Result<Vec<MatrixVersion>, UnknownVersionError>>()
|
||||
.map_err(|e| ClientBuildError::Generic { message: e.to_string() })?,
|
||||
);
|
||||
}
|
||||
|
||||
let sdk_client = RUNTIME.block_on(async move { inner_builder.build().await })?;
|
||||
let sdk_client = inner_builder.build().await?;
|
||||
|
||||
// At this point, `sdk_client` might contain a `sliding_sync_proxy` that has
|
||||
// been configured by the homeserver (if it's a `ServerName` and the
|
||||
@@ -247,31 +342,3 @@ impl ClientBuilder {
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ClientBuilder {
|
||||
fn default() -> Self {
|
||||
let encryption_settings = EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
auto_enable_backups: true,
|
||||
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
};
|
||||
let inner = MatrixClient::builder().with_encryption_settings(encryption_settings);
|
||||
|
||||
Self {
|
||||
base_path: None,
|
||||
username: None,
|
||||
server_name: None,
|
||||
homeserver_url: None,
|
||||
server_versions: None,
|
||||
passphrase: Zeroizing::new(None),
|
||||
user_agent: None,
|
||||
sliding_sync_proxy: None,
|
||||
proxy: None,
|
||||
disable_ssl_verification: false,
|
||||
disable_automatic_token_refresh: false,
|
||||
inner,
|
||||
cross_process_refresh_lock_id: None,
|
||||
session_delegate: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::encryption::{backups, recovery};
|
||||
use matrix_sdk::{
|
||||
encryption,
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -34,6 +37,11 @@ pub trait RecoveryStateListener: Sync + Send {
|
||||
fn on_update(&self, status: RecoveryState);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait VerificationStateListener: Sync + Send {
|
||||
fn on_update(&self, status: VerificationState);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum BackupUploadState {
|
||||
Waiting,
|
||||
@@ -186,6 +194,23 @@ impl From<recovery::EnableProgress> for EnableRecoveryProgress {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum VerificationState {
|
||||
Unknown,
|
||||
Verified,
|
||||
Unverified,
|
||||
}
|
||||
|
||||
impl From<encryption::VerificationState> for VerificationState {
|
||||
fn from(value: encryption::VerificationState) -> Self {
|
||||
match &value {
|
||||
encryption::VerificationState::Unknown => Self::Unknown,
|
||||
encryption::VerificationState::Verified => Self::Verified,
|
||||
encryption::VerificationState::Unverified => Self::Unverified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl Encryption {
|
||||
pub fn backup_state_listener(&self, listener: Box<dyn BackupStateListener>) -> Arc<TaskHandle> {
|
||||
@@ -326,4 +351,20 @@ impl Encryption {
|
||||
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
pub fn verification_state(&self) -> VerificationState {
|
||||
self.inner.verification_state().get().into()
|
||||
}
|
||||
|
||||
pub fn verification_state_listener(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn VerificationStateListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let mut subscriber = self.inner.verification_state();
|
||||
Arc::new(TaskHandle::new(RUNTIME.spawn(async move {
|
||||
while let Some(verification_state) = subscriber.next().await {
|
||||
listener.on_update(verification_state.into());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use matrix_sdk::{
|
||||
self, encryption::CryptoStoreError, oidc::OidcError, HttpError, IdParseError,
|
||||
NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, HttpError,
|
||||
IdParseError, NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
@@ -115,6 +115,12 @@ impl From<RoomError> for ClientError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventCacheError> for ClientError {
|
||||
fn from(e: EventCacheError) -> Self {
|
||||
Self::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum RoomError {
|
||||
|
||||
@@ -216,3 +216,113 @@ where
|
||||
event.as_original().context("Failed to get original content")?.content.clone();
|
||||
Ok(original_content)
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum StateEventType {
|
||||
CallMember,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
}
|
||||
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAliases => Self::RoomAliases,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
StateEventType::RoomEncryption => Self::RoomEncryption,
|
||||
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules => Self::RoomJoinRules,
|
||||
StateEventType::RoomMemberEvent => Self::RoomMember,
|
||||
StateEventType::RoomName => Self::RoomName,
|
||||
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
|
||||
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
|
||||
StateEventType::RoomServerAcl => Self::RoomServerAcl,
|
||||
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
|
||||
StateEventType::RoomTombstone => Self::RoomTombstone,
|
||||
StateEventType::RoomTopic => Self::RoomTopic,
|
||||
StateEventType::SpaceChild => Self::SpaceChild,
|
||||
StateEventType::SpaceParent => Self::SpaceParent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MessageLikeEventType {
|
||||
CallAnswer,
|
||||
CallCandidates,
|
||||
CallHangup,
|
||||
CallInvite,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationDone,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
PollEnd,
|
||||
PollResponse,
|
||||
PollStart,
|
||||
Reaction,
|
||||
RoomEncrypted,
|
||||
RoomMessage,
|
||||
RoomRedaction,
|
||||
Sticker,
|
||||
UnstablePollEnd,
|
||||
UnstablePollResponse,
|
||||
UnstablePollStart,
|
||||
}
|
||||
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
fn from(val: MessageLikeEventType) -> Self {
|
||||
match val {
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
|
||||
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
|
||||
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
|
||||
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
|
||||
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
|
||||
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
|
||||
MessageLikeEventType::Reaction => Self::Reaction,
|
||||
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
|
||||
MessageLikeEventType::RoomMessage => Self::RoomMessage,
|
||||
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
|
||||
MessageLikeEventType::Sticker => Self::Sticker,
|
||||
MessageLikeEventType::PollEnd => Self::PollEnd,
|
||||
MessageLikeEventType::PollResponse => Self::PollResponse,
|
||||
MessageLikeEventType::PollStart => Self::PollStart,
|
||||
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
|
||||
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
|
||||
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ mod notification;
|
||||
mod notification_settings;
|
||||
mod platform;
|
||||
mod room;
|
||||
mod room_directory_search;
|
||||
mod room_info;
|
||||
mod room_list;
|
||||
mod room_member;
|
||||
@@ -40,6 +41,7 @@ mod session_verification;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod timeline_event_filter;
|
||||
mod tracing;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
@@ -21,6 +21,7 @@ pub enum NotificationEvent {
|
||||
pub struct NotificationSenderInfo {
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub is_name_ambiguous: bool,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -63,6 +64,7 @@ impl NotificationItem {
|
||||
sender_info: NotificationSenderInfo {
|
||||
display_name: item.sender_display_name,
|
||||
avatar_url: item.sender_avatar_url,
|
||||
is_name_ambiguous: item.is_sender_name_ambiguous,
|
||||
},
|
||||
room_info: NotificationRoomInfo {
|
||||
display_name: item.room_display_name,
|
||||
|
||||
@@ -297,10 +297,10 @@ impl NotificationSettings {
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
/// Check if [MSC 4028 push rule][rule] is enabled.
|
||||
/// Returns true if [MSC 4028 push rule][rule] is supported and enabled.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
pub async fn can_push_encrypted_event_to_device(&self) -> bool {
|
||||
let notification_settings = self.sdk_notification_settings.read().await;
|
||||
// Check stable identifier
|
||||
if let Ok(enabled) = notification_settings
|
||||
@@ -319,6 +319,13 @@ impl NotificationSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether [MSC 4028 push rule][rule] is enabled on the homeserver.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
self.sdk_client.can_homeserver_push_encrypted_event_to_device().await.unwrap()
|
||||
}
|
||||
|
||||
/// Set whether user mentions are enabled.
|
||||
pub async fn set_user_mention_enabled(
|
||||
&self,
|
||||
|
||||
@@ -6,6 +6,7 @@ use opentelemetry::KeyValue;
|
||||
use opentelemetry_otlp::{Protocol, WithExportConfig};
|
||||
use opentelemetry_sdk::{runtime::RuntimeChannel, trace::Tracer, Resource};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, time::FormatTime, FormatEvent, FormatFields, FormattedFields},
|
||||
@@ -202,13 +203,26 @@ where
|
||||
}
|
||||
|
||||
let file_layer = config.write_to_files.map(|c| {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&c.file_prefix);
|
||||
|
||||
if let Some(max_files) = c.max_files {
|
||||
builder = builder.max_log_files(max_files as usize)
|
||||
};
|
||||
if let Some(file_suffix) = c.file_suffix {
|
||||
builder = builder.filename_suffix(file_suffix)
|
||||
}
|
||||
|
||||
let writer = builder.build(&c.path).expect("Failed to create a rolling file appender.");
|
||||
|
||||
fmt::layer()
|
||||
.event_format(EventFormatter::new())
|
||||
// EventFormatter doesn't support ANSI colors anyways, but the
|
||||
// default field formatter does, which is unhelpful for iOS +
|
||||
// Android logs, but enabled by default.
|
||||
.with_ansi(false)
|
||||
.with_writer(tracing_appender::rolling::hourly(c.path, c.file_prefix))
|
||||
.with_writer(writer)
|
||||
});
|
||||
|
||||
Layer::and_then(
|
||||
@@ -237,6 +251,8 @@ where
|
||||
pub struct TracingFileConfiguration {
|
||||
path: String,
|
||||
file_prefix: String,
|
||||
file_suffix: Option<String>,
|
||||
max_files: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use matrix_sdk::{room::Room as SdkRoom, RoomMemberships, RoomState};
|
||||
use matrix_sdk::{
|
||||
room::{power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole},
|
||||
RoomMemberships, RoomState,
|
||||
};
|
||||
use matrix_sdk_ui::timeline::RoomExt;
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
|
||||
EventId, UserId,
|
||||
events::{
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
TimelineEventType,
|
||||
},
|
||||
EventId, Int, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
@@ -17,10 +26,11 @@ use super::RUNTIME;
|
||||
use crate::{
|
||||
chunk_iterator::ChunkIterator,
|
||||
error::{ClientError, MediaInfoError, RoomError},
|
||||
event::{MessageLikeEventType, StateEventType},
|
||||
room_info::RoomInfo,
|
||||
room_member::{MessageLikeEventType, RoomMember, StateEventType},
|
||||
room_member::RoomMember,
|
||||
ruma::ImageInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
timeline::{EventTimelineItem, ReceiptType, Timeline},
|
||||
utils::u64_to_uint,
|
||||
TaskHandle,
|
||||
};
|
||||
@@ -124,34 +134,37 @@ impl Room {
|
||||
self.inner.active_room_call_participants().iter().map(|u| u.to_string()).collect()
|
||||
}
|
||||
|
||||
pub fn inviter(&self) -> Option<Arc<RoomMember>> {
|
||||
pub fn inviter(&self) -> Option<RoomMember> {
|
||||
if self.inner.state() == RoomState::Invited {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner
|
||||
.invite_details()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|a| a.inviter)
|
||||
.map(|m| Arc::new(RoomMember::new(m)))
|
||||
self.inner.invite_details().await.ok().and_then(|a| a.inviter).map(|m| m.into())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn timeline(&self) -> Arc<Timeline> {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
timeline.clone()
|
||||
} else {
|
||||
let timeline = Timeline::new(self.inner.timeline().await);
|
||||
*write_guard = Some(timeline.clone());
|
||||
timeline
|
||||
}
|
||||
/// Forces the currently active room key, which is used to encrypt messages,
|
||||
/// to be rotated.
|
||||
///
|
||||
/// A new room key will be crated and shared with all the room members the
|
||||
/// next time a message will be sent. You don't have to call this method,
|
||||
/// room keys will be rotated automatically when necessary. This method is
|
||||
/// still useful for debugging purposes.
|
||||
pub async fn discard_room_key(&self) -> Result<(), ClientError> {
|
||||
self.inner.discard_room_key().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn poll_history(&self) -> Arc<Timeline> {
|
||||
Timeline::new(self.inner.poll_history().await)
|
||||
pub async fn timeline(&self) -> Result<Arc<Timeline>, ClientError> {
|
||||
let mut write_guard = self.timeline.write().await;
|
||||
if let Some(timeline) = &*write_guard {
|
||||
Ok(timeline.clone())
|
||||
} else {
|
||||
let timeline = Timeline::new(self.inner.timeline().await?);
|
||||
*write_guard = Some(timeline.clone());
|
||||
Ok(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Result<String, ClientError> {
|
||||
@@ -171,10 +184,16 @@ impl Room {
|
||||
Ok(Arc::new(RoomMembersIterator::new(self.inner.members(RoomMemberships::empty()).await?)))
|
||||
}
|
||||
|
||||
pub async fn member(&self, user_id: String) -> Result<Arc<RoomMember>, ClientError> {
|
||||
pub async fn members_no_sync(&self) -> Result<Arc<RoomMembersIterator>, ClientError> {
|
||||
Ok(Arc::new(RoomMembersIterator::new(
|
||||
self.inner.members_no_sync(RoomMemberships::empty()).await?,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn member(&self, user_id: String) -> Result<RoomMember, ClientError> {
|
||||
let user_id = UserId::parse(&*user_id).context("Invalid user id.")?;
|
||||
let member = self.inner.get_member(&user_id).await?.context("No user found")?;
|
||||
Ok(Arc::new(RoomMember::new(member)))
|
||||
Ok(member.into())
|
||||
}
|
||||
|
||||
pub fn member_avatar_url(&self, user_id: String) -> Result<Option<String>, ClientError> {
|
||||
@@ -250,6 +269,24 @@ impl Room {
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn set_is_favourite(
|
||||
&self,
|
||||
is_favourite: bool,
|
||||
tag_order: Option<f64>,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.set_is_favourite(is_favourite, tag_order).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_is_low_priority(
|
||||
&self,
|
||||
is_low_priority: bool,
|
||||
tag_order: Option<f64>,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.set_is_low_priority(is_low_priority, tag_order).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Redacts an event from the room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -317,13 +354,11 @@ impl Room {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_id` - The ID of the user to ignore.
|
||||
pub fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.client().account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
})
|
||||
/// * `user_id` - The ID of the user to ignore.
|
||||
pub async fn ignore_user(&self, user_id: String) -> Result<(), ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
self.inner.client().account().ignore_user(&user_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leave this room.
|
||||
@@ -414,9 +449,14 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn can_user_redact(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
pub async fn can_user_redact_own(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact(&user_id).await?)
|
||||
Ok(self.inner.can_user_redact_own(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_redact_other(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.can_user_redact_other(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn can_user_ban(&self, user_id: String) -> Result<bool, ClientError> {
|
||||
@@ -494,6 +534,140 @@ impl Room {
|
||||
pub async fn typing_notice(&self, is_typing: bool) -> Result<(), ClientError> {
|
||||
Ok(self.inner.typing_notice(is_typing).await?)
|
||||
}
|
||||
|
||||
pub fn subscribe_to_typing_notifications(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn TypingNotificationsListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
Arc::new(TaskHandle::new(RUNTIME.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 {
|
||||
let typing_user_ids =
|
||||
typing_user_ids.into_iter().map(|user_id| user_id.to_string()).collect();
|
||||
listener.call(typing_user_ids);
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
/// Set (or unset) a flag on the room to indicate that the user has
|
||||
/// explicitly marked it as unread.
|
||||
pub async fn set_unread_flag(&self, new_value: bool) -> Result<(), ClientError> {
|
||||
Ok(self.inner.set_unread_flag(new_value).await?)
|
||||
}
|
||||
|
||||
/// Mark a room as read, by attaching a read receipt on the latest event.
|
||||
///
|
||||
/// Note: this does NOT unset the unread flag; it's the caller's
|
||||
/// responsibility to do so, if needs be.
|
||||
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
|
||||
let timeline = self.timeline().await?;
|
||||
|
||||
timeline.mark_as_read(receipt_type).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
let power_levels = self.inner.room_power_levels().await?;
|
||||
Ok(RoomPowerLevels::from(power_levels))
|
||||
}
|
||||
|
||||
pub async fn apply_power_level_changes(
|
||||
&self,
|
||||
changes: RoomPowerLevelChanges,
|
||||
) -> Result<(), ClientError> {
|
||||
self.inner.apply_power_level_changes(changes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_power_levels_for_users(
|
||||
&self,
|
||||
updates: Vec<UserPowerLevelUpdate>,
|
||||
) -> Result<(), ClientError> {
|
||||
let updates = updates
|
||||
.iter()
|
||||
.map(|update| {
|
||||
let user_id: &UserId = update.user_id.as_str().try_into()?;
|
||||
let power_level = Int::new(update.power_level).context("Invalid power level")?;
|
||||
Ok((user_id, power_level))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
self.inner
|
||||
.update_power_levels(updates)
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn suggested_role_for_user(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> Result<RoomMemberRole, ClientError> {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.get_suggested_user_role(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn reset_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
Ok(RoomPowerLevels::from(self.inner.reset_power_levels().await?))
|
||||
}
|
||||
|
||||
pub async fn matrix_to_permalink(&self) -> Result<String, ClientError> {
|
||||
Ok(self.inner.matrix_to_permalink().await?.to_string())
|
||||
}
|
||||
|
||||
pub async fn matrix_to_event_permalink(&self, event_id: String) -> Result<String, ClientError> {
|
||||
let event_id = EventId::parse(event_id)?;
|
||||
Ok(self.inner.matrix_to_event_permalink(event_id).await?.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPowerLevels {
|
||||
/// The level required to ban a user.
|
||||
pub ban: i64,
|
||||
/// The level required to invite a user.
|
||||
pub invite: i64,
|
||||
/// The level required to kick a user.
|
||||
pub kick: i64,
|
||||
/// The level required to redact an event.
|
||||
pub redact: i64,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: i64,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: i64,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: i64,
|
||||
/// The level required to change the room's name.
|
||||
pub room_name: i64,
|
||||
/// The level required to change the room's avatar.
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevels {
|
||||
fn from(value: RumaPowerLevels) -> Self {
|
||||
fn state_event_level_for(
|
||||
power_levels: &RumaPowerLevels,
|
||||
event_type: &TimelineEventType,
|
||||
) -> i64 {
|
||||
let default_state: i64 = power_levels.state_default.into();
|
||||
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
|
||||
}
|
||||
Self {
|
||||
ban: value.ban.into(),
|
||||
invite: value.invite.into(),
|
||||
kick: value.kick.into(),
|
||||
redact: value.redact.into(),
|
||||
events_default: value.events_default.into(),
|
||||
state_default: value.state_default.into(),
|
||||
users_default: value.users_default.into(),
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
@@ -501,6 +675,11 @@ pub trait RoomInfoListener: Sync + Send {
|
||||
fn call(&self, room_info: RoomInfo);
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait TypingNotificationsListener: Sync + Send {
|
||||
fn call(&self, typing_user_ids: Vec<String>);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMembersIterator {
|
||||
chunk_iterator: ChunkIterator<matrix_sdk::room::RoomMember>,
|
||||
@@ -518,13 +697,22 @@ impl RoomMembersIterator {
|
||||
self.chunk_iterator.len()
|
||||
}
|
||||
|
||||
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<Arc<RoomMember>>> {
|
||||
fn next_chunk(&self, chunk_size: u32) -> Option<Vec<RoomMember>> {
|
||||
self.chunk_iterator
|
||||
.next(chunk_size)
|
||||
.map(|members| members.into_iter().map(RoomMember::new).map(Arc::new).collect())
|
||||
.map(|members| members.into_iter().map(|m| m.into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// An update for a particular user's power level within the room.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UserPowerLevelUpdate {
|
||||
/// The user ID of the user to update.
|
||||
user_id: String,
|
||||
/// The power level to assign to the user.
|
||||
power_level: i64,
|
||||
}
|
||||
|
||||
impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
|
||||
type Error = MediaInfoError;
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright 2024 Mauro Romito
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::room_directory_search::RoomDirectorySearch as SdkRoomDirectorySearch;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{error::ClientError, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum PublicRoomJoinRule {
|
||||
Public,
|
||||
Knock,
|
||||
}
|
||||
|
||||
impl TryFrom<ruma::directory::PublicRoomJoinRule> for PublicRoomJoinRule {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: ruma::directory::PublicRoomJoinRule) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ruma::directory::PublicRoomJoinRule::Public => Ok(Self::Public),
|
||||
ruma::directory::PublicRoomJoinRule::Knock => Ok(Self::Knock),
|
||||
rule => Err(format!("unsupported join rule: {rule:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDescription {
|
||||
pub room_id: String,
|
||||
pub name: Option<String>,
|
||||
pub topic: Option<String>,
|
||||
pub alias: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub join_rule: Option<PublicRoomJoinRule>,
|
||||
pub is_world_readable: bool,
|
||||
pub joined_members: u64,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::room_directory_search::RoomDescription> for RoomDescription {
|
||||
fn from(value: matrix_sdk::room_directory_search::RoomDescription) -> Self {
|
||||
Self {
|
||||
room_id: value.room_id.to_string(),
|
||||
name: value.name,
|
||||
topic: value.topic,
|
||||
alias: value.alias.map(|alias| alias.to_string()),
|
||||
avatar_url: value.avatar_url.map(|url| url.to_string()),
|
||||
join_rule: value.join_rule.try_into().ok(),
|
||||
is_world_readable: value.is_world_readable,
|
||||
joined_members: value.joined_members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomDirectorySearch {
|
||||
pub(crate) inner: RwLock<SdkRoomDirectorySearch>,
|
||||
}
|
||||
|
||||
impl RoomDirectorySearch {
|
||||
pub fn new(inner: SdkRoomDirectorySearch) -> Self {
|
||||
Self { inner: RwLock::new(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl RoomDirectorySearch {
|
||||
pub async fn next_page(&self) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.next_page().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search(&self, filter: Option<String>, batch_size: u32) -> Result<(), ClientError> {
|
||||
let mut inner = self.inner.write().await;
|
||||
inner.search(filter, batch_size).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn loaded_pages(&self) -> Result<u32, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.loaded_pages() as u32)
|
||||
}
|
||||
|
||||
pub async fn is_at_last_page(&self) -> Result<bool, ClientError> {
|
||||
let inner = self.inner.read().await;
|
||||
Ok(inner.is_at_last_page())
|
||||
}
|
||||
|
||||
pub async fn results(
|
||||
&self,
|
||||
listener: Box<dyn RoomDirectorySearchEntriesListener>,
|
||||
) -> 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(),
|
||||
}]);
|
||||
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(|diff| diff.into()).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomDirectorySearchEntriesResult {
|
||||
pub entries_stream: Arc<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomDirectorySearchEntryUpdate {
|
||||
Append { values: Vec<RoomDescription> },
|
||||
Clear,
|
||||
PushFront { value: RoomDescription },
|
||||
PushBack { value: RoomDescription },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: RoomDescription },
|
||||
Set { index: u32, value: RoomDescription },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<RoomDescription> },
|
||||
}
|
||||
|
||||
impl From<VectorDiff<matrix_sdk::room_directory_search::RoomDescription>>
|
||||
for RoomDirectorySearchEntryUpdate
|
||||
{
|
||||
fn from(diff: VectorDiff<matrix_sdk::room_directory_search::RoomDescription>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(|v| v.into()).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait RoomDirectorySearchEntriesListener: Send + Sync + Debug {
|
||||
fn on_update(&self, room_entries_update: Vec<RoomDirectorySearchEntryUpdate>);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::RoomState;
|
||||
use ruma::OwnedMxcUri;
|
||||
@@ -18,19 +18,23 @@ pub struct RoomInfo {
|
||||
is_public: bool,
|
||||
is_space: bool,
|
||||
is_tombstoned: bool,
|
||||
is_favourite: bool,
|
||||
canonical_alias: Option<String>,
|
||||
alternative_aliases: Vec<String>,
|
||||
membership: Membership,
|
||||
latest_event: Option<Arc<EventTimelineItem>>,
|
||||
inviter: Option<Arc<RoomMember>>,
|
||||
inviter: Option<RoomMember>,
|
||||
active_members_count: u64,
|
||||
invited_members_count: u64,
|
||||
joined_members_count: u64,
|
||||
user_power_levels: HashMap<String, i64>,
|
||||
highlight_count: u64,
|
||||
notification_count: u64,
|
||||
user_defined_notification_mode: Option<RoomNotificationMode>,
|
||||
has_room_call: bool,
|
||||
active_room_call_participants: Vec<String>,
|
||||
/// Whether this room has been explicitly marked as unread
|
||||
is_marked_unread: bool,
|
||||
/// "Interesting" messages received in that room, independently of the
|
||||
/// notification settings.
|
||||
num_unread_messages: u64,
|
||||
@@ -50,6 +54,12 @@ impl RoomInfo {
|
||||
) -> matrix_sdk::Result<Self> {
|
||||
let unread_notification_counts = room.unread_notification_counts();
|
||||
|
||||
let power_levels_map = room.users_with_power_levels().await;
|
||||
let mut user_power_levels = HashMap::<String, i64>::new();
|
||||
for (id, level) in power_levels_map.iter() {
|
||||
user_power_levels.insert(id.to_string(), *level);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
id: room.room_id().to_string(),
|
||||
name: room.name(),
|
||||
@@ -59,19 +69,19 @@ impl RoomInfo {
|
||||
is_public: room.is_public(),
|
||||
is_space: room.is_space(),
|
||||
is_tombstoned: room.is_tombstoned(),
|
||||
is_favourite: room.is_favourite(),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
|
||||
membership: room.state().into(),
|
||||
latest_event,
|
||||
inviter: match room.state() {
|
||||
RoomState::Invited => {
|
||||
room.invite_details().await?.inviter.map(|inner| Arc::new(RoomMember { inner }))
|
||||
}
|
||||
RoomState::Invited => room.invite_details().await?.inviter.map(|m| m.into()),
|
||||
_ => None,
|
||||
},
|
||||
active_members_count: room.active_members_count(),
|
||||
invited_members_count: room.invited_members_count(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
user_power_levels,
|
||||
highlight_count: unread_notification_counts.highlight_count,
|
||||
notification_count: unread_notification_counts.notification_count,
|
||||
user_defined_notification_mode: room
|
||||
@@ -84,6 +94,7 @@ impl RoomInfo {
|
||||
.iter()
|
||||
.map(|u| u.to_string())
|
||||
.collect(),
|
||||
is_marked_unread: room.is_marked_unread(),
|
||||
num_unread_messages: room.num_unread_messages(),
|
||||
num_unread_notifications: room.num_unread_notifications(),
|
||||
num_unread_mentions: room.num_unread_mentions(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{pin_mut, StreamExt, TryFutureExt};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::{
|
||||
@@ -12,9 +12,18 @@ use matrix_sdk::{
|
||||
},
|
||||
RoomListEntry as MatrixRoomListEntry,
|
||||
};
|
||||
use matrix_sdk_ui::room_list_service::filters::{
|
||||
new_filter_all, new_filter_all_non_left, new_filter_fuzzy_match_room_name, new_filter_none,
|
||||
new_filter_normalized_match_room_name,
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::{
|
||||
filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_favourite,
|
||||
new_filter_fuzzy_match_room_name, new_filter_invite, new_filter_non_left,
|
||||
new_filter_none, new_filter_normalized_match_room_name, new_filter_unread,
|
||||
RoomCategory,
|
||||
},
|
||||
BoxedFilterFn,
|
||||
},
|
||||
timeline::default_event_filter,
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -23,6 +32,7 @@ use crate::{
|
||||
room::Room,
|
||||
room_info::RoomInfo,
|
||||
timeline::{EventTimelineItem, Timeline},
|
||||
timeline_event_filter::TimelineEventTypeFilter,
|
||||
TaskHandle, RUNTIME,
|
||||
};
|
||||
|
||||
@@ -38,6 +48,14 @@ pub enum RoomListError {
|
||||
RoomNotFound { room_name: String },
|
||||
#[error("invalid room ID: {error}")]
|
||||
InvalidRoomId { error: String },
|
||||
#[error("A timeline instance already exists for room {room_name}")]
|
||||
TimelineAlreadyExists { room_name: String },
|
||||
#[error("A timeline instance hasn't been initialized for room {room_name}")]
|
||||
TimelineNotInitialized { room_name: String },
|
||||
#[error("Timeline couldn't be initialized: {error}")]
|
||||
InitializingTimeline { error: String },
|
||||
#[error("Event cache ran into an error: {error}")]
|
||||
EventCache { error: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
@@ -49,6 +67,13 @@ impl From<matrix_sdk_ui::room_list_service::Error> for RoomListError {
|
||||
UnknownList(list_name) => Self::UnknownList { list_name },
|
||||
InputCannotBeApplied(_) => Self::InputCannotBeApplied,
|
||||
RoomNotFound(room_id) => Self::RoomNotFound { room_name: room_id.to_string() },
|
||||
TimelineAlreadyExists(room_id) => {
|
||||
Self::TimelineAlreadyExists { room_name: room_id.to_string() }
|
||||
}
|
||||
InitializingTimeline(source) => {
|
||||
Self::InitializingTimeline { error: source.to_string() }
|
||||
}
|
||||
EventCache(error) => Self::EventCache { error: error.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +108,7 @@ impl From<RoomListInput> for matrix_sdk_ui::room_list_service::Input {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListService {
|
||||
pub(crate) inner: Arc<matrix_sdk_ui::RoomListService>,
|
||||
pub(crate) utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
@@ -104,6 +130,7 @@ impl RoomListService {
|
||||
|
||||
Ok(Arc::new(RoomListItem {
|
||||
inner: Arc::new(RUNTIME.block_on(async { self.inner.room(room_id).await })?),
|
||||
utd_hook: self.utd_hook.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -193,7 +220,10 @@ impl RoomList {
|
||||
listener: Box<dyn RoomListEntriesListener>,
|
||||
) -> RoomListEntriesWithDynamicAdaptersResult {
|
||||
let (entries_stream, dynamic_entries_controller) =
|
||||
self.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
|
||||
self.inner.entries_with_dynamic_adapters(
|
||||
page_size.try_into().unwrap(),
|
||||
self.room_list_service.inner.client().roominfo_update_receiver(),
|
||||
);
|
||||
|
||||
RoomListEntriesWithDynamicAdaptersResult {
|
||||
controller: Arc::new(RoomListDynamicEntriesController::new(
|
||||
@@ -375,19 +405,8 @@ impl RoomListDynamicEntriesController {
|
||||
#[uniffi::export]
|
||||
impl RoomListDynamicEntriesController {
|
||||
fn set_filter(&self, kind: RoomListEntriesDynamicFilterKind) -> bool {
|
||||
use RoomListEntriesDynamicFilterKind as Kind;
|
||||
|
||||
match kind {
|
||||
Kind::All => self.inner.set_filter(new_filter_all()),
|
||||
Kind::AllNonLeft => self.inner.set_filter(new_filter_all_non_left(&self.client)),
|
||||
Kind::None => self.inner.set_filter(new_filter_none()),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
self.inner.set_filter(new_filter_normalized_match_room_name(&self.client, &pattern))
|
||||
}
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
self.inner.set_filter(new_filter_fuzzy_match_room_name(&self.client, &pattern))
|
||||
}
|
||||
}
|
||||
let FilterWrapper(filter) = FilterWrapper::from(&self.client, kind);
|
||||
self.inner.set_filter(filter)
|
||||
}
|
||||
|
||||
fn add_one_page(&self) {
|
||||
@@ -401,16 +420,68 @@ impl RoomListDynamicEntriesController {
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListEntriesDynamicFilterKind {
|
||||
All,
|
||||
AllNonLeft,
|
||||
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
|
||||
NonLeft,
|
||||
Unread,
|
||||
Favourite,
|
||||
Invite,
|
||||
Category { expect: RoomListFilterCategory },
|
||||
None,
|
||||
NormalizedMatchRoomName { pattern: String },
|
||||
FuzzyMatchRoomName { pattern: String },
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum RoomListFilterCategory {
|
||||
Group,
|
||||
People,
|
||||
}
|
||||
|
||||
impl From<RoomListFilterCategory> for RoomCategory {
|
||||
fn from(value: RoomListFilterCategory) -> Self {
|
||||
match value {
|
||||
RoomListFilterCategory::Group => Self::Group,
|
||||
RoomListFilterCategory::People => Self::People,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom internal type to transform a `RoomListEntriesDynamicFilterKind` into
|
||||
/// a `BoxedFilterFn`.
|
||||
struct FilterWrapper(BoxedFilterFn);
|
||||
|
||||
impl FilterWrapper {
|
||||
fn from(client: &matrix_sdk::Client, value: RoomListEntriesDynamicFilterKind) -> Self {
|
||||
use RoomListEntriesDynamicFilterKind as Kind;
|
||||
|
||||
match value {
|
||||
Kind::All { filters } => Self(Box::new(new_filter_all(
|
||||
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
|
||||
))),
|
||||
Kind::Any { filters } => Self(Box::new(new_filter_any(
|
||||
filters.into_iter().map(|filter| FilterWrapper::from(client, filter).0).collect(),
|
||||
))),
|
||||
Kind::NonLeft => Self(Box::new(new_filter_non_left(client))),
|
||||
Kind::Unread => Self(Box::new(new_filter_unread(client))),
|
||||
Kind::Favourite => Self(Box::new(new_filter_favourite(client))),
|
||||
Kind::Invite => Self(Box::new(new_filter_invite(client))),
|
||||
Kind::Category { expect } => Self(Box::new(new_filter_category(client, expect.into()))),
|
||||
Kind::None => Self(Box::new(new_filter_none())),
|
||||
Kind::NormalizedMatchRoomName { pattern } => {
|
||||
Self(Box::new(new_filter_normalized_match_room_name(client, &pattern)))
|
||||
}
|
||||
Kind::FuzzyMatchRoomName { pattern } => {
|
||||
Self(Box::new(new_filter_fuzzy_match_room_name(client, &pattern)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomListItem {
|
||||
inner: Arc<matrix_sdk_ui::room_list_service::Room>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
@@ -441,20 +512,53 @@ impl RoomListItem {
|
||||
Ok(RoomInfo::new(self.inner.inner_room(), avatar_url, latest_event).await?)
|
||||
}
|
||||
|
||||
/// Building a `Room`.
|
||||
///
|
||||
/// Be careful that building a `Room` builds its entire `Timeline` at the
|
||||
/// same time.
|
||||
async fn full_room(&self) -> Arc<Room> {
|
||||
Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(self.inner.timeline().await)))),
|
||||
))
|
||||
/// Building a `Room`. If its internal timeline hasn't been initialized
|
||||
/// it'll fail.
|
||||
async fn full_room(&self) -> Result<Arc<Room>, RoomListError> {
|
||||
if let Some(timeline) = self.inner.timeline() {
|
||||
Ok(Arc::new(Room::with_timeline(
|
||||
self.inner.inner_room().clone(),
|
||||
Arc::new(RwLock::new(Some(Timeline::from_arc(timeline)))),
|
||||
)))
|
||||
} else {
|
||||
Err(RoomListError::TimelineNotInitialized {
|
||||
room_name: self.inner.inner_room().room_id().to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary workaround for coroutine leaks on Kotlin.
|
||||
fn full_room_blocking(&self) -> Arc<Room> {
|
||||
RUNTIME.block_on(async move { self.full_room().await })
|
||||
/// Checks whether the Room's timeline has been initialized before.
|
||||
fn is_timeline_initialized(&self) -> bool {
|
||||
self.inner.is_timeline_initialized()
|
||||
}
|
||||
|
||||
/// Initializes the timeline for this room using the provided parameters.
|
||||
///
|
||||
/// * `event_type_filter` - An optional [`TimelineEventTypeFilter`] to be
|
||||
/// used to filter timeline events besides the default timeline filter. If
|
||||
/// `None` is passed, only the default timeline filter will be used.
|
||||
async fn init_timeline(
|
||||
&self,
|
||||
event_type_filter: Option<Arc<TimelineEventTypeFilter>>,
|
||||
) -> Result<(), RoomListError> {
|
||||
let mut timeline_builder = self
|
||||
.inner
|
||||
.default_room_timeline_builder()
|
||||
.await
|
||||
.map_err(|err| RoomListError::InitializingTimeline { error: err.to_string() })?;
|
||||
|
||||
if let Some(event_type_filter) = event_type_filter {
|
||||
timeline_builder = timeline_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(utd_hook) = self.utd_hook.clone() {
|
||||
timeline_builder = timeline_builder.with_unable_to_decrypt_hook(utd_hook);
|
||||
}
|
||||
|
||||
self.inner.init_timeline_with_builder(timeline_builder).map_err(RoomListError::from).await
|
||||
}
|
||||
|
||||
fn subscribe(&self, settings: Option<RoomSubscription>) {
|
||||
@@ -468,14 +572,6 @@ impl RoomListItem {
|
||||
async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
|
||||
self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new)
|
||||
}
|
||||
|
||||
fn has_unread_notifications(&self) -> bool {
|
||||
self.inner.has_unread_notifications()
|
||||
}
|
||||
|
||||
fn unread_notifications(&self) -> Arc<UnreadNotificationsCount> {
|
||||
Arc::new(self.inner.unread_notifications().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, uniffi::Enum)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use matrix_sdk::room::RoomMember as SdkRoomMember;
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::UserId;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::ClientError;
|
||||
use crate::error::ClientError;
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MembershipState {
|
||||
@@ -35,203 +35,57 @@ impl From<matrix_sdk::ruma::events::room::member::MembershipState> for Membershi
|
||||
matrix_sdk::ruma::events::room::member::MembershipState::Leave => {
|
||||
MembershipState::Leave
|
||||
}
|
||||
_ => todo!(
|
||||
_ => unimplemented!(
|
||||
"Handle Custom case: https://github.com/matrix-org/matrix-rust-sdk/issues/1254"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomMember {
|
||||
pub(crate) inner: SdkRoomMember,
|
||||
#[uniffi::export]
|
||||
pub fn suggested_role_for_power_level(power_level: i64) -> RoomMemberRole {
|
||||
// It's not possible to expose the constructor on the Enum through Uniffi ☹️
|
||||
RoomMemberRole::suggested_role_for_power_level(power_level)
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl RoomMember {
|
||||
pub fn user_id(&self) -> String {
|
||||
self.inner.user_id().to_string()
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Option<String> {
|
||||
self.inner.display_name().map(|d| d.to_owned())
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self) -> Option<String> {
|
||||
self.inner.avatar_url().map(ToString::to_string)
|
||||
}
|
||||
|
||||
pub fn membership(&self) -> MembershipState {
|
||||
self.inner.membership().to_owned().into()
|
||||
}
|
||||
|
||||
pub fn is_name_ambiguous(&self) -> bool {
|
||||
self.inner.name_ambiguous()
|
||||
}
|
||||
|
||||
pub fn power_level(&self) -> i64 {
|
||||
self.inner.power_level()
|
||||
}
|
||||
|
||||
pub fn normalized_power_level(&self) -> i64 {
|
||||
self.inner.normalized_power_level()
|
||||
}
|
||||
|
||||
pub fn is_ignored(&self) -> bool {
|
||||
self.inner.is_ignored()
|
||||
}
|
||||
|
||||
pub fn is_account_user(&self) -> bool {
|
||||
self.inner.is_account_user()
|
||||
}
|
||||
|
||||
/// Adds the room member to the current account data's ignore list
|
||||
/// which will ignore the user across all rooms.
|
||||
pub fn ignore(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.ignore().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the room member from the current account data's ignore list
|
||||
/// which will unignore the user across all rooms.
|
||||
pub fn unignore(&self) -> Result<(), ClientError> {
|
||||
RUNTIME.block_on(async move {
|
||||
self.inner.unignore().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn can_ban(&self) -> bool {
|
||||
self.inner.can_ban()
|
||||
}
|
||||
|
||||
pub fn can_invite(&self) -> bool {
|
||||
self.inner.can_invite()
|
||||
}
|
||||
|
||||
pub fn can_kick(&self) -> bool {
|
||||
self.inner.can_kick()
|
||||
}
|
||||
|
||||
pub fn can_redact(&self) -> bool {
|
||||
self.inner.can_redact()
|
||||
}
|
||||
|
||||
pub fn can_send_state(&self, state_event: StateEventType) -> bool {
|
||||
self.inner.can_send_state(state_event.into())
|
||||
}
|
||||
|
||||
pub fn can_send_message(&self, event: MessageLikeEventType) -> bool {
|
||||
self.inner.can_send_message(event.into())
|
||||
}
|
||||
|
||||
pub fn can_trigger_room_notification(&self) -> bool {
|
||||
self.inner.can_trigger_room_notification()
|
||||
}
|
||||
pub fn suggested_power_level_for_role(role: RoomMemberRole) -> i64 {
|
||||
// It's not possible to expose methods on an Enum through Uniffi ☹️
|
||||
role.suggested_power_level()
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub fn new(room_member: SdkRoomMember) -> Self {
|
||||
RoomMember { inner: room_member }
|
||||
}
|
||||
/// Generates a `matrix.to` permalink from to the given userID.
|
||||
#[uniffi::export]
|
||||
pub fn matrix_to_user_permalink(user_id: String) -> Result<String, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
Ok(user_id.matrix_to_uri().to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum StateEventType {
|
||||
CallMember,
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
RoomEncryption,
|
||||
RoomGuestAccess,
|
||||
RoomHistoryVisibility,
|
||||
RoomJoinRules,
|
||||
RoomMemberEvent,
|
||||
RoomName,
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite,
|
||||
RoomTombstone,
|
||||
RoomTopic,
|
||||
SpaceChild,
|
||||
SpaceParent,
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomMember {
|
||||
pub user_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub membership: MembershipState,
|
||||
pub is_name_ambiguous: bool,
|
||||
pub power_level: i64,
|
||||
pub normalized_power_level: i64,
|
||||
pub is_ignored: bool,
|
||||
pub suggested_role_for_power_level: RoomMemberRole,
|
||||
}
|
||||
|
||||
impl From<StateEventType> for ruma::events::StateEventType {
|
||||
fn from(val: StateEventType) -> Self {
|
||||
match val {
|
||||
StateEventType::CallMember => Self::CallMember,
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAliases => Self::RoomAliases,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
StateEventType::RoomEncryption => Self::RoomEncryption,
|
||||
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules => Self::RoomJoinRules,
|
||||
StateEventType::RoomMemberEvent => Self::RoomMember,
|
||||
StateEventType::RoomName => Self::RoomName,
|
||||
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
|
||||
StateEventType::RoomPowerLevels => Self::RoomPowerLevels,
|
||||
StateEventType::RoomServerAcl => Self::RoomServerAcl,
|
||||
StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite,
|
||||
StateEventType::RoomTombstone => Self::RoomTombstone,
|
||||
StateEventType::RoomTopic => Self::RoomTopic,
|
||||
StateEventType::SpaceChild => Self::SpaceChild,
|
||||
StateEventType::SpaceParent => Self::SpaceParent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MessageLikeEventType {
|
||||
CallAnswer,
|
||||
CallInvite,
|
||||
CallHangup,
|
||||
CallCandidates,
|
||||
KeyVerificationReady,
|
||||
KeyVerificationStart,
|
||||
KeyVerificationCancel,
|
||||
KeyVerificationAccept,
|
||||
KeyVerificationKey,
|
||||
KeyVerificationMac,
|
||||
KeyVerificationDone,
|
||||
ReactionSent,
|
||||
RoomEncrypted,
|
||||
RoomMessage,
|
||||
RoomRedaction,
|
||||
Sticker,
|
||||
}
|
||||
|
||||
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
|
||||
fn from(val: MessageLikeEventType) -> Self {
|
||||
match val {
|
||||
MessageLikeEventType::CallAnswer => Self::CallAnswer,
|
||||
MessageLikeEventType::CallInvite => Self::CallInvite,
|
||||
MessageLikeEventType::CallHangup => Self::CallHangup,
|
||||
MessageLikeEventType::CallCandidates => Self::CallCandidates,
|
||||
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
|
||||
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
|
||||
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
|
||||
MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept,
|
||||
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
|
||||
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
|
||||
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
|
||||
MessageLikeEventType::ReactionSent => Self::Reaction,
|
||||
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
|
||||
MessageLikeEventType::RoomMessage => Self::RoomMessage,
|
||||
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
|
||||
MessageLikeEventType::Sticker => Self::Sticker,
|
||||
impl From<SdkRoomMember> for RoomMember {
|
||||
fn from(m: SdkRoomMember) -> Self {
|
||||
RoomMember {
|
||||
user_id: m.user_id().to_string(),
|
||||
display_name: m.display_name().map(|s| s.to_owned()),
|
||||
avatar_url: m.avatar_url().map(|a| a.to_string()),
|
||||
membership: m.membership().clone().into(),
|
||||
is_name_ambiguous: m.name_ambiguous(),
|
||||
power_level: m.power_level(),
|
||||
normalized_power_level: m.normalized_power_level(),
|
||||
is_ignored: m.is_ignored(),
|
||||
suggested_role_for_power_level: m.suggested_role_for_power_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ use ruma::{
|
||||
ImageInfo as RumaImageInfo, MediaSource, ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
serde::JsonObject,
|
||||
OwnedUserId, UInt, UserId,
|
||||
MatrixToUri, MatrixUri as RumaMatrixUri, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
@@ -55,6 +56,72 @@ use crate::{
|
||||
utils::u64_to_uint,
|
||||
};
|
||||
|
||||
/// Parse a matrix entity from a given URI, be it either
|
||||
/// a `matrix.to` link or a `matrix:` URI
|
||||
#[uniffi::export]
|
||||
pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
|
||||
if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
|
||||
return Some(MatrixEntity {
|
||||
id: matrix_uri.id().into(),
|
||||
via: matrix_uri.via().iter().map(|via| via.to_string()).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(matrix_to_uri) = MatrixToUri::parse(&uri) {
|
||||
return Some(MatrixEntity {
|
||||
id: matrix_to_uri.id().into(),
|
||||
via: matrix_to_uri.via().iter().map(|via| via.to_string()).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// A Matrix entity that can be a room, room alias, user, or event, and a list
|
||||
/// of via servers.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct MatrixEntity {
|
||||
id: MatrixId,
|
||||
via: Vec<String>,
|
||||
}
|
||||
|
||||
/// A Matrix ID that can be a room, room alias, user, or event.
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MatrixId {
|
||||
Room { id: String },
|
||||
RoomAlias { alias: String },
|
||||
User { id: String },
|
||||
EventOnRoomId { room_id: String, event_id: String },
|
||||
EventOnRoomAlias { alias: String, event_id: String },
|
||||
}
|
||||
|
||||
impl From<&RumaMatrixId> for MatrixId {
|
||||
fn from(value: &RumaMatrixId) -> Self {
|
||||
match value {
|
||||
RumaMatrixId::User(id) => MatrixId::User { id: id.to_string() },
|
||||
RumaMatrixId::Room(id) => MatrixId::Room { id: id.to_string() },
|
||||
RumaMatrixId::RoomAlias(id) => MatrixId::RoomAlias { alias: id.to_string() },
|
||||
|
||||
RumaMatrixId::Event(room_id_or_alias, event_id) => {
|
||||
if room_id_or_alias.is_room_id() {
|
||||
MatrixId::EventOnRoomId {
|
||||
room_id: room_id_or_alias.to_string(),
|
||||
event_id: event_id.to_string(),
|
||||
}
|
||||
} else if room_id_or_alias.is_room_alias_id() {
|
||||
MatrixId::EventOnRoomAlias {
|
||||
alias: room_id_or_alias.to_string(),
|
||||
event_id: event_id.to_string(),
|
||||
}
|
||||
} else {
|
||||
panic!("Unexpected MatrixId type: {:?}", room_id_or_alias)
|
||||
}
|
||||
}
|
||||
_ => panic!("Unexpected MatrixId type: {:?}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
|
||||
Arc::new(MediaSource::Plain(url.into()))
|
||||
@@ -172,23 +239,38 @@ impl TryFrom<MessageType> for RumaMessageType {
|
||||
formatted: content.formatted.map(Into::into),
|
||||
}))
|
||||
}
|
||||
MessageType::Image { content } => Self::Image(
|
||||
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Audio { content } => Self::Audio(
|
||||
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Video { content } => Self::Video(
|
||||
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::File { content } => Self::File(
|
||||
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.filename(content.filename)
|
||||
.info(content.info.map(Into::into).map(Box::new)),
|
||||
),
|
||||
MessageType::Image { content } => {
|
||||
let mut event_content =
|
||||
RumaImageMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::Image(event_content)
|
||||
}
|
||||
MessageType::Audio { content } => {
|
||||
let mut event_content =
|
||||
RumaAudioMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::Audio(event_content)
|
||||
}
|
||||
MessageType::Video { content } => {
|
||||
let mut event_content =
|
||||
RumaVideoMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::Video(event_content)
|
||||
}
|
||||
MessageType::File { content } => {
|
||||
let mut event_content =
|
||||
RumaFileMessageEventContent::new(content.body, (*content.source).clone())
|
||||
.info(content.info.map(Into::into).map(Box::new));
|
||||
event_content.formatted = content.formatted.map(Into::into);
|
||||
event_content.filename = content.filename;
|
||||
Self::File(event_content)
|
||||
}
|
||||
MessageType::Notice { content } => {
|
||||
Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), {
|
||||
formatted: content.formatted.map(Into::into),
|
||||
@@ -221,6 +303,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::Image(c) => MessageType::Image {
|
||||
content: ImageMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
@@ -228,6 +312,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::Audio(c) => MessageType::Audio {
|
||||
content: AudioMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
audio: c.audio.map(Into::into),
|
||||
@@ -237,6 +323,8 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::Video(c) => MessageType::Video {
|
||||
content: VideoMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
},
|
||||
@@ -244,6 +332,7 @@ impl From<RumaMessageType> for MessageType {
|
||||
RumaMessageType::File(c) => MessageType::File {
|
||||
content: FileMessageContent {
|
||||
body: c.body.clone(),
|
||||
formatted: c.formatted.as_ref().map(Into::into),
|
||||
filename: c.filename.clone(),
|
||||
source: Arc::new(c.source.clone()),
|
||||
info: c.info.as_deref().map(Into::into),
|
||||
@@ -295,6 +384,8 @@ pub struct EmoteMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct ImageMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<ImageInfo>,
|
||||
}
|
||||
@@ -302,6 +393,8 @@ pub struct ImageMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct AudioMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<AudioInfo>,
|
||||
pub audio: Option<UnstableAudioDetailsContent>,
|
||||
@@ -311,6 +404,8 @@ pub struct AudioMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct VideoMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<VideoInfo>,
|
||||
}
|
||||
@@ -318,6 +413,7 @@ pub struct VideoMessageContent {
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct FileMessageContent {
|
||||
pub body: String,
|
||||
pub formatted: Option<FormattedBody>,
|
||||
pub filename: Option<String>,
|
||||
pub source: Arc<MediaSource>,
|
||||
pub info: Option<FileInfo>,
|
||||
|
||||
@@ -12,13 +12,18 @@
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use std::{fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use futures_util::pin_mut;
|
||||
use matrix_sdk::Client;
|
||||
use matrix_sdk_ui::sync_service::{
|
||||
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
|
||||
SyncServiceBuilder as MatrixSyncServiceBuilder,
|
||||
use matrix_sdk_ui::{
|
||||
sync_service::{
|
||||
State as MatrixSyncServiceState, SyncService as MatrixSyncService,
|
||||
SyncServiceBuilder as MatrixSyncServiceBuilder,
|
||||
},
|
||||
unable_to_decrypt_hook::{
|
||||
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo, UtdHookManager,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -53,12 +58,16 @@ pub trait SyncServiceStateObserver: Send + Sync + Debug {
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SyncService {
|
||||
pub(crate) inner: Arc<MatrixSyncService>,
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl SyncService {
|
||||
pub fn room_list_service(&self) -> Arc<RoomListService> {
|
||||
Arc::new(RoomListService { inner: self.inner.room_list_service() })
|
||||
Arc::new(RoomListService {
|
||||
inner: self.inner.room_list_service(),
|
||||
utd_hook: self.utd_hook.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start(&self) {
|
||||
@@ -85,24 +94,106 @@ impl SyncService {
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
pub struct SyncServiceBuilder {
|
||||
builder: MatrixSyncServiceBuilder,
|
||||
|
||||
utd_hook: Option<Arc<UtdHookManager>>,
|
||||
}
|
||||
|
||||
impl SyncServiceBuilder {
|
||||
pub(crate) fn new(client: Client) -> Arc<Self> {
|
||||
Arc::new(Self { builder: MatrixSyncService::builder(client) })
|
||||
Arc::new(Self { builder: MatrixSyncService::builder(client), utd_hook: None })
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl SyncServiceBuilder {
|
||||
pub fn with_unified_invites_in_room_list(
|
||||
self: Arc<Self>,
|
||||
with_unified_invites: bool,
|
||||
) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_unified_invites_in_room_list(with_unified_invites);
|
||||
Arc::new(Self { builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_cross_process_lock(app_identifier);
|
||||
Arc::new(Self { builder })
|
||||
Arc::new(Self { builder, utd_hook: this.utd_hook })
|
||||
}
|
||||
|
||||
pub fn with_utd_hook(self: Arc<Self>, delegate: Box<dyn UnableToDecryptDelegate>) -> Arc<Self> {
|
||||
// UTDs detected before this duration may be reclassified as "late decryption"
|
||||
// events (or discarded, if they get decrypted fast enough).
|
||||
const UTD_HOOK_GRACE_PERIOD: Duration = Duration::from_secs(60);
|
||||
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let utd_hook = Some(Arc::new(
|
||||
UtdHookManager::new(Arc::new(UtdHook { delegate }))
|
||||
.with_max_delay(UTD_HOOK_GRACE_PERIOD),
|
||||
));
|
||||
Arc::new(Self { builder: this.builder, utd_hook })
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Ok(Arc::new(SyncService { inner: Arc::new(this.builder.build().await?) }))
|
||||
Ok(Arc::new(SyncService {
|
||||
inner: Arc::new(this.builder.build().await?),
|
||||
utd_hook: this.utd_hook,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
pub trait UnableToDecryptDelegate: Sync + Send {
|
||||
fn on_utd(&self, info: UnableToDecryptInfo);
|
||||
}
|
||||
|
||||
struct UtdHook {
|
||||
delegate: Box<dyn UnableToDecryptDelegate>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UtdHook {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UtdHook").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnableToDecryptHook for UtdHook {
|
||||
fn on_utd(&self, info: SdkUnableToDecryptInfo) {
|
||||
const IGNORE_UTD_PERIOD: Duration = Duration::from_secs(4);
|
||||
|
||||
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
|
||||
// not considered UTDs.
|
||||
if let Some(duration) = &info.time_to_decrypt {
|
||||
if *duration < IGNORE_UTD_PERIOD {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Report the UTD to the client.
|
||||
self.delegate.on_utd(info.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct UnableToDecryptInfo {
|
||||
/// The identifier of the event that couldn't get decrypted.
|
||||
event_id: String,
|
||||
|
||||
/// If the event could be decrypted late (that is, the event was encrypted
|
||||
/// at first, but could be decrypted later on), then this indicates the
|
||||
/// time it took to decrypt the event. If it is not set, this is
|
||||
/// considered a definite UTD.
|
||||
///
|
||||
/// If set, this is in milliseconds.
|
||||
pub time_to_decrypt_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<SdkUnableToDecryptInfo> for UnableToDecryptInfo {
|
||||
fn from(value: SdkUnableToDecryptInfo) -> Self {
|
||||
Self {
|
||||
event_id: value.event_id.to_string(),
|
||||
time_to_decrypt_ms: value.time_to_decrypt.map(|ttd| ttd.as_millis() as u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_ui::timeline::{PollResult, TimelineDetails};
|
||||
use tracing::warn;
|
||||
|
||||
@@ -40,6 +41,7 @@ impl TimelineItemContent {
|
||||
}
|
||||
}
|
||||
Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()),
|
||||
Content::CallInvite => TimelineItemContentKind::CallInvite,
|
||||
Content::UnableToDecrypt(msg) => {
|
||||
TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) }
|
||||
}
|
||||
@@ -112,6 +114,7 @@ pub enum TimelineItemContentKind {
|
||||
end_time: Option<u64>,
|
||||
has_been_edited: bool,
|
||||
},
|
||||
CallInvite,
|
||||
UnableToDecrypt {
|
||||
msg: EncryptedMessage,
|
||||
},
|
||||
@@ -307,7 +310,7 @@ pub enum OtherState {
|
||||
RoomJoinRules,
|
||||
RoomName { name: Option<String> },
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels,
|
||||
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite { display_name: Option<String> },
|
||||
RoomTombstone,
|
||||
@@ -350,7 +353,20 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Self::RoomName { name }
|
||||
}
|
||||
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
|
||||
Content::RoomPowerLevels(_) => Self::RoomPowerLevels,
|
||||
Content::RoomPowerLevels(c) => match c {
|
||||
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
|
||||
users: power_level_user_changes(content, prev_content)
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect(),
|
||||
previous: prev_content.as_ref().map(|prev_content| {
|
||||
prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
|
||||
}),
|
||||
},
|
||||
FullContent::Redacted(_) => {
|
||||
Self::RoomPowerLevels { users: Default::default(), previous: None }
|
||||
}
|
||||
},
|
||||
Content::RoomServerAcl(_) => Self::RoomServerAcl,
|
||||
Content::RoomThirdPartyInvite(c) => {
|
||||
let display_name = match c {
|
||||
|
||||
@@ -52,19 +52,21 @@ use tokio::{
|
||||
use tracing::{error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
use crate::{
|
||||
client::ProgressWatcher,
|
||||
error::{ClientError, RoomError},
|
||||
helpers::unwrap_or_clone_arc,
|
||||
ruma::{AssetType, AudioInfo, FileInfo, ImageInfo, PollKind, ThumbnailInfo, VideoInfo},
|
||||
ruma::{
|
||||
AssetType, AudioInfo, FileInfo, FormattedBody, ImageInfo, PollKind, ThumbnailInfo,
|
||||
VideoInfo,
|
||||
},
|
||||
task_handle::TaskHandle,
|
||||
RUNTIME,
|
||||
};
|
||||
|
||||
mod content;
|
||||
|
||||
pub use self::content::{Reaction, ReactionSenderData, TimelineItemContent};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
#[repr(transparent)]
|
||||
pub struct Timeline {
|
||||
@@ -106,12 +108,12 @@ impl Timeline {
|
||||
|
||||
async fn send_attachment(
|
||||
&self,
|
||||
url: String,
|
||||
filename: String,
|
||||
mime_type: Mime,
|
||||
attachment_config: AttachmentConfig,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Result<(), RoomError> {
|
||||
let request = self.inner.send_attachment(url, mime_type, attachment_config);
|
||||
let request = self.inner.send_attachment(filename, mime_type, attachment_config);
|
||||
if let Some(progress_watcher) = progress_watcher {
|
||||
let mut subscriber = request.subscribe_to_send_progress();
|
||||
RUNTIME.spawn(async move {
|
||||
@@ -196,6 +198,17 @@ impl Timeline {
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark the room as read by trying to attach an *unthreaded* read receipt
|
||||
/// to the latest room event.
|
||||
///
|
||||
/// This works even if the latest event belongs to a thread, as a threaded
|
||||
/// reply also belongs to the unthreaded timeline. No threaded receipt
|
||||
/// will be sent here (see also #3123).
|
||||
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
|
||||
self.inner.mark_as_read(receipt_type.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send(self: Arc<Self>, msg: Arc<RoomMessageEventContentWithoutRelation>) {
|
||||
RUNTIME.spawn(async move {
|
||||
self.inner.send((*msg).to_owned().with_relation(None).into()).await;
|
||||
@@ -207,6 +220,8 @@ impl Timeline {
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
image_info: ImageInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -227,7 +242,9 @@ impl Timeline {
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
}
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -238,6 +255,8 @@ impl Timeline {
|
||||
url: String,
|
||||
thumbnail_url: Option<String>,
|
||||
video_info: VideoInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -258,7 +277,9 @@ impl Timeline {
|
||||
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
|
||||
}
|
||||
_ => AttachmentConfig::new().info(attachment_info),
|
||||
};
|
||||
}
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -268,6 +289,8 @@ impl Timeline {
|
||||
self: Arc<Self>,
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -280,7 +303,10 @@ impl Timeline {
|
||||
.map_err(|_| RoomError::InvalidAttachmentData)?;
|
||||
|
||||
let attachment_info = AttachmentInfo::Audio(base_audio_info);
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -291,6 +317,8 @@ impl Timeline {
|
||||
url: String,
|
||||
audio_info: AudioInfo,
|
||||
waveform: Vec<u16>,
|
||||
caption: Option<String>,
|
||||
formatted_caption: Option<FormattedBody>,
|
||||
progress_watcher: Option<Box<dyn ProgressWatcher>>,
|
||||
) -> Arc<SendAttachmentJoinHandle> {
|
||||
SendAttachmentJoinHandle::new(RUNTIME.spawn(async move {
|
||||
@@ -304,7 +332,10 @@ impl Timeline {
|
||||
|
||||
let attachment_info =
|
||||
AttachmentInfo::Voice { audio_info: base_audio_info, waveform: Some(waveform) };
|
||||
let attachment_config = AttachmentConfig::new().info(attachment_info);
|
||||
let attachment_config = AttachmentConfig::new()
|
||||
.info(attachment_info)
|
||||
.caption(caption)
|
||||
.formatted_caption(formatted_caption.map(Into::into));
|
||||
|
||||
self.send_attachment(url, mime_type, attachment_config, progress_watcher).await
|
||||
}))
|
||||
@@ -534,6 +565,12 @@ impl Timeline {
|
||||
Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype)))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn latest_event(&self) -> Option<Arc<EventTimelineItem>> {
|
||||
let latest_event = self.inner.latest_event().await;
|
||||
|
||||
latest_event.map(|item| Arc::new(EventTimelineItem(item)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
@@ -991,7 +1028,7 @@ impl From<ReceiptType> for ruma::api::client::receipt::create_receipt::v3::Recei
|
||||
fn from(value: ReceiptType) -> Self {
|
||||
match value {
|
||||
ReceiptType::Read => Self::Read,
|
||||
ReceiptType::ReadPrivate => Self::Read,
|
||||
ReceiptType::ReadPrivate => Self::ReadPrivate,
|
||||
ReceiptType::FullyRead => Self::FullyRead,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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,
|
||||
}
|
||||
|
||||
#[uniffi::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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
[bindings.kotlin]
|
||||
package_name = "org.matrix.rustcomponents.sdk"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
cdylib_name = "matrix_sdk_ffi"
|
||||
android_cleaner = true
|
||||
@@ -1,3 +1,9 @@
|
||||
# unreleased
|
||||
|
||||
- Replace the `Notification` type from Ruma in `SyncResponse` and `StateChanges` by a custom one
|
||||
- The ambiguity maps in `SyncResponse` are moved to `JoinedRoom` and `LeftRoom`
|
||||
- `AmbiguityCache` contains the room member's user ID
|
||||
|
||||
# 0.7.0
|
||||
|
||||
- Rename `RoomType` to `RoomState`
|
||||
|
||||
@@ -23,6 +23,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"]
|
||||
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
|
||||
message-ids = ["matrix-sdk-crypto?/message-ids"]
|
||||
experimental-sliding-sync = ["ruma/unstable-msc3575"]
|
||||
uniffi = ["dep:uniffi"]
|
||||
|
||||
# helpers for testing features build upon this
|
||||
testing = [
|
||||
@@ -38,7 +39,7 @@ as_variant = { workspace = true }
|
||||
assert_matches = { workspace = true, optional = true }
|
||||
assert_matches2 = { workspace = true, optional = true }
|
||||
async-trait = { workspace = true }
|
||||
bitflags = "2.1.0"
|
||||
bitflags = { version = "2.4.0", features = ["serde"] }
|
||||
eyeball = { workspace = true }
|
||||
eyeball-im = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
@@ -48,12 +49,13 @@ matrix-sdk-crypto = { workspace = true, optional = true }
|
||||
matrix-sdk-store-encryption = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true, optional = true }
|
||||
once_cell = { workspace = true }
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381"] }
|
||||
ruma = { workspace = true, features = ["canonical-json", "unstable-msc3381", "unstable-msc2867"] }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uniffi = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
@@ -63,9 +65,13 @@ futures-executor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
matrix-sdk-test = { workspace = true }
|
||||
stream_assert = { workspace = true }
|
||||
web-time = "1.1.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,10 @@
|
||||
|
||||
//! Helpers for creating `std::fmt::Debug` implementations.
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
use std::fmt;
|
||||
|
||||
pub use matrix_sdk_common::debug::*;
|
||||
use ruma::{
|
||||
api::client::{push::get_notifications::v3::Notification, sync::sync_events::v3::InvitedRoom},
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
};
|
||||
use ruma::{api::client::sync::sync_events::v3::InvitedRoom, serde::Raw};
|
||||
|
||||
/// A wrapper around a slice of `Raw` events that implements `Debug` in a way
|
||||
/// that only prints the event type of each item.
|
||||
@@ -36,47 +32,6 @@ impl<'a, T> fmt::Debug for DebugListOfRawEventsNoId<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a notification map as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type
|
||||
/// for the raw events contained in each notification.
|
||||
pub struct DebugNotificationMap<'a>(pub &'a BTreeMap<OwnedRoomId, Vec<Notification>>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotificationMap<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
map.entries(self.0.iter().map(|(room_id, raw)| (room_id, DebugNotificationList(raw))));
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugNotificationList<'a>(&'a [Notification]);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotificationList<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
list.entries(self.0.iter().map(DebugNotification));
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugNotification<'a>(&'a Notification);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugNotification<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Notification")
|
||||
.field("actions", &self.0.actions)
|
||||
.field("event", &DebugRawEvent(&self.0.event))
|
||||
.field("profile_tag", &self.0.profile_tag)
|
||||
.field("read", &self.0.read)
|
||||
.field("room_id", &self.0.room_id)
|
||||
.field("ts", &self.0.ts)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an invited room as found in `/sync` responses that
|
||||
/// implements `Debug` in a way that only prints the event ID and event type for
|
||||
/// the raw events contained in `invite_state`.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
//! SDK-specific variations of response types from Ruma.
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
use std::{collections::BTreeMap, fmt, iter};
|
||||
|
||||
pub use matrix_sdk_common::deserialized_responses::*;
|
||||
use ruma::{
|
||||
@@ -23,7 +23,7 @@ use ruma::{
|
||||
member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, EventContentFromType,
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
|
||||
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
|
||||
StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
@@ -34,9 +34,12 @@ use serde::Serialize;
|
||||
|
||||
/// A change in ambiguity of room members that an `m.room.member` event
|
||||
/// triggers.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct AmbiguityChange {
|
||||
/// The user ID of the member that is contained in the state key of the
|
||||
/// `m.room.member` event.
|
||||
pub member_id: OwnedUserId,
|
||||
/// Is the member that is contained in the state key of the `m.room.member`
|
||||
/// event itself ambiguous because of the event.
|
||||
pub member_ambiguous: bool,
|
||||
@@ -46,6 +49,15 @@ pub struct AmbiguityChange {
|
||||
pub ambiguated_member: Option<OwnedUserId>,
|
||||
}
|
||||
|
||||
impl AmbiguityChange {
|
||||
/// Get an iterator over the user IDs listed in this `AmbiguityChange`.
|
||||
pub fn user_ids(&self) -> impl Iterator<Item = &UserId> {
|
||||
iter::once(&*self.member_id)
|
||||
.chain(self.disambiguated_member.as_deref())
|
||||
.chain(self.ambiguated_member.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
@@ -66,6 +78,16 @@ pub struct MembersResponse {
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any event received via sync.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RawAnySyncOrStrippedTimelineEvent {
|
||||
/// An event from a room in joined or left state.
|
||||
Sync(Raw<AnySyncTimelineEvent>),
|
||||
/// An event from a room in invited state.
|
||||
Stripped(Raw<AnyStrippedStateEvent>),
|
||||
}
|
||||
|
||||
/// Wrapper around both versions of any raw state event.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
|
||||
@@ -56,4 +56,9 @@ pub enum Error {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
MegolmError(#[from] MegolmError),
|
||||
|
||||
/// An error caused by calling the `BaseClient::receive_all_members`
|
||||
/// function with invalid parameters
|
||||
#[error("receive_all_members function was called with invalid parameters")]
|
||||
InvalidReceiveMembersParameters,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ use ruma::events::{
|
||||
poll::unstable_start::SyncUnstablePollStartEvent, room::message::SyncRoomMessageEvent,
|
||||
AnySyncMessageLikeEvent, AnySyncTimelineEvent,
|
||||
};
|
||||
use ruma::{MxcUri, OwnedEventId};
|
||||
use ruma::{
|
||||
events::{call::invite::SyncCallInviteEvent, relation::RelationType},
|
||||
MxcUri, OwnedEventId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalRoomMemberEvent;
|
||||
@@ -25,11 +28,15 @@ pub enum PossibleLatestEvent<'a> {
|
||||
YesRoomMessage(&'a SyncRoomMessageEvent),
|
||||
/// This message is suitable - it is a poll
|
||||
YesPoll(&'a SyncUnstablePollStartEvent),
|
||||
|
||||
/// This message is suitable - it is a call invite
|
||||
YesCallInvite(&'a SyncCallInviteEvent),
|
||||
|
||||
// Later: YesState(),
|
||||
// Later: YesReaction(),
|
||||
/// Not suitable - it's a state event
|
||||
NoUnsupportedEventType,
|
||||
/// Not suitable - it's not an m.room.message
|
||||
/// Not suitable - it's not a m.room.message or an edit/replacement
|
||||
NoUnsupportedMessageLikeType,
|
||||
/// Not suitable - it's encrypted
|
||||
NoEncrypted,
|
||||
@@ -40,15 +47,37 @@ pub enum PossibleLatestEvent<'a> {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> {
|
||||
match event {
|
||||
// Suitable - we have an m.room.message that was not redacted
|
||||
// Suitable - we have an m.room.message that was not redacted or edited
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
|
||||
PossibleLatestEvent::YesRoomMessage(message)
|
||||
// Check if this is a replacement for another message. If it is, ignore it
|
||||
if let Some(original_message) = message.as_original() {
|
||||
let is_replacement =
|
||||
original_message.content.relates_to.as_ref().map_or(false, |relates_to| {
|
||||
if let Some(relation_type) = relates_to.rel_type() {
|
||||
relation_type == RelationType::Replacement
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if is_replacement {
|
||||
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
|
||||
} else {
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
return PossibleLatestEvent::YesRoomMessage(message);
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
|
||||
PossibleLatestEvent::YesPoll(poll)
|
||||
}
|
||||
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
|
||||
PossibleLatestEvent::YesCallInvite(invite)
|
||||
}
|
||||
|
||||
// Encrypted events are not suitable
|
||||
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
|
||||
PossibleLatestEvent::NoEncrypted
|
||||
@@ -225,10 +254,15 @@ mod tests {
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use ruma::{
|
||||
events::{
|
||||
call::{
|
||||
invite::{CallInviteEventContent, SyncCallInviteEvent},
|
||||
SessionDescription,
|
||||
},
|
||||
poll::unstable_start::{
|
||||
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent, UnstablePollAnswer,
|
||||
UnstablePollStartContentBlock,
|
||||
},
|
||||
relation::Replacement,
|
||||
room::{
|
||||
encrypted::{
|
||||
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
|
||||
@@ -236,7 +270,7 @@ mod tests {
|
||||
},
|
||||
message::{
|
||||
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
|
||||
RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
|
||||
},
|
||||
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
|
||||
ImageInfo, MediaSource,
|
||||
@@ -249,14 +283,14 @@ mod tests {
|
||||
},
|
||||
owned_event_id, owned_mxc_uri, owned_user_id,
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch, UInt,
|
||||
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLatestEvent};
|
||||
|
||||
#[test]
|
||||
fn room_messages_are_suitable() {
|
||||
fn test_room_messages_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomMessageEventContent::new(MessageType::Image(
|
||||
@@ -280,7 +314,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polls_are_suitable() {
|
||||
fn test_polls_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
|
||||
SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
|
||||
@@ -303,7 +337,29 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_types_of_messagelike_are_unsuitable() {
|
||||
fn test_call_invites_are_suitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
|
||||
SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: CallInviteEventContent::new(
|
||||
"call_id".into(),
|
||||
UInt::new(123).unwrap(),
|
||||
SessionDescription::new("".into(), "".into()),
|
||||
VoipVersionId::V1,
|
||||
),
|
||||
event_id: owned_event_id!("$1"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
assert_let!(
|
||||
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
|
||||
is_suitable_for_latest_event(&event)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_types_of_messagelike_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
|
||||
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: StickerEventContent::new(
|
||||
@@ -325,7 +381,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacted_messages_are_suitable() {
|
||||
fn test_redacted_messages_are_suitable() {
|
||||
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
|
||||
let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({
|
||||
"content": {},
|
||||
@@ -353,7 +409,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_messages_are_unsuitable() {
|
||||
fn test_encrypted_messages_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
|
||||
SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: RoomEncryptedEventContent::new(
|
||||
@@ -373,7 +429,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_events_are_unsuitable() {
|
||||
fn test_state_events_are_unsuitable() {
|
||||
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
|
||||
SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
|
||||
content: RoomTopicEventContent::new("".to_owned()),
|
||||
@@ -392,7 +448,31 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_latest_event() {
|
||||
fn test_replacement_events_are_unsuitable() {
|
||||
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
|
||||
event_content.relates_to = Some(Relation::Replacement(Replacement::new(
|
||||
owned_event_id!("$1"),
|
||||
RoomMessageEventContent::text_plain("Hello, world!").into(),
|
||||
)));
|
||||
|
||||
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
|
||||
content: event_content,
|
||||
event_id: owned_event_id!("$2"),
|
||||
sender: owned_user_id!("@a:b.c"),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
|
||||
unsigned: MessageLikeUnsigned::new(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_matches!(
|
||||
is_suitable_for_latest_event(&event),
|
||||
PossibleLatestEvent::NoUnsupportedMessageLikeType
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_latest_event() {
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct TestStruct {
|
||||
latest_event: LatestEvent,
|
||||
|
||||
@@ -31,15 +31,20 @@ pub mod latest_event;
|
||||
pub mod media;
|
||||
mod rooms;
|
||||
|
||||
mod read_receipts;
|
||||
pub mod read_receipts;
|
||||
pub use read_receipts::PreviousEventsProvider;
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
mod sliding_sync;
|
||||
|
||||
pub mod store;
|
||||
pub mod sync;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
mod test_utils;
|
||||
mod utils;
|
||||
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
pub use client::BaseClient;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use http;
|
||||
@@ -47,8 +52,8 @@ pub use http;
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{
|
||||
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomMember, RoomMemberships,
|
||||
RoomState, RoomStateFilter,
|
||||
DisplayName, Room, RoomCreateWithCreatorEventContent, RoomInfo, RoomInfoUpdate, RoomMember,
|
||||
RoomMemberships, RoomState, RoomStateFilter,
|
||||
};
|
||||
pub use store::{StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError};
|
||||
pub use utils::{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,8 +52,12 @@ pub struct RoomMember {
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub(crate) fn from_parts(member_info: MemberInfo, room_info: &MemberRoomInfo<'_>) -> Self {
|
||||
let MemberInfo { event, profile, presence } = member_info;
|
||||
pub(crate) fn from_parts(
|
||||
event: MemberEvent,
|
||||
profile: Option<MinimalRoomMemberEvent>,
|
||||
presence: Option<PresenceEvent>,
|
||||
room_info: &MemberRoomInfo<'_>,
|
||||
) -> Self {
|
||||
let MemberRoomInfo {
|
||||
power_levels,
|
||||
max_power_level,
|
||||
@@ -122,7 +126,8 @@ impl RoomMember {
|
||||
/// Get the normalized power level of this member.
|
||||
///
|
||||
/// The normalized power level depends on the maximum power level that can
|
||||
/// be found in a certain room, it's always in the range of 0-100.
|
||||
/// be found in a certain room, positive values are always in the range of
|
||||
/// 0-100.
|
||||
pub fn normalized_power_level(&self) -> i64 {
|
||||
if self.max_power_level > 0 {
|
||||
(self.power_level() * 100) / self.max_power_level
|
||||
@@ -160,11 +165,19 @@ impl RoomMember {
|
||||
self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
|
||||
}
|
||||
|
||||
/// Whether this user can redact events based on the power levels.
|
||||
/// Whether this user can redact their own events based on the power levels.
|
||||
///
|
||||
/// Same as `member.can_do(PowerLevelAction::Redact)`.
|
||||
pub fn can_redact(&self) -> bool {
|
||||
self.can_do_impl(|pls| pls.user_can_redact(self.user_id()))
|
||||
/// Same as `member.can_do(PowerLevelAction::RedactOwn)`.
|
||||
pub fn can_redact_own(&self) -> bool {
|
||||
self.can_do_impl(|pls| pls.user_can_redact_own_event(self.user_id()))
|
||||
}
|
||||
|
||||
/// Whether this user can redact events of other users based on the power
|
||||
/// levels.
|
||||
///
|
||||
/// Same as `member.can_do(PowerLevelAction::RedactOther)`.
|
||||
pub fn can_redact_other(&self) -> bool {
|
||||
self.can_do_impl(|pls| pls.user_can_redact_event_of_other(self.user_id()))
|
||||
}
|
||||
|
||||
/// Whether this user can send message events based on the power levels.
|
||||
@@ -222,14 +235,7 @@ impl RoomMember {
|
||||
}
|
||||
}
|
||||
|
||||
// Information about a room member.
|
||||
pub(crate) struct MemberInfo {
|
||||
pub event: MemberEvent,
|
||||
pub(crate) profile: Option<MinimalRoomMemberEvent>,
|
||||
pub(crate) presence: Option<PresenceEvent>,
|
||||
}
|
||||
|
||||
// Information about a the room a member is in.
|
||||
// Information about the room a member is in.
|
||||
pub(crate) struct MemberRoomInfo<'a> {
|
||||
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
|
||||
pub(crate) max_power_level: i64,
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::{
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{Room, RoomInfo, RoomState, RoomStateFilter};
|
||||
pub use normal::{Room, RoomInfo, RoomInfoUpdate, RoomState, RoomStateFilter};
|
||||
use ruma::{
|
||||
assign,
|
||||
events::{
|
||||
@@ -30,6 +30,7 @@ use ruma::{
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
tag::{TagName, Tags},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncStateEvent,
|
||||
},
|
||||
@@ -104,6 +105,15 @@ pub struct BaseRoomInfo {
|
||||
/// memberships.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
|
||||
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
|
||||
/// Whether this room has been manually marked as unread.
|
||||
#[serde(default)]
|
||||
pub(crate) is_marked_unread: bool,
|
||||
/// Some notable tags.
|
||||
///
|
||||
/// We are not interested by all the tags. Some tags are more important than
|
||||
/// others, and this field collects them.
|
||||
#[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
|
||||
pub(crate) notable_tags: RoomNotableTags,
|
||||
}
|
||||
|
||||
impl BaseRoomInfo {
|
||||
@@ -285,6 +295,36 @@ impl BaseRoomInfo {
|
||||
self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_notable_tags(&mut self, tags: &Tags) {
|
||||
let mut notable_tags = RoomNotableTags::empty();
|
||||
|
||||
if tags.contains_key(&TagName::Favorite) {
|
||||
notable_tags.insert(RoomNotableTags::FAVOURITE);
|
||||
}
|
||||
|
||||
if tags.contains_key(&TagName::LowPriority) {
|
||||
notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
|
||||
}
|
||||
|
||||
self.notable_tags = notable_tags;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Notable tags, i.e. subset of tags that we are more interested by.
|
||||
///
|
||||
/// We are not interested by all the tags. Some tags are more important than
|
||||
/// others, and this struct describes them.
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
|
||||
pub(crate) struct RoomNotableTags: u8 {
|
||||
/// The `m.favourite` tag.
|
||||
const FAVOURITE = 0b0000_0001;
|
||||
|
||||
/// THe `m.lowpriority` tag.
|
||||
const LOW_PRIORITY = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
trait OptionExt {
|
||||
@@ -317,6 +357,8 @@ impl Default for BaseRoomInfo {
|
||||
tombstone: None,
|
||||
topic: None,
|
||||
rtc_member: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -520,7 +562,11 @@ impl RoomMemberships {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{calculate_room_name, DisplayName};
|
||||
use std::ops::Not;
|
||||
|
||||
use ruma::events::tag::{TagInfo, TagName, Tags};
|
||||
|
||||
use super::{calculate_room_name, BaseRoomInfo, DisplayName, RoomNotableTags};
|
||||
|
||||
#[test]
|
||||
fn test_calculate_room_name() {
|
||||
@@ -554,4 +600,34 @@ mod tests {
|
||||
actual = calculate_room_name(1, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_favourite() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::Favorite, TagInfo::default());
|
||||
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_notable_tags_low_priority() {
|
||||
let mut base_room_info = BaseRoomInfo::default();
|
||||
|
||||
let mut tags = Tags::new();
|
||||
tags.insert(TagName::LowPriority, TagInfo::default());
|
||||
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
|
||||
tags.clear();
|
||||
base_room_info.handle_notable_tags(&tags);
|
||||
assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::sync::RwLock as SyncRwLock;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
mem,
|
||||
sync::Arc,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
@@ -34,6 +34,7 @@ use ruma::{
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
receipt::{Receipt, ReceiptThread, ReceiptType},
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
encryption::RoomEncryptionEventContent,
|
||||
guest_access::GuestAccess,
|
||||
history_visibility::HistoryVisibility,
|
||||
@@ -53,11 +54,12 @@ use ruma::{
|
||||
RoomId, RoomVersionId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, field::debug, info, instrument, trace, warn};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, field::debug, info, instrument, warn};
|
||||
|
||||
use super::{
|
||||
members::{MemberInfo, MemberRoomInfo},
|
||||
BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent, RoomMember,
|
||||
members::MemberRoomInfo, BaseRoomInfo, DisplayName, RoomCreateWithCreatorEventContent,
|
||||
RoomMember, RoomNotableTags,
|
||||
};
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use crate::latest_event::LatestEvent;
|
||||
@@ -69,6 +71,20 @@ use crate::{
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomMemberships,
|
||||
};
|
||||
|
||||
/// A summary of changes to room information.
|
||||
///
|
||||
/// It also indicates whether this update should update the room list.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoomInfoUpdate {
|
||||
/// The room which was updated.
|
||||
pub room_id: OwnedRoomId,
|
||||
/// Whether this event should trigger the room list to update.
|
||||
///
|
||||
/// If the change is minor or if another action already causes the room list
|
||||
/// to update, this should be false to avoid duplicate updates.
|
||||
pub trigger_room_list_update: bool,
|
||||
}
|
||||
|
||||
/// The underlying room data structure collecting state for joined, left and
|
||||
/// invited rooms.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -76,6 +92,7 @@ pub struct Room {
|
||||
room_id: OwnedRoomId,
|
||||
own_user_id: OwnedUserId,
|
||||
inner: SharedObservable<RoomInfo>,
|
||||
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
|
||||
store: Arc<DynStateStore>,
|
||||
|
||||
/// The most recent few encrypted events. When the keys come through to
|
||||
@@ -133,23 +150,27 @@ impl From<&MembershipState> for RoomState {
|
||||
|
||||
impl Room {
|
||||
/// The size of the latest_encrypted_events RingBuffer
|
||||
// SAFETY: `new_unchecked` is safe because 10 is not zero.
|
||||
#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))]
|
||||
const MAX_ENCRYPTED_EVENTS: usize = 10;
|
||||
const MAX_ENCRYPTED_EVENTS: std::num::NonZeroUsize =
|
||||
unsafe { std::num::NonZeroUsize::new_unchecked(10) };
|
||||
|
||||
pub(crate) fn new(
|
||||
own_user_id: &UserId,
|
||||
store: Arc<DynStateStore>,
|
||||
room_id: &RoomId,
|
||||
room_state: RoomState,
|
||||
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
|
||||
) -> Self {
|
||||
let room_info = RoomInfo::new(room_id, room_state);
|
||||
Self::restore(own_user_id, store, room_info)
|
||||
Self::restore(own_user_id, store, room_info, roominfo_update_sender)
|
||||
}
|
||||
|
||||
pub(crate) fn restore(
|
||||
own_user_id: &UserId,
|
||||
store: Arc<DynStateStore>,
|
||||
room_info: RoomInfo,
|
||||
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
|
||||
) -> Self {
|
||||
Self {
|
||||
own_user_id: own_user_id.into(),
|
||||
@@ -160,6 +181,7 @@ impl Room {
|
||||
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
|
||||
Self::MAX_ENCRYPTED_EVENTS,
|
||||
))),
|
||||
roominfo_update_sender,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +218,11 @@ impl Room {
|
||||
self.inner.read().read_receipts.num_unread
|
||||
}
|
||||
|
||||
/// Get the detailed information about read receipts for the room.
|
||||
pub fn read_receipts(&self) -> RoomReadReceipts {
|
||||
self.inner.read().read_receipts.clone()
|
||||
}
|
||||
|
||||
/// Get the number of unread notifications (computed client-side).
|
||||
///
|
||||
/// This might be more precise than [`Self::unread_notification_counts`] for
|
||||
@@ -335,6 +362,12 @@ impl Room {
|
||||
self.inner.read().base_info.dm_targets.clone()
|
||||
}
|
||||
|
||||
/// If this room is a direct message, returns the number of members that
|
||||
/// we're sharing the room with.
|
||||
pub fn direct_targets_length(&self) -> usize {
|
||||
self.inner.read().base_info.dm_targets.len()
|
||||
}
|
||||
|
||||
/// Is the room encrypted.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.inner.read().is_encrypted()
|
||||
@@ -514,10 +547,7 @@ impl Room {
|
||||
for event in member_events {
|
||||
let profile = profiles.remove(event.user_id());
|
||||
let presence = presences.remove(event.user_id());
|
||||
|
||||
let member_info = MemberInfo { event, profile, presence };
|
||||
|
||||
members.push(RoomMember::from_parts(member_info, &room_info))
|
||||
members.push(RoomMember::from_parts(event, profile, presence, &room_info))
|
||||
}
|
||||
|
||||
Ok(members)
|
||||
@@ -630,10 +660,17 @@ impl Room {
|
||||
self.inner.get()
|
||||
}
|
||||
|
||||
/// Update the inner summary with the given RoomInfo, and notify
|
||||
/// subscribers.
|
||||
pub fn set_room_info(&self, room_info: RoomInfo) {
|
||||
/// Update the summary with given RoomInfo.
|
||||
///
|
||||
/// This also triggers an update for room info observers if
|
||||
/// `trigger_room_list_update` is true.
|
||||
pub fn set_room_info(&self, room_info: RoomInfo, trigger_room_list_update: bool) {
|
||||
self.inner.set(room_info);
|
||||
|
||||
// Ignore error if no receiver exists.
|
||||
let _ = self
|
||||
.roominfo_update_sender
|
||||
.send(RoomInfoUpdate { room_id: self.room_id.clone(), trigger_room_list_update });
|
||||
}
|
||||
|
||||
/// Get the `RoomMember` with the given `user_id`.
|
||||
@@ -641,30 +678,23 @@ impl Room {
|
||||
/// Returns `None` if the member was never part of this room, otherwise
|
||||
/// return a `RoomMember` that can be in a joined, invited, left, banned
|
||||
/// state.
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
|
||||
trace!("Fetching member event");
|
||||
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
|
||||
debug!("Member event not found in state store");
|
||||
debug!(%user_id, "Member event not found in state store");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
trace!("Deserializing member event");
|
||||
let event = raw_event.deserialize()?;
|
||||
|
||||
trace!("Fetching presence event");
|
||||
let presence =
|
||||
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
|
||||
|
||||
trace!("Fetching profile");
|
||||
let profile = self.store.get_profile(self.room_id(), user_id).await?;
|
||||
|
||||
let display_names = [event.display_name().to_owned()];
|
||||
let room_info = self.member_room_info(&display_names).await?;
|
||||
|
||||
trace!("Got all member information");
|
||||
let member_info = MemberInfo { event, profile, presence };
|
||||
Ok(Some(RoomMember::from_parts(member_info, &room_info)))
|
||||
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
|
||||
}
|
||||
|
||||
/// The current `MemberRoomInfo` for this room.
|
||||
@@ -675,18 +705,15 @@ impl Room {
|
||||
let max_power_level = self.max_power_level();
|
||||
let room_creator = self.inner.read().creator().map(ToOwned::to_owned);
|
||||
|
||||
trace!("Fetching power levels");
|
||||
let power_levels = self
|
||||
.store
|
||||
.get_state_event_static(self.room_id())
|
||||
.await?
|
||||
.and_then(|e| e.deserialize().ok());
|
||||
|
||||
trace!("Fetching users based on display names");
|
||||
let users_display_names =
|
||||
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
|
||||
|
||||
trace!("Fetching ignored users");
|
||||
let ignored_users = self
|
||||
.store
|
||||
.get_account_data_event_static::<IgnoredUserListEventContent>()
|
||||
@@ -718,6 +745,21 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the room is marked as favourite.
|
||||
///
|
||||
/// A room is considered favourite if it has received the `m.favourite` tag.
|
||||
pub fn is_favourite(&self) -> bool {
|
||||
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
|
||||
}
|
||||
|
||||
/// Check whether the room is marked as low priority.
|
||||
///
|
||||
/// A room is considered low priority if it has received the `m.lowpriority`
|
||||
/// tag.
|
||||
pub fn is_low_priority(&self) -> bool {
|
||||
self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
|
||||
}
|
||||
|
||||
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
|
||||
/// `receipt_type`, `thread` and `user_id` in this room.
|
||||
pub async fn load_user_receipt(
|
||||
@@ -742,6 +784,12 @@ impl Room {
|
||||
.get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a boolean indicating if this room has been manually marked as
|
||||
/// unread
|
||||
pub fn is_marked_unread(&self) -> bool {
|
||||
self.inner.read().base_info.is_marked_unread
|
||||
}
|
||||
}
|
||||
|
||||
/// The underlying pure data structure for joined and left rooms.
|
||||
@@ -787,6 +835,12 @@ pub struct RoomInfo {
|
||||
/// Base room info which holds some basic event contents important for the
|
||||
/// room state.
|
||||
pub(crate) base_info: Box<BaseRoomInfo>,
|
||||
|
||||
/// Did we already warn about an unknown room version in
|
||||
/// [`RoomInfo::room_version_or_default`]? This is done to avoid
|
||||
/// spamming about unknown room versions in the log for the same room.
|
||||
#[serde(skip)]
|
||||
pub(crate) warned_about_unknown_room_version: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -822,6 +876,7 @@ impl RoomInfo {
|
||||
latest_event: None,
|
||||
read_receipts: Default::default(),
|
||||
base_info: Box::new(BaseRoomInfo::new()),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,7 +993,7 @@ impl RoomInfo {
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
if let Some(latest_event) = &mut self.latest_event {
|
||||
trace!("Checking if redaction applies to latest event");
|
||||
tracing::trace!("Checking if redaction applies to latest event");
|
||||
if latest_event.event_id().as_deref() == Some(redacts) {
|
||||
match apply_redaction(&latest_event.event().event, _raw, room_version) {
|
||||
Some(redacted) => {
|
||||
@@ -964,6 +1019,16 @@ impl RoomInfo {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Update the room avatar
|
||||
pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
|
||||
self.base_info.avatar = url.map(|url| {
|
||||
let mut content = RoomAvatarEventContent::new();
|
||||
content.url = Some(url);
|
||||
|
||||
MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
|
||||
});
|
||||
}
|
||||
|
||||
/// Update the notifications count
|
||||
pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
|
||||
self.notification_counts = notification_counts;
|
||||
@@ -1037,6 +1102,26 @@ impl RoomInfo {
|
||||
self.base_info.room_version()
|
||||
}
|
||||
|
||||
/// Get the room version of this room, or a sensible default.
|
||||
///
|
||||
/// Will warn (at most once) if the room creation event is missing from this
|
||||
/// [`RoomInfo`].
|
||||
pub fn room_version_or_default(&self) -> RoomVersionId {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
self.base_info.room_version().cloned().unwrap_or_else(|| {
|
||||
if self
|
||||
.warned_about_unknown_room_version
|
||||
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
warn!("Unknown room version, falling back to v10");
|
||||
}
|
||||
|
||||
RoomVersionId::V10
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the room type of this room.
|
||||
pub fn room_type(&self) -> Option<&RoomType> {
|
||||
match self.base_info.create.as_ref()? {
|
||||
@@ -1140,6 +1225,12 @@ impl RoomInfo {
|
||||
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
|
||||
self.active_room_call_memberships().iter().map(|(user_id, _)| user_id.clone()).collect()
|
||||
}
|
||||
|
||||
/// Returns the latest (decrypted) event recorded for this room.
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
pub fn latest_event(&self) -> Option<&LatestEvent> {
|
||||
self.latest_event.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
@@ -1230,10 +1321,9 @@ impl RoomStateFilter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
ops::Sub,
|
||||
ops::{Not, Sub},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use assign::assign;
|
||||
@@ -1262,6 +1352,8 @@ mod tests {
|
||||
user_id, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use stream_assert::{assert_pending, assert_ready};
|
||||
use web_time::{Duration, SystemTime};
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use super::SyncInfo;
|
||||
@@ -1270,12 +1362,12 @@ mod tests {
|
||||
use crate::latest_event::LatestEvent;
|
||||
use crate::{
|
||||
store::{MemoryStore, StateChanges, StateStore},
|
||||
DisplayName, MinimalStateEvent, OriginalMinimalStateEvent,
|
||||
BaseClient, DisplayName, MinimalStateEvent, OriginalMinimalStateEvent, SessionMeta,
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn room_info_serialization() {
|
||||
fn test_room_info_serialization() {
|
||||
// This test exists to make sure we don't accidentally change the
|
||||
// serialized format for `RoomInfo`.
|
||||
|
||||
@@ -1303,6 +1395,7 @@ mod tests {
|
||||
))),
|
||||
base_info: Box::new(BaseRoomInfo::new()),
|
||||
read_receipts: Default::default(),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
};
|
||||
|
||||
let info_json = json!({
|
||||
@@ -1337,6 +1430,7 @@ mod tests {
|
||||
"encryption": null,
|
||||
"guest_access": null,
|
||||
"history_visibility": null,
|
||||
"is_marked_unread": false,
|
||||
"join_rules": null,
|
||||
"max_power_level": 100,
|
||||
"name": null,
|
||||
@@ -1347,7 +1441,8 @@ mod tests {
|
||||
"num_unread": 0,
|
||||
"num_mentions": 0,
|
||||
"num_notifications": 0,
|
||||
"latest_read_receipt_event_id": null,
|
||||
"latest_active": null,
|
||||
"pending": []
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1356,7 +1451,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn room_info_deserialization_without_optional_items() {
|
||||
fn test_room_info_deserialization_without_optional_items() {
|
||||
// Ensure we can still deserialize RoomInfos before we added things to its
|
||||
// schema
|
||||
|
||||
@@ -1422,12 +1517,153 @@ mod tests {
|
||||
assert!(info.base_info.topic.is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_favourite() {
|
||||
// Given a room,
|
||||
let client = BaseClient::new();
|
||||
|
||||
client
|
||||
.set_session_meta(SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// Sanity checks to ensure the room isn't marked as favourite.
|
||||
assert!(room.is_favourite().not());
|
||||
|
||||
// Subscribe to the `RoomInfo`.
|
||||
let mut room_info_subscriber = room.subscribe_info();
|
||||
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// Create the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {
|
||||
"m.favourite": {
|
||||
"order": 0.0
|
||||
},
|
||||
},
|
||||
},
|
||||
"type": "m.tag",
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
|
||||
// When the new tag is handled and applied.
|
||||
let mut changes = StateChanges::default();
|
||||
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
|
||||
client.apply_changes(&changes, false);
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as favourite.
|
||||
assert!(room.is_favourite());
|
||||
|
||||
// Now, let's remove the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {},
|
||||
},
|
||||
"type": "m.tag"
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
|
||||
client.apply_changes(&changes, false);
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as _not_ favourite.
|
||||
assert!(room.is_favourite().not());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_is_low_priority() {
|
||||
// Given a room,
|
||||
let client = BaseClient::new();
|
||||
|
||||
client
|
||||
.set_session_meta(SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let room = client.get_or_create_room(room_id, RoomState::Joined);
|
||||
|
||||
// Sanity checks to ensure the room isn't marked as low priority.
|
||||
assert!(!room.is_low_priority());
|
||||
|
||||
// Subscribe to the `RoomInfo`.
|
||||
let mut room_info_subscriber = room.subscribe_info();
|
||||
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// Create the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {
|
||||
"m.lowpriority": {
|
||||
"order": 0.0
|
||||
},
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
|
||||
// When the new tag is handled and applied.
|
||||
let mut changes = StateChanges::default();
|
||||
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
|
||||
client.apply_changes(&changes, false);
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as low priority.
|
||||
assert!(room.is_low_priority());
|
||||
|
||||
// Now, let's remove the tag.
|
||||
let tag_raw = Raw::new(&json!({
|
||||
"content": {
|
||||
"tags": {},
|
||||
},
|
||||
"type": "m.tag"
|
||||
}))
|
||||
.unwrap()
|
||||
.cast();
|
||||
client.handle_room_account_data(room_id, &[tag_raw], &mut changes).await;
|
||||
client.apply_changes(&changes, false);
|
||||
|
||||
// The `RoomInfo` is getting notified.
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// The room is now marked as _not_ low priority.
|
||||
assert!(room.is_low_priority().not());
|
||||
}
|
||||
|
||||
fn make_room(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
|
||||
let store = Arc::new(MemoryStore::new());
|
||||
let user_id = user_id!("@me:example.org");
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
(store.clone(), Room::new(user_id, store, room_id, room_type))
|
||||
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
|
||||
}
|
||||
|
||||
fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
|
||||
@@ -1459,13 +1695,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_for_joined_room_is_empty_if_no_info() {
|
||||
async fn test_display_name_for_joined_room_is_empty_if_no_info() {
|
||||
let (_, room) = make_room(RoomState::Joined);
|
||||
assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_for_joined_room_uses_canonical_alias_if_available() {
|
||||
async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
|
||||
let (_, room) = make_room(RoomState::Joined);
|
||||
room.inner
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
@@ -1473,7 +1709,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_for_joined_room_prefers_name_over_alias() {
|
||||
async fn test_display_name_for_joined_room_prefers_name_over_alias() {
|
||||
let (_, room) = make_room(RoomState::Joined);
|
||||
room.inner
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
@@ -1484,13 +1720,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_for_invited_room_is_empty_if_no_info() {
|
||||
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
|
||||
let (_, room) = make_room(RoomState::Invited);
|
||||
assert_eq!(room.display_name().await.unwrap(), DisplayName::Empty);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_for_invited_room_is_empty_if_room_name_empty() {
|
||||
async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
|
||||
let (_, room) = make_room(RoomState::Invited);
|
||||
|
||||
let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
|
||||
@@ -1503,7 +1739,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_for_invited_room_uses_canonical_alias_if_available() {
|
||||
async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
|
||||
let (_, room) = make_room(RoomState::Invited);
|
||||
room.inner
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
@@ -1511,7 +1747,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_for_invited_room_prefers_name_over_alias() {
|
||||
async fn test_display_name_for_invited_room_prefers_name_over_alias() {
|
||||
let (_, room) = make_room(RoomState::Invited);
|
||||
room.inner
|
||||
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
|
||||
@@ -1668,7 +1904,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_the_name_on_room_info_creates_a_fake_event() {
|
||||
fn test_setting_the_name_on_room_info_creates_a_fake_event() {
|
||||
// Given a room
|
||||
let mut room_info = RoomInfo::new(room_id!("!r:e.uk"), RoomState::Joined);
|
||||
|
||||
@@ -1689,10 +1925,11 @@ mod tests {
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_update() {
|
||||
// Given a room,
|
||||
let client = crate::BaseClient::new();
|
||||
|
||||
let client = BaseClient::new();
|
||||
|
||||
client
|
||||
.set_session_meta(crate::SessionMeta {
|
||||
.set_session_meta(SessionMeta {
|
||||
user_id: user_id!("@alice:example.org").into(),
|
||||
device_id: ruma::device_id!("AYEAYEAYE").into(),
|
||||
})
|
||||
@@ -1717,21 +1954,20 @@ mod tests {
|
||||
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
|
||||
|
||||
// The subscriber isn't notified at this point.
|
||||
stream_assert::assert_pending!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
|
||||
// Then updating the room info will store the event,
|
||||
client.apply_changes(&changes);
|
||||
client.apply_changes(&changes, false);
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
|
||||
// And wake up the subscriber.
|
||||
use futures_util::FutureExt as _;
|
||||
assert!(room_info_subscriber.next().now_or_never().is_some());
|
||||
stream_assert::assert_pending!(room_info_subscriber);
|
||||
assert_ready!(room_info_subscriber);
|
||||
assert_pending!(room_info_subscriber);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
|
||||
fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
|
||||
// Given a room with an encrypted event
|
||||
let (_store, room) = make_room(RoomState::Joined);
|
||||
add_encrypted_event(&room, "$A");
|
||||
@@ -1742,7 +1978,7 @@ mod tests {
|
||||
let event = make_latest_event("$A");
|
||||
let mut changes = StateChanges::default();
|
||||
room.on_latest_event_decrypted(event.clone(), 0, &mut changes);
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap(), false);
|
||||
|
||||
// Then is it stored
|
||||
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
|
||||
@@ -1750,7 +1986,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
|
||||
fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
|
||||
// Given a room with some encrypted events and a latest event
|
||||
let (_store, room) = make_room(RoomState::Joined);
|
||||
room.inner.update(|info| info.latest_event = Some(make_latest_event("$A")));
|
||||
@@ -1764,7 +2000,7 @@ mod tests {
|
||||
let new_event_index = 1;
|
||||
let mut changes = StateChanges::default();
|
||||
room.on_latest_event_decrypted(new_event.clone(), new_event_index, &mut changes);
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap(), false);
|
||||
|
||||
// Then the encrypted events list is shortened to only newer events
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
@@ -1778,7 +2014,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
fn replacing_the_newest_event_leaves_none_left() {
|
||||
fn test_replacing_the_newest_event_leaves_none_left() {
|
||||
// Given a room with some encrypted events
|
||||
let (_store, room) = make_room(RoomState::Joined);
|
||||
add_encrypted_event(&room, "$0");
|
||||
@@ -1791,7 +2027,7 @@ mod tests {
|
||||
let new_event_index = 3;
|
||||
let mut changes = StateChanges::default();
|
||||
room.on_latest_event_decrypted(new_event, new_event_index, &mut changes);
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap());
|
||||
room.set_room_info(changes.room_infos.get(room.room_id()).cloned().unwrap(), false);
|
||||
|
||||
// Then the encrypted events list ie empty
|
||||
let enc_evs = room.latest_encrypted_events();
|
||||
@@ -1900,7 +2136,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_correct_active_call_state() {
|
||||
fn test_show_correct_active_call_state() {
|
||||
let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
|
||||
// This check also tests the ordering.
|
||||
@@ -1914,7 +2150,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_call_is_false_when_everyone_left() {
|
||||
fn test_active_call_is_false_when_everyone_left() {
|
||||
let room = create_call_with_member_events_for_user(&ALICE, &BOB, &CAROL);
|
||||
|
||||
let b_empty_membership = call_member_state_event(Vec::new(), "$1234_1", &BOB);
|
||||
|
||||
@@ -20,16 +20,13 @@ use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use ruma::events::AnyToDeviceEvent;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
push::get_notifications::v3::Notification,
|
||||
sync::sync_events::{
|
||||
v3::{self, InvitedRoom, RoomSummary},
|
||||
v4,
|
||||
},
|
||||
api::client::sync::sync_events::{
|
||||
v3::{self, InvitedRoom, RoomSummary},
|
||||
v4,
|
||||
},
|
||||
events::{AnySyncStateEvent, AnySyncTimelineEvent},
|
||||
events::{AnyRoomAccountDataEvent, AnySyncStateEvent, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
OwnedRoomId, RoomId,
|
||||
JsOption, OwnedRoomId, RoomId,
|
||||
};
|
||||
use tracing::{instrument, trace, warn};
|
||||
|
||||
@@ -39,12 +36,11 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use crate::RoomMemberships;
|
||||
use crate::{
|
||||
deserialized_responses::AmbiguityChanges,
|
||||
error::Result,
|
||||
read_receipts::{compute_notifications, PreviousEventsProvider},
|
||||
read_receipts::{compute_unread_counts, PreviousEventsProvider},
|
||||
rooms::RoomState,
|
||||
store::{ambiguity_map::AmbiguityCache, StateChanges, Store},
|
||||
sync::{JoinedRoom, LeftRoom, Rooms, SyncResponse},
|
||||
sync::{JoinedRoomUpdate, LeftRoomUpdate, Notification, RoomUpdates, SyncResponse},
|
||||
Room, RoomInfo,
|
||||
};
|
||||
|
||||
@@ -100,7 +96,7 @@ impl BaseClient {
|
||||
|
||||
trace!("ready to submit changes to store");
|
||||
self.store.save_changes(&changes).await?;
|
||||
self.apply_changes(&changes);
|
||||
self.apply_changes(&changes, true);
|
||||
trace!("applied changes");
|
||||
|
||||
Ok(to_device)
|
||||
@@ -153,15 +149,16 @@ impl BaseClient {
|
||||
self.handle_account_data(&account_data.global, &mut changes).await;
|
||||
}
|
||||
|
||||
let mut new_rooms = Rooms::default();
|
||||
let mut new_rooms = RoomUpdates::default();
|
||||
let mut notifications = Default::default();
|
||||
let mut rooms_account_data = account_data.rooms.clone();
|
||||
|
||||
for (room_id, response_room_data) in rooms {
|
||||
let (room_info, joined_room, left_room, invited_room) = self
|
||||
.process_sliding_sync_room(
|
||||
room_id,
|
||||
response_room_data,
|
||||
account_data,
|
||||
&mut rooms_account_data,
|
||||
&store,
|
||||
&mut changes,
|
||||
&mut notifications,
|
||||
@@ -207,7 +204,7 @@ impl BaseClient {
|
||||
new_rooms
|
||||
.join
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(JoinedRoom::default)
|
||||
.or_insert_with(JoinedRoomUpdate::default)
|
||||
.ephemeral
|
||||
.push(raw.clone().cast());
|
||||
}
|
||||
@@ -217,11 +214,34 @@ impl BaseClient {
|
||||
new_rooms
|
||||
.join
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(JoinedRoom::default)
|
||||
.or_insert_with(JoinedRoomUpdate::default)
|
||||
.ephemeral
|
||||
.push(raw.clone().cast());
|
||||
}
|
||||
|
||||
// Handle room account data
|
||||
for (room_id, raw) in &rooms_account_data {
|
||||
self.handle_room_account_data(room_id, raw, &mut changes).await;
|
||||
|
||||
if let Some(room) = self.store.get_room(room_id) {
|
||||
match room.state() {
|
||||
RoomState::Joined => new_rooms
|
||||
.join
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(JoinedRoomUpdate::default)
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Left => new_rooms
|
||||
.leave
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(LeftRoomUpdate::default)
|
||||
.account_data
|
||||
.append(&mut raw.to_vec()),
|
||||
RoomState::Invited => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rooms in `new_rooms.join` either have a timeline update, or a new read
|
||||
// receipt. Update the read receipt accordingly.
|
||||
let user_id = &self.session_meta().expect("logged in user").user_id;
|
||||
@@ -233,14 +253,18 @@ impl BaseClient {
|
||||
.cloned()
|
||||
.or_else(|| self.get_room(room_id).map(|r| r.clone_info()))
|
||||
{
|
||||
if compute_notifications(
|
||||
let prev_read_receipts = room_info.read_receipts.clone();
|
||||
|
||||
compute_unread_counts(
|
||||
user_id,
|
||||
room_id,
|
||||
changes.receipts.get(room_id),
|
||||
previous_events_provider,
|
||||
previous_events_provider.for_room(room_id),
|
||||
&joined_room_update.timeline.events,
|
||||
&mut room_info.read_receipts,
|
||||
)? {
|
||||
);
|
||||
|
||||
if prev_read_receipts != room_info.read_receipts {
|
||||
changes.add_room(room_info);
|
||||
}
|
||||
}
|
||||
@@ -268,12 +292,11 @@ impl BaseClient {
|
||||
|
||||
trace!("ready to submit changes to store");
|
||||
store.save_changes(&changes).await?;
|
||||
self.apply_changes(&changes);
|
||||
self.apply_changes(&changes, false);
|
||||
trace!("applied changes");
|
||||
|
||||
Ok(SyncResponse {
|
||||
rooms: new_rooms,
|
||||
ambiguity_changes: AmbiguityChanges { changes: ambiguity_cache.changes },
|
||||
notifications,
|
||||
// FIXME not yet supported by sliding sync.
|
||||
presence: Default::default(),
|
||||
@@ -287,26 +310,29 @@ impl BaseClient {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
room_data: &v4::SlidingSyncRoom,
|
||||
account_data: &v4::AccountData,
|
||||
rooms_account_data: &mut BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
|
||||
store: &Store,
|
||||
changes: &mut StateChanges,
|
||||
notifications: &mut BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
ambiguity_cache: &mut AmbiguityCache,
|
||||
) -> Result<(RoomInfo, Option<JoinedRoom>, Option<LeftRoom>, Option<InvitedRoom>)> {
|
||||
let mut state_events = Self::deserialize_state_events(&room_data.required_state);
|
||||
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
|
||||
) -> Result<(RoomInfo, Option<JoinedRoomUpdate>, Option<LeftRoomUpdate>, Option<InvitedRoom>)>
|
||||
{
|
||||
let (raw_state_events, state_events): (Vec<_>, Vec<_>) = {
|
||||
let mut state_events = Vec::new();
|
||||
|
||||
let (raw_state_events, state_events): (Vec<_>, Vec<_>) = state_events.into_iter().unzip();
|
||||
// Read state events from the `required_state` field.
|
||||
state_events.extend(Self::deserialize_state_events(&room_data.required_state));
|
||||
|
||||
// Read state events from the `timeline` field.
|
||||
state_events.extend(Self::deserialize_state_events_from_timeline(&room_data.timeline));
|
||||
|
||||
state_events.into_iter().unzip()
|
||||
};
|
||||
|
||||
// Find or create the room in the store
|
||||
#[allow(unused_mut)] // Required for some feature flag combinations
|
||||
let (mut room, mut room_info, invited_room) = self.process_sliding_sync_room_membership(
|
||||
room_data,
|
||||
&state_events,
|
||||
store,
|
||||
room_id,
|
||||
changes,
|
||||
);
|
||||
let (mut room, mut room_info, invited_room) =
|
||||
self.process_sliding_sync_room_membership(room_data, &state_events, store, room_id);
|
||||
|
||||
room_info.mark_state_partially_synced();
|
||||
|
||||
@@ -323,17 +349,22 @@ impl BaseClient {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let room_account_data = if let Some(events) = account_data.rooms.get(room_id) {
|
||||
self.handle_room_account_data(room_id, events, changes).await;
|
||||
Some(events.to_vec())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let push_rules = self.get_push_rules(changes).await?;
|
||||
|
||||
if let Some(invite_state) = &room_data.invite_state {
|
||||
self.handle_invited_state(
|
||||
&room,
|
||||
invite_state,
|
||||
&push_rules,
|
||||
&mut room_info,
|
||||
changes,
|
||||
notifications,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
process_room_properties(room_data, &mut room_info);
|
||||
|
||||
let push_rules = self.get_push_rules(changes).await?;
|
||||
|
||||
let timeline = self
|
||||
.handle_timeline(
|
||||
&room,
|
||||
@@ -375,6 +406,9 @@ impl BaseClient {
|
||||
let notification_count = room_data.unread_notifications.clone().into();
|
||||
room_info.update_notification_count(notification_count);
|
||||
|
||||
let ambiguity_changes = ambiguity_cache.changes.remove(room_id).unwrap_or_default();
|
||||
let room_account_data = rooms_account_data.get(room_id).cloned();
|
||||
|
||||
match room_info.state() {
|
||||
RoomState::Joined => {
|
||||
// Ephemeral events are added separately, because we might not
|
||||
@@ -384,12 +418,13 @@ impl BaseClient {
|
||||
|
||||
Ok((
|
||||
room_info,
|
||||
Some(JoinedRoom::new(
|
||||
Some(JoinedRoomUpdate::new(
|
||||
timeline,
|
||||
raw_state_events,
|
||||
room_account_data.unwrap_or_default(),
|
||||
ephemeral,
|
||||
notification_count,
|
||||
ambiguity_changes,
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
@@ -399,10 +434,11 @@ impl BaseClient {
|
||||
RoomState::Left => Ok((
|
||||
room_info,
|
||||
None,
|
||||
Some(LeftRoom::new(
|
||||
Some(LeftRoomUpdate::new(
|
||||
timeline,
|
||||
raw_state_events,
|
||||
room_account_data.unwrap_or_default(),
|
||||
ambiguity_changes,
|
||||
)),
|
||||
None,
|
||||
)),
|
||||
@@ -422,10 +458,13 @@ impl BaseClient {
|
||||
state_events: &[AnySyncStateEvent],
|
||||
store: &Store,
|
||||
room_id: &RoomId,
|
||||
changes: &mut StateChanges,
|
||||
) -> (Room, RoomInfo, Option<InvitedRoom>) {
|
||||
if let Some(invite_state) = &room_data.invite_state {
|
||||
let room = store.get_or_create_room(room_id, RoomState::Invited);
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Invited,
|
||||
self.roominfo_update_sender.clone(),
|
||||
);
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// We don't actually know what events are inside invite_state. In theory, they
|
||||
@@ -441,15 +480,17 @@ impl BaseClient {
|
||||
// no content at all.
|
||||
room_info.mark_as_invited();
|
||||
|
||||
self.handle_invited_state(invite_state.as_slice(), &mut room_info, changes);
|
||||
|
||||
(
|
||||
room,
|
||||
room_info,
|
||||
Some(v3::InvitedRoom::from(v3::InviteState::from(invite_state.clone()))),
|
||||
)
|
||||
} else {
|
||||
let room = store.get_or_create_room(room_id, RoomState::Joined);
|
||||
let room = store.get_or_create_room(
|
||||
room_id,
|
||||
RoomState::Joined,
|
||||
self.roominfo_update_sender.clone(),
|
||||
);
|
||||
let mut room_info = room.clone_info();
|
||||
|
||||
// We default to considering this room joined if it's not an invite. If it's
|
||||
@@ -487,7 +528,7 @@ impl BaseClient {
|
||||
if let AnySyncStateEvent::RoomMember(member) = &event {
|
||||
// If this event updates the current user's membership, record that in the
|
||||
// room_info.
|
||||
if member.sender() == meta.user_id && member.state_key() == meta.user_id.as_str() {
|
||||
if member.state_key() == meta.user_id.as_str() {
|
||||
room_info.set_state(member.membership().into());
|
||||
break;
|
||||
}
|
||||
@@ -544,8 +585,10 @@ async fn cache_latest_events(
|
||||
for event in events.iter().rev() {
|
||||
if let Ok(timeline_event) = event.event.deserialize() {
|
||||
match is_suitable_for_latest_event(&timeline_event) {
|
||||
PossibleLatestEvent::YesRoomMessage(_) | PossibleLatestEvent::YesPoll(_) => {
|
||||
// m.room.message or m.poll.start - we found one! Store it.
|
||||
PossibleLatestEvent::YesRoomMessage(_)
|
||||
| PossibleLatestEvent::YesPoll(_)
|
||||
| PossibleLatestEvent::YesCallInvite(_) => {
|
||||
// We found a suitable latest event. Store it.
|
||||
|
||||
// In order to make the latest event fast to read, we want to keep the
|
||||
// associated sender in cache. This is a best-effort to gather enough
|
||||
@@ -639,10 +682,25 @@ async fn cache_latest_events(
|
||||
}
|
||||
|
||||
fn process_room_properties(room_data: &v4::SlidingSyncRoom, room_info: &mut RoomInfo) {
|
||||
// Handle the room's name.
|
||||
if let Some(name) = &room_data.name {
|
||||
room_info.update_name(name.to_owned());
|
||||
}
|
||||
|
||||
// Handle the room's avatar.
|
||||
//
|
||||
// It can be updated via the state events, or via the `SlidingSyncRoom::avatar`
|
||||
// field. This part of the code handles the latter case. The former case is
|
||||
// handled by [`BaseClient::handle_state`].
|
||||
match &room_data.avatar {
|
||||
// A new avatar!
|
||||
JsOption::Some(avatar_uri) => room_info.update_avatar(Some(avatar_uri.to_owned())),
|
||||
// Avatar must be removed.
|
||||
JsOption::Null => room_info.update_avatar(None),
|
||||
// Nothing to do.
|
||||
JsOption::Undefined => {}
|
||||
}
|
||||
|
||||
// Sliding sync doesn't have a room summary, nevertheless it contains the joined
|
||||
// and invited member counts. It likely will never have a heroes concept since
|
||||
// it calculates the room display name for us.
|
||||
@@ -672,7 +730,7 @@ mod tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
api::client::sync::sync_events::{v4, UnreadNotificationsCount},
|
||||
assign, device_id, event_id,
|
||||
assign, event_id,
|
||||
events::{
|
||||
direct::DirectEventContent,
|
||||
room::{
|
||||
@@ -686,16 +744,18 @@ mod tests {
|
||||
},
|
||||
mxc_uri, room_alias_id, room_id,
|
||||
serde::Raw,
|
||||
uint, user_id, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
|
||||
uint, user_id, JsOption, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::cache_latest_events;
|
||||
use crate::{store::MemoryStore, BaseClient, Room, RoomState, SessionMeta};
|
||||
use crate::{
|
||||
store::MemoryStore, test_utils::logged_in_base_client, BaseClient, Room, RoomState,
|
||||
};
|
||||
|
||||
#[async_test]
|
||||
async fn test_notification_count_set() {
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
|
||||
let mut response = v4::Response::new("42".to_owned());
|
||||
let room_id = room_id!("!room:example.org");
|
||||
@@ -724,16 +784,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn can_process_empty_sliding_sync_response() {
|
||||
let client = logged_in_client().await;
|
||||
async fn test_can_process_empty_sliding_sync_response() {
|
||||
let client = logged_in_base_client(None).await;
|
||||
let empty_response = v4::Response::new("5".to_owned());
|
||||
client.process_sliding_sync(&empty_response, &()).await.expect("Failed to process sync");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn room_with_unspecified_state_is_added_to_client_and_joined_list() {
|
||||
async fn test_room_with_unspecified_state_is_added_to_client_and_joined_list() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// When I send sliding sync response containing a room (with identifiable data
|
||||
@@ -751,15 +811,15 @@ mod tests {
|
||||
assert_eq!(client_room.state(), RoomState::Joined);
|
||||
|
||||
// And it is added to the list of joined rooms only.
|
||||
assert!(sync_resp.rooms.join.get(room_id).is_some());
|
||||
assert!(sync_resp.rooms.leave.get(room_id).is_none());
|
||||
assert!(sync_resp.rooms.invite.get(room_id).is_none());
|
||||
assert!(sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.invite.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn room_name_is_found_when_processing_sliding_sync_response() {
|
||||
async fn test_room_name_is_found_when_processing_sliding_sync_response() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// When I send sliding sync response containing a room with a name
|
||||
@@ -775,15 +835,15 @@ mod tests {
|
||||
assert_eq!(client_room.state(), RoomState::Joined);
|
||||
|
||||
// And it is added to the list of joined rooms only.
|
||||
assert!(sync_resp.rooms.join.get(room_id).is_some());
|
||||
assert!(sync_resp.rooms.leave.get(room_id).is_none());
|
||||
assert!(sync_resp.rooms.invite.get(room_id).is_none());
|
||||
assert!(sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.invite.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn invited_room_name_is_found_when_processing_sliding_sync_response() {
|
||||
async fn test_invited_room_name_is_found_when_processing_sliding_sync_response() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@w:e.uk");
|
||||
|
||||
@@ -801,15 +861,15 @@ mod tests {
|
||||
assert_eq!(client_room.state(), RoomState::Invited);
|
||||
|
||||
// And it is added to the list of invited rooms only.
|
||||
assert!(sync_resp.rooms.join.get(room_id).is_none());
|
||||
assert!(sync_resp.rooms.leave.get(room_id).is_none());
|
||||
assert!(sync_resp.rooms.invite.get(room_id).is_some());
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(sync_resp.rooms.invite.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn left_a_room_from_required_state_event() {
|
||||
async fn test_left_a_room_from_required_state_event() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
@@ -830,16 +890,53 @@ mod tests {
|
||||
// The room is left.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
|
||||
// And it is added to the list of invited rooms only.
|
||||
assert!(sync_resp.rooms.join.get(room_id).is_none());
|
||||
assert!(sync_resp.rooms.leave.get(room_id).is_some());
|
||||
assert!(sync_resp.rooms.invite.get(room_id).is_none());
|
||||
// And it is added to the list of left rooms only.
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.invite.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn left_a_room_from_timeline_state_event() {
|
||||
async fn test_kick_or_ban_updates_room_to_left() {
|
||||
for membership in [MembershipState::Leave, MembershipState::Ban] {
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_a_id = user_id!("@a:e.uk");
|
||||
let user_b_id = user_id!("@b:e.uk");
|
||||
let client = logged_in_base_client(Some(user_a_id)).await;
|
||||
|
||||
// When I join…
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
set_room_joined(&mut room, user_a_id);
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
|
||||
|
||||
// And then get kicked/banned with a `required_state` state event…
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
room.required_state.push(make_state_event(
|
||||
user_b_id,
|
||||
user_a_id.as_str(),
|
||||
RoomMemberEventContent::new(membership),
|
||||
None,
|
||||
));
|
||||
let response = response_with_room(room_id, room).await;
|
||||
let sync_resp =
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// The room is left.
|
||||
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
|
||||
|
||||
// And it is added to the list of left rooms only.
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
assert!(sync_resp.rooms.leave.contains_key(room_id));
|
||||
assert!(!sync_resp.rooms.invite.contains_key(room_id));
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_left_a_room_from_timeline_state_event() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
@@ -861,11 +958,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn can_be_reinvited_to_a_left_room() {
|
||||
async fn test_can_be_reinvited_to_a_left_room() {
|
||||
// See https://github.com/matrix-org/matrix-rust-sdk/issues/1834
|
||||
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
@@ -896,13 +993,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn other_person_leaving_a_dm_is_reflected_in_their_membership_and_direct_targets() {
|
||||
async fn test_other_person_leaving_a_dm_is_reflected_in_their_membership_and_direct_targets() {
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_a_id = user_id!("@a:e.uk");
|
||||
let user_b_id = user_id!("@b:e.uk");
|
||||
|
||||
// Given we have a DM with B, who is joined
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
|
||||
|
||||
// (Sanity: B is a direct target, and is in Join state)
|
||||
@@ -920,14 +1017,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn other_person_refusing_invite_to_a_dm_is_reflected_in_their_membership_and_direct_targets(
|
||||
async fn test_other_person_refusing_invite_to_a_dm_is_reflected_in_their_membership_and_direct_targets(
|
||||
) {
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_a_id = user_id!("@a:e.uk");
|
||||
let user_b_id = user_id!("@b:e.uk");
|
||||
|
||||
// Given I have invited B to a DM
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
|
||||
|
||||
// (Sanity: B is a direct target, and is in Invite state)
|
||||
@@ -945,13 +1042,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn members_count_in_a_dm_where_other_person_has_joined() {
|
||||
async fn test_members_count_in_a_dm_where_other_person_has_joined() {
|
||||
let room_id = room_id!("!r:bar.org");
|
||||
let user_a_id = user_id!("@a:bar.org");
|
||||
let user_b_id = user_id!("@b:bar.org");
|
||||
|
||||
// Given we have a DM with B, who is joined
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
|
||||
|
||||
// (Sanity: A is in Join state)
|
||||
@@ -969,13 +1066,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn members_count_in_a_dm_where_other_person_is_invited() {
|
||||
async fn test_members_count_in_a_dm_where_other_person_is_invited() {
|
||||
let room_id = room_id!("!r:bar.org");
|
||||
let user_a_id = user_id!("@a:bar.org");
|
||||
let user_b_id = user_id!("@b:bar.org");
|
||||
|
||||
// Given we have a DM with B, who is joined
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
|
||||
|
||||
// (Sanity: A is in Join state)
|
||||
@@ -993,9 +1090,89 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn avatar_is_found_when_processing_sliding_sync_response() {
|
||||
async fn test_avatar_is_found_when_processing_sliding_sync_response() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// When I send sliding sync response containing a room with an avatar
|
||||
let room = {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
room.avatar = JsOption::from_option(Some(mxc_uri!("mxc://e.uk/med1").to_owned()));
|
||||
|
||||
room
|
||||
};
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room in the client has the avatar
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(
|
||||
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
|
||||
"med1"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_avatar_can_be_unset_when_processing_sliding_sync_response() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
|
||||
// Set the avatar.
|
||||
|
||||
// When I send sliding sync response containing a room with an avatar
|
||||
let room = {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
room.avatar = JsOption::from_option(Some(mxc_uri!("mxc://e.uk/med1").to_owned()));
|
||||
|
||||
room
|
||||
};
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room in the client has the avatar
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(
|
||||
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
|
||||
"med1"
|
||||
);
|
||||
|
||||
// No avatar. Still here.
|
||||
|
||||
// When I send sliding sync response containing no avatar.
|
||||
let room = v4::SlidingSyncRoom::new();
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room in the client still has the avatar
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert_eq!(
|
||||
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
|
||||
"med1"
|
||||
);
|
||||
|
||||
// Avatar is unset.
|
||||
|
||||
// When I send sliding sync response containing an avatar set to `null` (!).
|
||||
let room = {
|
||||
let mut room = v4::SlidingSyncRoom::new();
|
||||
room.avatar = JsOption::Null;
|
||||
|
||||
room
|
||||
};
|
||||
let response = response_with_room(room_id, room).await;
|
||||
client.process_sliding_sync(&response, &()).await.expect("Failed to process sync");
|
||||
|
||||
// Then the room in the client has no more avatar
|
||||
let client_room = client.get_room(room_id).expect("No room found");
|
||||
assert!(client_room.avatar_url().is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_avatar_is_found_from_required_state_when_processing_sliding_sync_response() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
@@ -1013,9 +1190,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn invitation_room_is_added_to_client_and_invite_list() {
|
||||
async fn test_invitation_room_is_added_to_client_and_invite_list() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
@@ -1033,13 +1210,13 @@ mod tests {
|
||||
|
||||
// And it is added to the list of invited rooms, not the joined ones
|
||||
assert!(!sync_resp.rooms.invite[room_id].invite_state.is_empty());
|
||||
assert!(sync_resp.rooms.join.get(room_id).is_none());
|
||||
assert!(!sync_resp.rooms.join.contains_key(room_id));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn avatar_is_found_in_invitation_room_when_processing_sliding_sync_response() {
|
||||
async fn test_avatar_is_found_in_invitation_room_when_processing_sliding_sync_response() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
|
||||
@@ -1058,9 +1235,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn canonical_alias_is_found_in_invitation_room_when_processing_sliding_sync_response() {
|
||||
async fn test_canonical_alias_is_found_in_invitation_room_when_processing_sliding_sync_response(
|
||||
) {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
let room_alias_id = room_alias_id!("#myroom:e.uk");
|
||||
@@ -1077,9 +1255,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn display_name_from_sliding_sync_overrides_alias() {
|
||||
async fn test_display_name_from_sliding_sync_overrides_alias() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let user_id = user_id!("@u:e.uk");
|
||||
let room_alias_id = room_alias_id!("#myroom:e.uk");
|
||||
@@ -1100,9 +1278,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn last_event_from_sliding_sync_is_cached() {
|
||||
async fn test_last_event_from_sliding_sync_is_cached() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let event_a = json!({
|
||||
"sender":"@alice:example.com",
|
||||
@@ -1134,9 +1312,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn cached_latest_event_can_be_redacted() {
|
||||
async fn test_cached_latest_event_can_be_redacted() {
|
||||
// Given a logged-in client
|
||||
let client = logged_in_client().await;
|
||||
let client = logged_in_base_client(None).await;
|
||||
let room_id = room_id!("!r:e.uk");
|
||||
let event_a = json!({
|
||||
"sender": "@alice:example.com",
|
||||
@@ -1187,14 +1365,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn when_no_events_we_dont_cache_any() {
|
||||
async fn test_when_no_events_we_dont_cache_any() {
|
||||
let events = &[];
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
assert!(chosen.is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn when_only_one_event_we_cache_it() {
|
||||
async fn test_when_only_one_event_we_cache_it() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let events = &[event1.clone()];
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
@@ -1202,7 +1380,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn with_multiple_events_we_cache_the_last_one() {
|
||||
async fn test_with_multiple_events_we_cache_the_last_one() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
let events = &[event1, event2.clone()];
|
||||
@@ -1211,7 +1389,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() {
|
||||
async fn test_cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() {
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
let event3 = make_event("m.room.powerlevels", "$3");
|
||||
@@ -1222,7 +1400,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn prefer_to_cache_nothing_rather_than_irrelevant_events() {
|
||||
async fn test_prefer_to_cache_nothing_rather_than_irrelevant_events() {
|
||||
let event1 = make_event("m.room.power_levels", "$1");
|
||||
let events = &[event1];
|
||||
let chosen = choose_event_to_cache(events).await;
|
||||
@@ -1230,7 +1408,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn cache_encrypted_events_that_are_after_latest_message() {
|
||||
async fn test_cache_encrypted_events_that_are_after_latest_message() {
|
||||
// Given two message events followed by two encrypted
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
@@ -1249,7 +1427,7 @@ mod tests {
|
||||
rawev_id(event2.clone())
|
||||
);
|
||||
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
assert_eq!(
|
||||
ev_id(room.latest_event().map(|latest_event| latest_event.event().clone())),
|
||||
rawev_id(event2)
|
||||
@@ -1260,7 +1438,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn dont_cache_encrypted_events_that_are_before_latest_message() {
|
||||
async fn test_dont_cache_encrypted_events_that_are_before_latest_message() {
|
||||
// Given an encrypted event before and after the message
|
||||
let event1 = make_encrypted_event("$1");
|
||||
let event2 = make_event("m.room.message", "$2");
|
||||
@@ -1271,7 +1449,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
|
||||
// The latest message is stored
|
||||
assert_eq!(
|
||||
@@ -1284,7 +1462,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn skip_irrelevant_events_eg_receipts_even_if_after_message() {
|
||||
async fn test_skip_irrelevant_events_eg_receipts_even_if_after_message() {
|
||||
// Given two message events followed by two encrypted, with a receipt in the
|
||||
// middle
|
||||
let event1 = make_event("m.room.message", "$1");
|
||||
@@ -1298,7 +1476,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
|
||||
// The latest message is stored, ignoring the receipt
|
||||
assert_eq!(
|
||||
@@ -1311,7 +1489,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn only_store_the_max_number_of_encrypted_events() {
|
||||
async fn test_only_store_the_max_number_of_encrypted_events() {
|
||||
// Given two message events followed by lots of encrypted and other irrelevant
|
||||
// events
|
||||
let evente = make_event("m.room.message", "$e");
|
||||
@@ -1351,7 +1529,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
|
||||
// The latest message is stored, ignoring encrypted and receipts
|
||||
assert_eq!(
|
||||
@@ -1369,7 +1547,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn dont_overflow_capacity_if_previous_encrypted_events_exist() {
|
||||
async fn test_dont_overflow_capacity_if_previous_encrypted_events_exist() {
|
||||
// Given a RoomInfo with lots of encrypted events already inside it
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
@@ -1392,7 +1570,7 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
|
||||
// Sanity: room_info has 10 encrypted events inside it
|
||||
assert_eq!(room.latest_encrypted_events.read().unwrap().len(), 10);
|
||||
@@ -1401,7 +1579,7 @@ mod tests {
|
||||
let eventa = make_encrypted_event("$a");
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, &[eventa], None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
|
||||
// The oldest event is gone
|
||||
assert!(!rawevs_ids(&room.latest_encrypted_events).contains(&"$0".to_owned()));
|
||||
@@ -1411,7 +1589,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn existing_encrypted_events_are_deleted_if_we_receive_unencrypted() {
|
||||
async fn test_existing_encrypted_events_are_deleted_if_we_receive_unencrypted() {
|
||||
// Given a RoomInfo with some encrypted events already inside it
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
@@ -1423,13 +1601,13 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
room.set_room_info(room_info.clone());
|
||||
room.set_room_info(room_info.clone(), false);
|
||||
|
||||
// When I ask to cache an unencrypted event, and some more encrypted events
|
||||
let eventa = make_event("m.room.message", "$a");
|
||||
let eventb = make_encrypted_event("$b");
|
||||
cache_latest_events(&room, &mut room_info, &[eventa, eventb], None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
|
||||
// The only encrypted events stored are the ones after the decrypted one
|
||||
assert_eq!(rawevs_ids(&room.latest_encrypted_events), &["$b"]);
|
||||
@@ -1442,7 +1620,7 @@ mod tests {
|
||||
let room = make_room();
|
||||
let mut room_info = room.clone_info();
|
||||
cache_latest_events(&room, &mut room_info, events, None, None).await;
|
||||
room.set_room_info(room_info);
|
||||
room.set_room_info(room_info, false);
|
||||
room.latest_event().map(|latest_event| latest_event.event().clone())
|
||||
}
|
||||
|
||||
@@ -1463,11 +1641,14 @@ mod tests {
|
||||
}
|
||||
|
||||
fn make_room() -> Room {
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
Room::new(
|
||||
user_id!("@u:e.co"),
|
||||
Arc::new(MemoryStore::new()),
|
||||
room_id!("!r:e.co"),
|
||||
RoomState::Joined,
|
||||
sender,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1587,18 +1768,6 @@ mod tests {
|
||||
.push(make_global_account_data_event(DirectEventContent(direct_content)));
|
||||
}
|
||||
|
||||
async fn logged_in_client() -> BaseClient {
|
||||
let client = BaseClient::new();
|
||||
client
|
||||
.set_session_meta(SessionMeta {
|
||||
user_id: user_id!("@u:e.uk").to_owned(),
|
||||
device_id: device_id!("XYZ").to_owned(),
|
||||
})
|
||||
.await
|
||||
.expect("Failed to set session meta");
|
||||
client
|
||||
}
|
||||
|
||||
async fn response_with_room(room_id: &RoomId, room: v4::SlidingSyncRoom) -> v4::Response {
|
||||
let mut response = v4::Response::new("5".to_owned());
|
||||
response.rooms.insert(room_id.to_owned(), room);
|
||||
|
||||
@@ -118,6 +118,7 @@ impl AmbiguityCache {
|
||||
self.update(room_id, old_map, new_map);
|
||||
|
||||
let change = AmbiguityChange {
|
||||
member_id: member_event.state_key().clone(),
|
||||
disambiguated_member,
|
||||
ambiguated_member,
|
||||
member_ambiguous: ambiguous,
|
||||
@@ -159,12 +160,9 @@ impl AmbiguityCache {
|
||||
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
|
||||
use MembershipState::*;
|
||||
|
||||
let old_event = if let Some(m) = changes
|
||||
.state
|
||||
.get(room_id)
|
||||
.and_then(|events| events.get(&StateEventType::RoomMember))
|
||||
.and_then(|m| m.get(member_event.state_key().as_str()))
|
||||
{
|
||||
let old_event = if let Some(m) = changes.state.get(room_id).and_then(|events| {
|
||||
events.get(&StateEventType::RoomMember)?.get(member_event.state_key().as_str())
|
||||
}) {
|
||||
Some(RawMemberEvent::Sync(m.clone().cast()))
|
||||
} else {
|
||||
self.store.get_member_event(room_id, member_event.state_key()).await?
|
||||
@@ -173,20 +171,15 @@ impl AmbiguityCache {
|
||||
// FIXME: Use let chains once stable
|
||||
let old_display_name = if let Some(Ok(event)) = old_event.map(|r| r.deserialize()) {
|
||||
if matches!(event.membership(), Join | Invite) {
|
||||
let display_name = if let Some(d) = changes
|
||||
.profiles
|
||||
.get(room_id)
|
||||
.and_then(|p| p.get(member_event.state_key()))
|
||||
.and_then(|p| p.as_original())
|
||||
.and_then(|p| p.content.displayname.as_deref())
|
||||
{
|
||||
let display_name = if let Some(d) = changes.profiles.get(room_id).and_then(|p| {
|
||||
p.get(member_event.state_key())?.as_original()?.content.displayname.as_deref()
|
||||
}) {
|
||||
Some(d.to_owned())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, member_event.state_key())
|
||||
.await?
|
||||
.and_then(|p| p.into_original())
|
||||
.and_then(|p| p.content.displayname)
|
||||
.and_then(|p| p.into_original()?.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
|
||||
@@ -41,8 +41,8 @@ use crate::{
|
||||
|
||||
/// `StateStore` integration tests.
|
||||
///
|
||||
/// This trait is not meant to be used directly, but will be used with the [``]
|
||||
/// macro.
|
||||
/// This trait is not meant to be used directly, but will be used with the
|
||||
/// [`statestore_integration_tests!`] macro.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait StateStoreIntegrationTests {
|
||||
@@ -74,6 +74,8 @@ pub trait StateStoreIntegrationTests {
|
||||
async fn test_stripped_non_stripped(&self) -> Result<()>;
|
||||
/// Test room removal.
|
||||
async fn test_room_removal(&self) -> Result<()>;
|
||||
/// Test profile removal.
|
||||
async fn test_profile_removal(&self) -> Result<()>;
|
||||
/// Test presence saving.
|
||||
async fn test_presence_saving(&self);
|
||||
/// Test display names saving.
|
||||
@@ -1058,6 +1060,77 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_profile_removal(&self) -> Result<()> {
|
||||
let room_id = room_id();
|
||||
|
||||
// Both the user id and invited user id get a profile in populate().
|
||||
let user_id = user_id();
|
||||
let invited_user_id = invited_user_id();
|
||||
|
||||
self.populate().await?;
|
||||
|
||||
let new_invite_member_json = json!({
|
||||
"content": {
|
||||
"avatar_url": "mxc://localhost/SEsfnsuifSDFSSEG",
|
||||
"displayname": "example after update",
|
||||
"membership": "invite",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSm:localhost",
|
||||
"origin_server_ts": 1432735824,
|
||||
"room_id": room_id,
|
||||
"sender": user_id,
|
||||
"state_key": invited_user_id,
|
||||
"type": "m.room.member",
|
||||
});
|
||||
let new_invite_member_event: SyncRoomMemberEvent =
|
||||
serde_json::from_value(new_invite_member_json.clone()).unwrap();
|
||||
|
||||
let mut changes = StateChanges {
|
||||
// Both get their profiles deleted…
|
||||
profiles_to_delete: [(
|
||||
room_id.to_owned(),
|
||||
vec![user_id.to_owned(), invited_user_id.to_owned()],
|
||||
)]
|
||||
.into(),
|
||||
|
||||
// …but the invited user get a new profile.
|
||||
profiles: {
|
||||
let mut map = BTreeMap::default();
|
||||
map.insert(
|
||||
room_id.to_owned(),
|
||||
[(invited_user_id.to_owned(), new_invite_member_event.into())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
map
|
||||
},
|
||||
|
||||
..StateChanges::default()
|
||||
};
|
||||
|
||||
let raw = serde_json::from_value::<Raw<AnySyncStateEvent>>(new_invite_member_json)
|
||||
.expect("can create sync-state-event for topic");
|
||||
let event = raw.deserialize().unwrap();
|
||||
changes.add_state_event(room_id, event, raw);
|
||||
|
||||
self.save_changes(&changes).await.unwrap();
|
||||
|
||||
// The profile for user has been removed.
|
||||
assert!(self.get_profile(room_id, user_id).await?.is_none());
|
||||
assert!(self.get_member_event(room_id, user_id).await?.is_some());
|
||||
|
||||
// The profile for the invited user has been updated.
|
||||
let invited_member_event = self.get_profile(room_id, invited_user_id).await?.unwrap();
|
||||
assert_eq!(
|
||||
invited_member_event.as_original().unwrap().content.displayname.as_deref(),
|
||||
Some("example after update")
|
||||
);
|
||||
assert!(self.get_member_event(room_id, invited_user_id).await?.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_presence_saving(&self) {
|
||||
let user_id = user_id();
|
||||
let second_user_id = user_id!("@second:localhost");
|
||||
@@ -1288,6 +1361,12 @@ macro_rules! statestore_integration_tests {
|
||||
store.test_room_removal().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_profile_removal() -> StoreResult<()> {
|
||||
let store = get_store().await?.into_state_store();
|
||||
store.test_profile_removal().await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_presence_saving() {
|
||||
let store = get_store().await.expect("creating store failed").into_state_store();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
num::NonZeroUsize,
|
||||
sync::RwLock as StdRwLock,
|
||||
};
|
||||
|
||||
@@ -45,8 +46,9 @@ use crate::{
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug)]
|
||||
pub struct MemoryStore {
|
||||
recently_visited_rooms: StdRwLock<HashMap<String, Vec<String>>>,
|
||||
user_avatar_url: StdRwLock<HashMap<String, String>>,
|
||||
sync_token: StdRwLock<Option<String>>,
|
||||
filters: StdRwLock<HashMap<String, String>>,
|
||||
@@ -82,10 +84,38 @@ pub struct MemoryStore {
|
||||
custom: StdRwLock<HashMap<Vec<u8>, Vec<u8>>>,
|
||||
}
|
||||
|
||||
// SAFETY: `new_unchecked` is safe because 20 is not zero.
|
||||
const NUMBER_OF_MEDIAS: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(20) };
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
recently_visited_rooms: Default::default(),
|
||||
user_avatar_url: Default::default(),
|
||||
sync_token: Default::default(),
|
||||
filters: Default::default(),
|
||||
account_data: Default::default(),
|
||||
profiles: Default::default(),
|
||||
display_names: Default::default(),
|
||||
members: Default::default(),
|
||||
room_info: Default::default(),
|
||||
room_state: Default::default(),
|
||||
room_account_data: Default::default(),
|
||||
stripped_room_state: Default::default(),
|
||||
stripped_members: Default::default(),
|
||||
presence: Default::default(),
|
||||
room_user_receipts: Default::default(),
|
||||
room_event_receipts: Default::default(),
|
||||
media: StdRwLock::new(RingBuffer::new(NUMBER_OF_MEDIAS)),
|
||||
custom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
/// Create a new empty MemoryStore
|
||||
pub fn new() -> Self {
|
||||
Self { media: StdRwLock::new(RingBuffer::new(20)), ..Default::default() }
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn get_user_room_receipt_event_impl(
|
||||
@@ -149,6 +179,13 @@ impl StateStore for MemoryStore {
|
||||
.get(user_id.as_str())
|
||||
.cloned()
|
||||
.map(StateStoreDataValue::UserAvatarUrl),
|
||||
StateStoreDataKey::RecentlyVisitedRooms(user_id) => self
|
||||
.recently_visited_rooms
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(user_id.as_str())
|
||||
.cloned()
|
||||
.map(StateStoreDataValue::RecentlyVisitedRooms),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -174,6 +211,14 @@ impl StateStore for MemoryStore {
|
||||
value.into_user_avatar_url().expect("Session data not a user avatar url"),
|
||||
);
|
||||
}
|
||||
StateStoreDataKey::RecentlyVisitedRooms(user_id) => {
|
||||
self.recently_visited_rooms.write().unwrap().insert(
|
||||
user_id.to_string(),
|
||||
value
|
||||
.into_recently_visited_rooms()
|
||||
.expect("Session data not a list of recently visited rooms"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -188,6 +233,9 @@ impl StateStore for MemoryStore {
|
||||
StateStoreDataKey::UserAvatarUrl(user_id) => {
|
||||
self.filters.write().unwrap().remove(user_id.as_str());
|
||||
}
|
||||
StateStoreDataKey::RecentlyVisitedRooms(user_id) => {
|
||||
self.recently_visited_rooms.write().unwrap().remove(user_id.as_str());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -199,14 +247,25 @@ impl StateStore for MemoryStore {
|
||||
*self.sync_token.write().unwrap() = Some(s.to_owned());
|
||||
}
|
||||
|
||||
for (room, users) in &changes.profiles {
|
||||
for (user_id, profile) in users {
|
||||
self.profiles
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(user_id.clone(), profile.clone());
|
||||
{
|
||||
let mut profiles = self.profiles.write().unwrap();
|
||||
|
||||
for (room, users) in &changes.profiles_to_delete {
|
||||
let Some(room_profiles) = profiles.get_mut(room) else {
|
||||
continue;
|
||||
};
|
||||
for user in users {
|
||||
room_profiles.remove(user);
|
||||
}
|
||||
}
|
||||
|
||||
for (room, users) in &changes.profiles {
|
||||
for (user_id, profile) in users {
|
||||
profiles
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(user_id.clone(), profile.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
//! Data migration helpers for StateStore implementations.
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "experimental-sliding-sync")]
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
@@ -44,7 +47,7 @@ use crate::{
|
||||
deserialized_responses::SyncOrStrippedState,
|
||||
rooms::{
|
||||
normal::{RoomSummary, SyncInfo},
|
||||
BaseRoomInfo,
|
||||
BaseRoomInfo, RoomNotableTags,
|
||||
},
|
||||
sync::UnreadNotificationsCount,
|
||||
MinimalStateEvent, OriginalMinimalStateEvent, RoomInfo, RoomState,
|
||||
@@ -120,6 +123,7 @@ impl RoomInfoV1 {
|
||||
latest_event: latest_event.map(|ev| Box::new(LatestEvent::new(ev))),
|
||||
read_receipts: Default::default(),
|
||||
base_info: base_info.migrate(create),
|
||||
warned_about_unknown_room_version: Arc::new(false.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +208,8 @@ impl BaseRoomInfoV1 {
|
||||
tombstone,
|
||||
topic,
|
||||
rtc_member: BTreeMap::new(),
|
||||
is_marked_unread: false,
|
||||
notable_tags: RoomNotableTags::empty(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +51,13 @@ use ruma::{
|
||||
serde::Raw,
|
||||
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
|
||||
/// BoxStream of owned Types
|
||||
pub type BoxStream<T> = Pin<Box<dyn futures_util::Stream<Item = T> + Send>>;
|
||||
|
||||
use crate::{
|
||||
rooms::{RoomInfo, RoomState},
|
||||
rooms::{normal::RoomInfoUpdate, RoomInfo, RoomState},
|
||||
MinimalRoomMemberEvent, Room, RoomStateFilter, SessionMeta,
|
||||
};
|
||||
|
||||
@@ -144,11 +144,8 @@ pub(crate) struct Store {
|
||||
pub(super) sync_token: Arc<RwLock<Option<String>>>,
|
||||
rooms: Arc<StdRwLock<BTreeMap<OwnedRoomId, Room>>>,
|
||||
/// A lock to synchronize access to the store, such that data by the sync is
|
||||
/// never overwritten. The sync processing is supposed to use write access,
|
||||
/// such that only it is currently accessing the store overall. Other things
|
||||
/// might acquire read access, such that access to different rooms can be
|
||||
/// parallelized.
|
||||
sync_lock: Arc<RwLock<()>>,
|
||||
/// never overwritten.
|
||||
sync_lock: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
@@ -164,7 +161,7 @@ impl Store {
|
||||
}
|
||||
|
||||
/// Get access to the syncing lock.
|
||||
pub fn sync_lock(&self) -> &RwLock<()> {
|
||||
pub fn sync_lock(&self) -> &Mutex<()> {
|
||||
&self.sync_lock
|
||||
}
|
||||
|
||||
@@ -174,9 +171,18 @@ impl Store {
|
||||
/// inner `StateStore`.
|
||||
///
|
||||
/// This method panics if it is called twice.
|
||||
pub async fn set_session_meta(&self, session_meta: SessionMeta) -> Result<()> {
|
||||
pub async fn set_session_meta(
|
||||
&self,
|
||||
session_meta: SessionMeta,
|
||||
roominfo_update_sender: &broadcast::Sender<RoomInfoUpdate>,
|
||||
) -> Result<()> {
|
||||
for info in self.inner.get_room_infos().await? {
|
||||
let room = Room::restore(&session_meta.user_id, self.inner.clone(), info);
|
||||
let room = Room::restore(
|
||||
&session_meta.user_id,
|
||||
self.inner.clone(),
|
||||
info,
|
||||
roominfo_update_sender.clone(),
|
||||
);
|
||||
self.rooms.write().unwrap().insert(room.room_id().to_owned(), room);
|
||||
}
|
||||
|
||||
@@ -217,7 +223,12 @@ impl Store {
|
||||
|
||||
/// Lookup the Room for the given RoomId, or create one, if it didn't exist
|
||||
/// yet in the store
|
||||
pub fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomState) -> Room {
|
||||
pub fn get_or_create_room(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
room_type: RoomState,
|
||||
roominfo_update_sender: broadcast::Sender<RoomInfoUpdate>,
|
||||
) -> Room {
|
||||
let user_id =
|
||||
&self.session_meta.get().expect("Creating room while not being logged in").user_id;
|
||||
|
||||
@@ -225,7 +236,9 @@ impl Store {
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, room_type))
|
||||
.or_insert_with(|| {
|
||||
Room::new(user_id, self.inner.clone(), room_id, room_type, roominfo_update_sender)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
@@ -264,6 +277,11 @@ pub struct StateChanges {
|
||||
/// `MinimalRoomMemberEvent`.
|
||||
pub profiles: BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, MinimalRoomMemberEvent>>,
|
||||
|
||||
/// A mapping of room profiles to delete.
|
||||
///
|
||||
/// These are deleted *before* other room profiles are inserted.
|
||||
pub profiles_to_delete: BTreeMap<OwnedRoomId, Vec<OwnedUserId>>,
|
||||
|
||||
/// A mapping of `RoomId` to a map of event type string to a state key and
|
||||
/// `AnySyncStateEvent`.
|
||||
pub state:
|
||||
|
||||
@@ -302,7 +302,8 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
/// * `key` - The key to fetch data for
|
||||
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Put arbitrary data into the custom store
|
||||
/// Put arbitrary data into the custom store, return the data previously
|
||||
/// stored
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -315,6 +316,27 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
value: Vec<u8>,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Put arbitrary data into the custom store, do not attempt to read any
|
||||
/// previous data
|
||||
///
|
||||
/// Optimization option for set_custom_values for stores that would perform
|
||||
/// better withouts the extra read and the caller not needing that data
|
||||
/// returned. Otherwise this just wraps around `set_custom_data` and
|
||||
/// discards the result.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to insert data into
|
||||
///
|
||||
/// * `value` - The value to insert
|
||||
async fn set_custom_value_no_read(
|
||||
&self,
|
||||
key: &[u8],
|
||||
value: Vec<u8>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.set_custom_value(key, value).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Remove arbitrary data from the custom store and return it if existed
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -783,6 +805,9 @@ pub enum StateStoreDataValue {
|
||||
|
||||
/// The user avatar url
|
||||
UserAvatarUrl(String),
|
||||
|
||||
/// A list of recently visited room identifiers for the current user
|
||||
RecentlyVisitedRooms(Vec<String>),
|
||||
}
|
||||
|
||||
impl StateStoreDataValue {
|
||||
@@ -800,6 +825,11 @@ impl StateStoreDataValue {
|
||||
pub fn into_user_avatar_url(self) -> Option<String> {
|
||||
as_variant!(self, Self::UserAvatarUrl)
|
||||
}
|
||||
|
||||
/// Get this value if it is a list of recently visited rooms.
|
||||
pub fn into_recently_visited_rooms(self) -> Option<Vec<String>> {
|
||||
as_variant!(self, Self::RecentlyVisitedRooms)
|
||||
}
|
||||
}
|
||||
|
||||
/// A key for key-value data.
|
||||
@@ -813,6 +843,9 @@ pub enum StateStoreDataKey<'a> {
|
||||
|
||||
/// Avatar URL
|
||||
UserAvatarUrl(&'a UserId),
|
||||
|
||||
/// Recently visited room identifiers
|
||||
RecentlyVisitedRooms(&'a UserId),
|
||||
}
|
||||
|
||||
impl StateStoreDataKey<'_> {
|
||||
@@ -823,4 +856,8 @@ impl StateStoreDataKey<'_> {
|
||||
/// Key prefix to use for the [`UserAvatarUrl`][Self::UserAvatarUrl]
|
||||
/// variant.
|
||||
pub const USER_AVATAR_URL: &'static str = "user_avatar_url";
|
||||
|
||||
/// Key prefix to use for the
|
||||
/// [`RecentlyVisitedRooms`][Self::RecentlyVisitedRooms] variant.
|
||||
pub const RECENTLY_VISITED_ROOMS: &'static str = "recently_visited_rooms";
|
||||
}
|
||||
|
||||
@@ -16,46 +16,41 @@
|
||||
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::SyncTimelineEvent;
|
||||
use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
push::get_notifications::v3::Notification,
|
||||
sync::sync_events::{
|
||||
v3::InvitedRoom, UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
api::client::sync::sync_events::{
|
||||
v3::InvitedRoom as InvitedRoomUpdate,
|
||||
UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
events::{
|
||||
presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
|
||||
AnySyncEphemeralRoomEvent, AnySyncStateEvent, AnyToDeviceEvent,
|
||||
},
|
||||
push::Action,
|
||||
serde::Raw,
|
||||
OwnedRoomId,
|
||||
OwnedEventId, OwnedRoomId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
debug::{
|
||||
DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId, DebugNotificationMap,
|
||||
},
|
||||
deserialized_responses::AmbiguityChanges,
|
||||
debug::{DebugInvitedRoom, DebugListOfRawEvents, DebugListOfRawEventsNoId},
|
||||
deserialized_responses::{AmbiguityChange, RawAnySyncOrStrippedTimelineEvent},
|
||||
};
|
||||
|
||||
/// Internal representation of a `/sync` response.
|
||||
/// Generalized representation of a `/sync` response.
|
||||
///
|
||||
/// This type is intended to be applicable regardless of the endpoint used for
|
||||
/// syncing.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SyncResponse {
|
||||
/// Updates to rooms.
|
||||
pub rooms: Rooms,
|
||||
pub rooms: RoomUpdates,
|
||||
/// Updates to the presence status of other users.
|
||||
pub presence: Vec<Raw<PresenceEvent>>,
|
||||
/// The global private data created by this user.
|
||||
pub account_data: Vec<Raw<AnyGlobalAccountDataEvent>>,
|
||||
/// Messages sent directly between devices.
|
||||
pub to_device: Vec<Raw<AnyToDeviceEvent>>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
/// New notifications per room.
|
||||
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
}
|
||||
@@ -67,37 +62,36 @@ impl fmt::Debug for SyncResponse {
|
||||
.field("rooms", &self.rooms)
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
.field("to_device", &DebugListOfRawEventsNoId(&self.to_device))
|
||||
.field("ambiguity_changes", &self.ambiguity_changes)
|
||||
.field("notifications", &DebugNotificationMap(&self.notifications))
|
||||
.field("notifications", &self.notifications)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates to rooms in a [`SyncResponse`].
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Rooms {
|
||||
pub struct RoomUpdates {
|
||||
/// The rooms that the user has left or been banned from.
|
||||
pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
|
||||
pub leave: BTreeMap<OwnedRoomId, LeftRoomUpdate>,
|
||||
/// The rooms that the user has joined.
|
||||
pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
|
||||
pub join: BTreeMap<OwnedRoomId, JoinedRoomUpdate>,
|
||||
/// The rooms that the user has been invited to.
|
||||
pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
|
||||
pub invite: BTreeMap<OwnedRoomId, InvitedRoomUpdate>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for Rooms {
|
||||
impl fmt::Debug for RoomUpdates {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Rooms")
|
||||
.field("leave", &self.leave)
|
||||
.field("join", &self.join)
|
||||
.field("invite", &DebugInvitedRooms(&self.invite))
|
||||
.field("invite", &DebugInvitedRoomUpdates(&self.invite))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates to joined rooms.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct JoinedRoom {
|
||||
pub struct JoinedRoomUpdate {
|
||||
/// Counts of unread notifications for this room.
|
||||
pub unread_notifications: UnreadNotificationsCount,
|
||||
/// The timeline of messages and state changes in the room.
|
||||
@@ -112,10 +106,15 @@ pub struct JoinedRoom {
|
||||
/// The ephemeral events in the room that aren't recorded in the timeline or
|
||||
/// state of the room. e.g. typing.
|
||||
pub ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
///
|
||||
/// This is a map of event ID of the `m.room.member` event to the
|
||||
/// details of the ambiguity change.
|
||||
pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for JoinedRoom {
|
||||
impl fmt::Debug for JoinedRoomUpdate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
.field("unread_notifications", &self.unread_notifications)
|
||||
@@ -123,19 +122,21 @@ impl fmt::Debug for JoinedRoom {
|
||||
.field("state", &DebugListOfRawEvents(&self.state))
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
.field("ephemeral", &self.ephemeral)
|
||||
.field("ambiguity_changes", &self.ambiguity_changes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl JoinedRoom {
|
||||
impl JoinedRoomUpdate {
|
||||
pub(crate) fn new(
|
||||
timeline: Timeline,
|
||||
state: Vec<Raw<AnySyncStateEvent>>,
|
||||
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
|
||||
ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
|
||||
unread_notifications: UnreadNotificationsCount,
|
||||
ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
) -> Self {
|
||||
Self { unread_notifications, timeline, state, account_data, ephemeral }
|
||||
Self { unread_notifications, timeline, state, account_data, ephemeral, ambiguity_changes }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +160,8 @@ impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
|
||||
}
|
||||
|
||||
/// Updates to left rooms.
|
||||
#[derive(Clone)]
|
||||
pub struct LeftRoom {
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LeftRoomUpdate {
|
||||
/// The timeline of messages and state changes in the room up to the point
|
||||
/// when the user left.
|
||||
pub timeline: Timeline,
|
||||
@@ -171,25 +172,32 @@ pub struct LeftRoom {
|
||||
pub state: Vec<Raw<AnySyncStateEvent>>,
|
||||
/// The private data that this user has attached to this room.
|
||||
pub account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
///
|
||||
/// This is a map of event ID of the `m.room.member` event to the
|
||||
/// details of the ambiguity change.
|
||||
pub ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
}
|
||||
|
||||
impl LeftRoom {
|
||||
impl LeftRoomUpdate {
|
||||
pub(crate) fn new(
|
||||
timeline: Timeline,
|
||||
state: Vec<Raw<AnySyncStateEvent>>,
|
||||
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
|
||||
ambiguity_changes: BTreeMap<OwnedEventId, AmbiguityChange>,
|
||||
) -> Self {
|
||||
Self { timeline, state, account_data }
|
||||
Self { timeline, state, account_data, ambiguity_changes }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for LeftRoom {
|
||||
impl fmt::Debug for LeftRoomUpdate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JoinedRoom")
|
||||
.field("timeline", &self.timeline)
|
||||
.field("state", &DebugListOfRawEvents(&self.state))
|
||||
.field("account_data", &DebugListOfRawEventsNoId(&self.account_data))
|
||||
.field("ambiguity_changes", &self.ambiguity_changes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -215,11 +223,36 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugInvitedRooms<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoom>);
|
||||
struct DebugInvitedRoomUpdates<'a>(&'a BTreeMap<OwnedRoomId, InvitedRoomUpdate>);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<'a> fmt::Debug for DebugInvitedRooms<'a> {
|
||||
impl<'a> fmt::Debug for DebugInvitedRoomUpdates<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map().entries(self.0.iter().map(|(k, v)| (k, DebugInvitedRoom(v)))).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A notification triggered by a sync response.
|
||||
#[derive(Clone)]
|
||||
pub struct Notification {
|
||||
/// The actions to perform when the conditions for this rule are met.
|
||||
pub actions: Vec<Action>,
|
||||
|
||||
/// The event that triggered the notification.
|
||||
pub event: RawAnySyncOrStrippedTimelineEvent,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for Notification {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let event_debug = match &self.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(ev) => DebugRawEvent(ev),
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(ev) => DebugRawEvent(ev.cast_ref()),
|
||||
};
|
||||
|
||||
f.debug_struct("Notification")
|
||||
.field("actions", &self.actions)
|
||||
.field("event", &event_debug)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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.
|
||||
|
||||
//! Testing utilities - DO NOT USE IN PRODUCTION.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use ruma::{owned_user_id, UserId};
|
||||
|
||||
use crate::{BaseClient, SessionMeta};
|
||||
|
||||
/// Create a [`BaseClient`] with the given user id, if provided, or an hardcoded
|
||||
/// one otherwise.
|
||||
pub(crate) async fn logged_in_base_client(user_id: Option<&UserId>) -> BaseClient {
|
||||
let client = BaseClient::new();
|
||||
let user_id =
|
||||
user_id.map(|user_id| user_id.to_owned()).unwrap_or_else(|| owned_user_id!("@u:e.uk"));
|
||||
client
|
||||
.set_session_meta(SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() })
|
||||
.await
|
||||
.expect("set_session_meta failed!");
|
||||
client
|
||||
}
|
||||
@@ -45,3 +45,6 @@ wasm-bindgen-test = "0.3.33"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
js-sys = "0.3.64"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
//! Abstraction over an executor so we can spawn tasks under WASM the same way
|
||||
//! we do usually.
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use std::convert::Infallible as JoinError;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use std::{
|
||||
future::Future,
|
||||
@@ -25,7 +23,12 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use futures_util::{future::RemoteHandle, FutureExt};
|
||||
pub use futures_util::future::Aborted as JoinError;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use futures_util::{
|
||||
future::{AbortHandle, Abortable, RemoteHandle},
|
||||
FutureExt,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::task::{spawn, JoinError, JoinHandle};
|
||||
|
||||
@@ -34,16 +37,31 @@ pub fn spawn<F, T>(future: F) -> JoinHandle<T>
|
||||
where
|
||||
F: Future<Output = T> + 'static,
|
||||
{
|
||||
let (fut, handle) = future.remote_handle();
|
||||
wasm_bindgen_futures::spawn_local(fut);
|
||||
let (future, remote_handle) = future.remote_handle();
|
||||
let (abort_handle, abort_registration) = AbortHandle::new_pair();
|
||||
let future = Abortable::new(future, abort_registration);
|
||||
|
||||
JoinHandle { handle }
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
// Poll the future, and ignore the result (either it's `Ok(())`, or it's
|
||||
// `Err(Aborted)`).
|
||||
let _ = future.await;
|
||||
});
|
||||
|
||||
JoinHandle { remote_handle, abort_handle }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug)]
|
||||
pub struct JoinHandle<T> {
|
||||
handle: RemoteHandle<T>,
|
||||
remote_handle: RemoteHandle<T>,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T> JoinHandle<T> {
|
||||
pub fn abort(&self) {
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -51,6 +69,37 @@ impl<T: 'static> Future for JoinHandle<T> {
|
||||
type Output = Result<T, JoinError>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.handle).poll(cx).map(Ok)
|
||||
if self.abort_handle.is_aborted() {
|
||||
// The future has been aborted. It is not possible to poll it again.
|
||||
Poll::Ready(Err(JoinError))
|
||||
} else {
|
||||
Pin::new(&mut self.remote_handle).poll(cx).map(Ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
use super::spawn;
|
||||
|
||||
#[async_test]
|
||||
async fn test_spawn() {
|
||||
let future = async { 42 };
|
||||
let join_handle = spawn(future);
|
||||
|
||||
assert_matches!(join_handle.await, Ok(42));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_abort() {
|
||||
let future = async { 42 };
|
||||
let join_handle = spawn(future);
|
||||
|
||||
join_handle.abort();
|
||||
|
||||
assert!(join_handle.await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@ use std::{
|
||||
vec_deque::{Drain, Iter},
|
||||
VecDeque,
|
||||
},
|
||||
num::NonZeroUsize,
|
||||
ops::RangeBounds,
|
||||
};
|
||||
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A simple fixed-size ring buffer implementation.
|
||||
///
|
||||
@@ -35,8 +36,8 @@ pub struct RingBuffer<T> {
|
||||
impl<T> RingBuffer<T> {
|
||||
/// Create a ring buffer with the supplied capacity, reserving it so we
|
||||
/// never need to reallocate.
|
||||
pub fn new(size: usize) -> Self {
|
||||
Self { inner: VecDeque::with_capacity(size) }
|
||||
pub fn new(size: NonZeroUsize) -> Self {
|
||||
Self { inner: VecDeque::with_capacity(size.into()) }
|
||||
}
|
||||
|
||||
/// Returns the number of items that are stored in this ring buffer.
|
||||
@@ -105,6 +106,14 @@ impl<T> RingBuffer<T> {
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.inner.capacity()
|
||||
}
|
||||
|
||||
/// Retains only the elements specified by the predicate.
|
||||
pub fn retain<F>(&mut self, predicate: F)
|
||||
where
|
||||
F: FnMut(&T) -> bool,
|
||||
{
|
||||
self.inner.retain(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
impl<U> Extend<U> for RingBuffer<U> {
|
||||
@@ -115,21 +124,15 @@ impl<U> Extend<U> for RingBuffer<U> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for RingBuffer<T> {
|
||||
fn default() -> Self {
|
||||
Self { inner: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Not;
|
||||
use std::{num::NonZeroUsize, ops::Not};
|
||||
|
||||
use super::RingBuffer;
|
||||
|
||||
#[test]
|
||||
pub fn test_fixed_size() {
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
|
||||
assert!(ring_buffer.is_empty());
|
||||
|
||||
@@ -163,7 +166,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
pub fn test_push_and_pop_and_remove_and_length() {
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
|
||||
ring_buffer.push(1);
|
||||
assert_eq!(ring_buffer.len(), 1);
|
||||
@@ -220,7 +223,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_drain() {
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
|
||||
ring_buffer.push(1);
|
||||
ring_buffer.push(2);
|
||||
@@ -241,7 +244,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_on_empty_buffer_is_a_noop() {
|
||||
let mut ring_buffer: RingBuffer<u8> = RingBuffer::new(3);
|
||||
let mut ring_buffer: RingBuffer<u8> = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.clear();
|
||||
assert_eq!(ring_buffer.len(), 0);
|
||||
}
|
||||
@@ -249,7 +252,7 @@ mod tests {
|
||||
#[test]
|
||||
fn clear_removes_all_items() {
|
||||
// Given a RingBuffer that has been used
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push(4);
|
||||
ring_buffer.push(5);
|
||||
ring_buffer.push(6);
|
||||
@@ -269,7 +272,7 @@ mod tests {
|
||||
#[test]
|
||||
fn clear_does_not_affect_capacity() {
|
||||
// Given a RingBuffer that has been used
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push(4);
|
||||
ring_buffer.push(5);
|
||||
ring_buffer.push(6);
|
||||
@@ -287,7 +290,7 @@ mod tests {
|
||||
#[test]
|
||||
fn capacity_is_what_we_passed_to_new() {
|
||||
// Given a RingBuffer
|
||||
let ring_buffer = RingBuffer::<i32>::new(13);
|
||||
let ring_buffer = RingBuffer::<i32>::new(NonZeroUsize::new(13).unwrap());
|
||||
// When I ask for its capacity I get what I provided at the start
|
||||
assert_eq!(ring_buffer.capacity(), 13);
|
||||
}
|
||||
@@ -295,7 +298,7 @@ mod tests {
|
||||
#[test]
|
||||
fn capacity_is_not_affected_by_overflowing() {
|
||||
// Given a RingBuffer that has been used
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push(4);
|
||||
ring_buffer.push(5);
|
||||
ring_buffer.push(6);
|
||||
@@ -317,7 +320,7 @@ mod tests {
|
||||
#[test]
|
||||
fn roundtrip_serialization() {
|
||||
// Given a RingBuffer
|
||||
let mut ring_buffer = RingBuffer::new(3);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(3).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -337,7 +340,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extending_an_empty_ringbuffer_adds_the_items() {
|
||||
// Given a RingBuffer
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
|
||||
// When I extend it
|
||||
ring_buffer.extend(vec!["a".to_owned(), "b".to_owned()]);
|
||||
@@ -349,7 +352,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extend_adds_items_to_the_end() {
|
||||
// Given a RingBuffer with something in it
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -366,7 +369,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extend_does_not_overflow_max_length() {
|
||||
// Given a RingBuffer with something in it
|
||||
let mut ring_buffer = RingBuffer::new(5);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(5).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -389,7 +392,7 @@ mod tests {
|
||||
#[test]
|
||||
fn extending_a_full_ringbuffer_preserves_max_length() {
|
||||
// Given a full RingBuffer with something in it
|
||||
let mut ring_buffer = RingBuffer::new(2);
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(2).unwrap());
|
||||
ring_buffer.push("1".to_owned());
|
||||
ring_buffer.push("2".to_owned());
|
||||
|
||||
@@ -405,4 +408,16 @@ mod tests {
|
||||
// Then only the last N items remain
|
||||
assert_eq!(ring_buffer.iter().map(String::as_str).collect::<Vec<_>>(), vec!["6", "7"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retain() {
|
||||
let mut ring_buffer = RingBuffer::new(NonZeroUsize::new(2).unwrap());
|
||||
ring_buffer.push(1);
|
||||
ring_buffer.push(2);
|
||||
|
||||
ring_buffer.retain(|v| v % 2 == 0);
|
||||
|
||||
assert_eq!(ring_buffer.len(), 1);
|
||||
assert_eq!(ring_buffer.get(0).copied().unwrap(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,61 @@
|
||||
# UNRELEASED
|
||||
|
||||
Changed:
|
||||
|
||||
- Fallback keys are rotated in a time-based manner, instead of waiting for the
|
||||
server to tell us that a fallback key got used.
|
||||
([#3151](https://github.com/matrix-org/matrix-rust-sdk/pull/3151))
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- Add a `backup_version` argument to `CryptoStore`'s
|
||||
`inbound_group_sessions_for_backup`,
|
||||
`mark_inbound_group_sessions_as_backed_up` and
|
||||
`inbound_group_session_counts` methods.
|
||||
([#3253](https://github.com/matrix-org/matrix-rust-sdk/pull/3253))
|
||||
|
||||
- Rename the `OlmMachine::invalidate_group_session` method to
|
||||
`OlmMachine::discard_room_key`
|
||||
|
||||
- Move `OlmMachine::export_room_keys` to `matrix_sdk_crypto::store::Store`.
|
||||
(Call it with `olm_machine.store().export_room_keys(...)`.)
|
||||
|
||||
- Add new `dehydrated` property to `olm::account::PickledAccount`.
|
||||
([#3164](https://github.com/matrix-org/matrix-rust-sdk/pull/3164))
|
||||
|
||||
Additions:
|
||||
|
||||
- Expose new method `OlmMachine::device_creation_time`.
|
||||
([#3275](https://github.com/matrix-org/matrix-rust-sdk/pull/3275))
|
||||
|
||||
- Log more details about the Olm session after encryption and decryption.
|
||||
([#3242](https://github.com/matrix-org/matrix-rust-sdk/pull/3242))
|
||||
|
||||
- When Olm message decryption fails, report the error code(s) from the failure.
|
||||
([#3212](https://github.com/matrix-org/matrix-rust-sdk/pull/3212))
|
||||
|
||||
- Expose new methods `OlmMachine::set_room_settings` and
|
||||
`OlmMachine::get_room_settings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Add new properties `session_rotation_period` and
|
||||
`session_rotation_period_msgs` to `store::RoomSettings`.
|
||||
([#3042](https://github.com/matrix-org/matrix-rust-sdk/pull/3042))
|
||||
|
||||
- Fix bug which caused `SecretStorageKey` to incorrectly reject secret storage
|
||||
keys whose metadata lacked check fields.
|
||||
([#3046](https://github.com/matrix-org/matrix-rust-sdk/pull/3046))
|
||||
|
||||
- Add new API `Device::encrypt_event_raw` that allows
|
||||
to encrypt an event to a specific device.
|
||||
([#3091](https://github.com/matrix-org/matrix-rust-sdk/pull/3091))
|
||||
|
||||
- Add new API `store::Store::export_room_keys_stream` that provides room
|
||||
keys on demand.
|
||||
|
||||
- Include event timestamps on logs from event decryption.
|
||||
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
|
||||
|
||||
# 0.7.0
|
||||
|
||||
- Add method to mark a list of inbound group sessions as backed up:
|
||||
|
||||
@@ -17,10 +17,12 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
[features]
|
||||
default = []
|
||||
automatic-room-key-forwarding = []
|
||||
js = ["ruma/js", "vodozemac/js"]
|
||||
js = ["ruma/js", "vodozemac/js", "matrix-sdk-common/js"]
|
||||
qrcode = ["dep:matrix-sdk-qrcode"]
|
||||
message-ids = ["dep:ulid"]
|
||||
experimental-algorithms = []
|
||||
uniffi = ["dep:uniffi"]
|
||||
_disable-minimum-rotation-period-ms = []
|
||||
|
||||
# Testing helpers for implementations based upon this
|
||||
testing = ["dep:http"]
|
||||
@@ -41,6 +43,7 @@ hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = { workspace = true, optional = true } # feature = testing only
|
||||
itertools = { workspace = true }
|
||||
js_option = "0.1.1"
|
||||
matrix-sdk-qrcode = { workspace = true, optional = true }
|
||||
matrix-sdk-common = { workspace = true }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
@@ -51,11 +54,13 @@ serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = "2.5.0"
|
||||
time = { version = "0.3.34", features = ["formatting"] }
|
||||
tokio-stream = { workspace = true, features = ["sync"] }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true, features = ["attributes"] }
|
||||
ulid = { version = "1.0.0", optional = true }
|
||||
uniffi = { workspace = true, optional = true }
|
||||
vodozemac = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
@@ -78,3 +83,6 @@ proptest = { version = "1.0.0", default-features = false, features = ["std"] }
|
||||
# required for async_test macro
|
||||
stream_assert = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -70,4 +70,6 @@ The following crate feature flags are available:
|
||||
|
||||
* `qrcode`: Enbles QRcode generation and reading code
|
||||
|
||||
* `testing`: provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
|
||||
* `testing`: Provides facilities and functions for tests, in particular for integration testing store implementations. ATTENTION: do not ever use outside of tests, we do not provide any stability warantees on these, these are merely helpers. If you find you _need_ any function provided here outside of tests, please open a Github Issue and inform us about your use case for us to consider.
|
||||
|
||||
* `_disable-minimum-rotation-period-ms`: Do not use except for testing. Disables the floor on the rotation period of room keys.
|
||||
|
||||
@@ -106,12 +106,12 @@ impl PkDecryption {
|
||||
|
||||
let keys = Keys::new(shared_secret);
|
||||
|
||||
let cipher = Aes256CbcDec::new(keys.aes_key(), keys.iv());
|
||||
let decrypted = cipher.decrypt_padded_vec_mut::<Pkcs7>(&message.ciphertext)?;
|
||||
|
||||
let hmac = keys.hmac();
|
||||
hmac.verify_truncated_left(&message.mac)?;
|
||||
|
||||
let cipher = Aes256CbcDec::new(keys.aes_key(), keys.iv());
|
||||
let decrypted = cipher.decrypt_padded_vec_mut::<Pkcs7>(&message.ciphertext)?;
|
||||
|
||||
Ok(decrypted)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ use std::{
|
||||
ops::DerefMut,
|
||||
};
|
||||
|
||||
use bs58;
|
||||
use ruma::api::client::backup::EncryptedSessionData;
|
||||
use thiserror::Error;
|
||||
use vodozemac::Curve25519PublicKey;
|
||||
@@ -45,7 +44,7 @@ pub enum DecodeError {
|
||||
/// The recovery key has an invalid length.
|
||||
#[error("The decoded recovery key has a invalid length: expected {0}, got {1}")]
|
||||
Length(usize, usize),
|
||||
/// The recovry key isn't valid base58.
|
||||
/// The recovery key isn't valid base58.
|
||||
#[error(transparent)]
|
||||
Base58(#[from] bs58::decode::Error),
|
||||
/// The recovery key isn't valid base64.
|
||||
|
||||
@@ -39,8 +39,7 @@ use crate::{
|
||||
olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
|
||||
store::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts, Store},
|
||||
types::{MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
|
||||
CryptoStoreError, Device, KeysBackupRequest, OutgoingRequest, RoomKeyImportResult,
|
||||
SignatureError,
|
||||
CryptoStoreError, Device, KeysBackupRequest, RoomKeyImportResult, SignatureError,
|
||||
};
|
||||
|
||||
mod keys;
|
||||
@@ -70,12 +69,6 @@ struct PendingBackup {
|
||||
sessions: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
|
||||
}
|
||||
|
||||
impl From<PendingBackup> for OutgoingRequest {
|
||||
fn from(b: PendingBackup) -> Self {
|
||||
OutgoingRequest { request_id: b.request_id, request: Arc::new(b.request.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a signature verification of a signed JSON object.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SignatureVerification {
|
||||
@@ -108,6 +101,7 @@ impl SignatureVerification {
|
||||
|
||||
/// The result of a signature check.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
|
||||
pub enum SignatureState {
|
||||
/// The signature is missing.
|
||||
#[default]
|
||||
@@ -403,7 +397,8 @@ impl BackupMachine {
|
||||
|
||||
/// Get the number of backed up room keys and the total number of room keys.
|
||||
pub async fn room_key_counts(&self) -> Result<RoomKeyCounts, CryptoStoreError> {
|
||||
self.store.inbound_group_session_counts().await
|
||||
let backup_version = self.backup_key.read().await.as_ref().and_then(|k| k.backup_version());
|
||||
self.store.inbound_group_session_counts(backup_version.as_deref()).await
|
||||
}
|
||||
|
||||
/// Disable and reset our backup state.
|
||||
@@ -482,7 +477,12 @@ impl BackupMachine {
|
||||
|
||||
trace!(request_id = ?r.request_id, keys = ?r.sessions, "Marking room keys as backed up");
|
||||
|
||||
self.store.mark_inbound_group_sessions_as_backed_up(&room_and_session_ids).await?;
|
||||
self.store
|
||||
.mark_inbound_group_sessions_as_backed_up(
|
||||
&r.request.version,
|
||||
&room_and_session_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
trace!(
|
||||
request_id = ?r.request_id,
|
||||
@@ -520,7 +520,7 @@ impl BackupMachine {
|
||||
};
|
||||
|
||||
let sessions =
|
||||
self.store.inbound_group_sessions_for_backup(Self::BACKUP_BATCH_SIZE).await?;
|
||||
self.store.inbound_group_sessions_for_backup(&version, Self::BACKUP_BATCH_SIZE).await?;
|
||||
|
||||
if sessions.is_empty() {
|
||||
trace!(?backup_key, "No room keys need to be backed up");
|
||||
@@ -625,6 +625,7 @@ mod tests {
|
||||
use ruma::{device_id, room_id, user_id, CanonicalJsonValue, DeviceId, RoomId, UserId};
|
||||
use serde_json::json;
|
||||
|
||||
use super::BackupMachine;
|
||||
use crate::{
|
||||
olm::BackedUpRoomKey, store::BackupDecryptionKey, types::RoomKeyBackupInfo, OlmError,
|
||||
OlmMachine,
|
||||
@@ -664,7 +665,10 @@ mod tests {
|
||||
|
||||
async fn backup_flow(machine: OlmMachine) -> Result<(), OlmError> {
|
||||
let backup_machine = machine.backup_machine();
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let backup_version = current_backup_version(backup_machine).await;
|
||||
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
|
||||
assert_eq!(counts.total, 0, "Initially no keys exist");
|
||||
assert_eq!(counts.backed_up, 0, "Initially no backed up keys exist");
|
||||
@@ -672,7 +676,8 @@ mod tests {
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id()).await?;
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id2()).await?;
|
||||
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
|
||||
assert_eq!(counts.backed_up, 0, "No room keys have been backed up yet");
|
||||
|
||||
@@ -691,8 +696,10 @@ mod tests {
|
||||
);
|
||||
|
||||
backup_machine.mark_request_as_sent(&request_id).await?;
|
||||
let backup_version = current_backup_version(backup_machine).await;
|
||||
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
assert_eq!(counts.total, 2);
|
||||
assert_eq!(counts.backed_up, 2, "All room keys have been backed up");
|
||||
|
||||
@@ -702,8 +709,10 @@ mod tests {
|
||||
);
|
||||
|
||||
backup_machine.disable_backup().await?;
|
||||
let backup_version = current_backup_version(backup_machine).await;
|
||||
|
||||
let counts = backup_machine.store.inbound_group_session_counts().await?;
|
||||
let counts =
|
||||
backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
|
||||
assert_eq!(counts.total, 2);
|
||||
assert_eq!(
|
||||
counts.backed_up, 0,
|
||||
@@ -713,6 +722,10 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn current_backup_version(backup_machine: &BackupMachine) -> Option<String> {
|
||||
backup_machine.backup_key.read().await.as_ref().and_then(|k| k.backup_version())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn memory_store_backups() -> Result<(), OlmError> {
|
||||
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
|
||||
|
||||
@@ -95,7 +95,7 @@ impl DehydratedDevices {
|
||||
let user_id = self.inner.user_id();
|
||||
let user_identity = self.inner.store().private_identity();
|
||||
|
||||
let account = Account::new(user_id);
|
||||
let account = Account::new_dehydrated(user_id);
|
||||
let store = Arc::new(CryptoStoreWrapper::new(user_id, MemoryStore::new()));
|
||||
|
||||
let verification_machine = VerificationMachine::new(
|
||||
@@ -318,7 +318,7 @@ impl DehydratedDevice {
|
||||
let mut transaction = self.store.transaction().await;
|
||||
|
||||
let account = transaction.account().await?;
|
||||
account.generate_fallback_key_helper();
|
||||
account.generate_fallback_key_if_needed();
|
||||
|
||||
let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload();
|
||||
|
||||
@@ -378,6 +378,7 @@ fn expand_pickle_key(key: &[u8; 32], device_id: &DeviceId) -> Box<[u8; 32]> {
|
||||
mod tests {
|
||||
use std::{collections::BTreeMap, iter};
|
||||
|
||||
use js_option::JsOption;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
api::client::keys::get_keys::v3::Response as KeysQueryResponse, assign,
|
||||
@@ -390,7 +391,7 @@ mod tests {
|
||||
create_session, get_prepared_machine_test_helper, to_device_requests_to_content,
|
||||
},
|
||||
olm::OutboundGroupSession,
|
||||
types::events::ToDeviceEvent,
|
||||
types::{events::ToDeviceEvent, DeviceKeys as DeviceKeysType},
|
||||
utilities::json_convert,
|
||||
EncryptionSettings, OlmMachine,
|
||||
};
|
||||
@@ -477,6 +478,13 @@ mod tests {
|
||||
!request.fallback_keys.is_empty(),
|
||||
"The dehydrated device creation request should contain some fallback keys"
|
||||
);
|
||||
|
||||
let device_keys: DeviceKeysType = request.device_keys.deserialize_as().unwrap();
|
||||
assert_eq!(
|
||||
device_keys.dehydrated,
|
||||
JsOption::Some(true),
|
||||
"The device keys of the dehydrated device should be marked as dehydrated."
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
|
||||
@@ -38,10 +38,6 @@ pub enum OlmError {
|
||||
#[error(transparent)]
|
||||
JsonError(#[from] SerdeError),
|
||||
|
||||
/// The event could not have been decrypted.
|
||||
#[error(transparent)]
|
||||
Decryption(#[from] vodozemac::olm::DecryptionError),
|
||||
|
||||
/// The received room key couldn't be converted into a valid Megolm session.
|
||||
#[error(transparent)]
|
||||
SessionCreation(#[from] SessionCreationError),
|
||||
@@ -263,11 +259,11 @@ pub enum SessionCreationError {
|
||||
)]
|
||||
InvalidSignature {
|
||||
/// The one-time key that failed the signature verification.
|
||||
one_time_key: SignedKey,
|
||||
one_time_key: Box<SignedKey>,
|
||||
/// The key that was used to verify the signature.
|
||||
signing_key: Option<Ed25519PublicKey>,
|
||||
signing_key: Option<Box<Ed25519PublicKey>>,
|
||||
/// The exact error describing why the signature verification failed.
|
||||
error: SignatureError,
|
||||
error: Box<SignatureError>,
|
||||
},
|
||||
|
||||
/// The user's device is missing a curve25519 key.
|
||||
@@ -289,3 +285,22 @@ pub enum SessionCreationError {
|
||||
#[error(transparent)]
|
||||
InboundCreation(#[from] vodozemac::olm::SessionCreationError),
|
||||
}
|
||||
|
||||
/// Errors that can be returned by
|
||||
/// [`crate::machine::OlmMachine::set_room_settings`].
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SetRoomSettingsError {
|
||||
/// The changes are rejected because they conflict with the previous
|
||||
/// settings for this room.
|
||||
#[error("the new settings would cause a downgrade of encryption security")]
|
||||
EncryptionDowngrade,
|
||||
|
||||
/// The changes are rejected because we would be unable to use them to
|
||||
/// encrypt events.
|
||||
#[error("the new settings are invalid")]
|
||||
InvalidSettings,
|
||||
|
||||
/// The store ran into an error.
|
||||
#[error(transparent)]
|
||||
Store(#[from] CryptoStoreError),
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub fn decrypt_room_key_export(
|
||||
/// # async {
|
||||
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
|
||||
/// let room_id = room_id!("!test:localhost");
|
||||
/// let exported_keys = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
|
||||
/// let exported_keys = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
|
||||
/// let encrypted_export = encrypt_room_key_export(&exported_keys, "1234", 1);
|
||||
/// # };
|
||||
/// ```
|
||||
@@ -293,7 +293,7 @@ mod tests {
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
|
||||
let export = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
|
||||
let export = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
|
||||
|
||||
assert!(!export.is_empty());
|
||||
|
||||
|
||||
@@ -313,7 +313,7 @@ impl GossipMachine {
|
||||
let device =
|
||||
self.inner.store.get_device(&event.sender, &event.content.requesting_device_id).await?;
|
||||
|
||||
Ok(if let Some(device) = device {
|
||||
if let Some(device) = device {
|
||||
if device.user_id() == self.user_id() {
|
||||
if device.is_verified() {
|
||||
info!(
|
||||
@@ -338,7 +338,7 @@ impl GossipMachine {
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}?
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
user_id = ?device.user_id(),
|
||||
@@ -347,7 +347,7 @@ impl GossipMachine {
|
||||
"Received a secret request that we won't serve, the device isn't trusted",
|
||||
);
|
||||
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
@@ -357,7 +357,7 @@ impl GossipMachine {
|
||||
"Received a secret request that we won't serve, the device doesn't belong to us",
|
||||
);
|
||||
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
@@ -374,8 +374,8 @@ impl GossipMachine {
|
||||
.mark_user_as_changed(&event.sender)
|
||||
.await?;
|
||||
|
||||
None
|
||||
})
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to encrypt the given `InboundGroupSession` for the given `Device` as
|
||||
@@ -1247,7 +1247,8 @@ mod tests {
|
||||
.with_transaction(|mut btr| async {
|
||||
let alice_account = atr.account().await?;
|
||||
let bob_account = btr.account().await?;
|
||||
let sessions = alice_account.create_session_for(bob_account).await;
|
||||
let sessions =
|
||||
alice_account.create_session_for_test_helper(bob_account).await;
|
||||
Ok((btr, sessions))
|
||||
})
|
||||
.await?;
|
||||
@@ -1845,7 +1846,7 @@ mod tests {
|
||||
.with_transaction(|mut tr| async {
|
||||
let alice_account = tr.account().await?;
|
||||
let (alice_session, _) =
|
||||
alice_account.create_session_for(&mut second_account).await;
|
||||
alice_account.create_session_for_test_helper(&mut second_account).await;
|
||||
Ok((tr, alice_session))
|
||||
})
|
||||
.await
|
||||
@@ -2068,7 +2069,8 @@ mod tests {
|
||||
.with_transaction(|mut btr| async {
|
||||
let alice_account = atr.account().await?;
|
||||
let bob_account = btr.account().await?;
|
||||
let sessions = alice_account.create_session_for(bob_account).await;
|
||||
let sessions =
|
||||
alice_account.create_session_for_test_helper(bob_account).await;
|
||||
Ok((btr, sessions))
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::Deref,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -30,12 +29,13 @@ use ruma::{
|
||||
OwnedDeviceKeyId, UInt, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{instrument, trace, warn};
|
||||
use vodozemac::{olm::SessionConfig, Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use super::{atomic_bool_deserializer, atomic_bool_serializer};
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[cfg(any(test, feature = "testing", doc))]
|
||||
use crate::OlmMachine;
|
||||
use crate::{
|
||||
error::{EventError, OlmError, OlmResult, SignatureError},
|
||||
@@ -440,6 +440,51 @@ impl Device {
|
||||
|
||||
self.encrypt(event_type, content).await
|
||||
}
|
||||
|
||||
/// Encrypt an event for this device.
|
||||
///
|
||||
/// Beware that the 1-to-1 session must be established prior to this
|
||||
/// call by using the [`OlmMachine::get_missing_sessions`] method.
|
||||
///
|
||||
/// Notable limitation: The caller is responsible for sending the encrypted
|
||||
/// event to the target device, this encryption method supports out-of-order
|
||||
/// messages to a certain extent (2000 messages), if multiple messages are
|
||||
/// encrypted using this method they should be sent in the same order as
|
||||
/// they are encrypted.
|
||||
///
|
||||
/// *Note*: To instead encrypt an event meant for a room use the
|
||||
/// [`OlmMachine::encrypt_room_event()`] method instead.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `event_type` - The type of the event to be sent.
|
||||
/// * `content` - The content of the event to be sent. This should be a type
|
||||
/// that implements the `Serialize` trait.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The encrypted raw content to be shared with your preferred transport
|
||||
/// layer (usually to-device), [`OlmError::MissingSession`] if there is
|
||||
/// no established session with the device.
|
||||
pub async fn encrypt_event_raw(
|
||||
&self,
|
||||
event_type: &str,
|
||||
content: &Value,
|
||||
) -> OlmResult<Raw<ToDeviceEncryptedEventContent>> {
|
||||
let (used_session, raw_encrypted) = self.encrypt(event_type, content).await?;
|
||||
|
||||
// perist the used session
|
||||
self.verification_machine
|
||||
.store
|
||||
.save_changes(Changes { sessions: vec![used_session], ..Default::default() })
|
||||
.await?;
|
||||
|
||||
Ok(raw_encrypted)
|
||||
}
|
||||
|
||||
/// Whether or not the device is a dehydrated device.
|
||||
pub fn is_dehydrated(&self) -> bool {
|
||||
self.inner.inner.dehydrated.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// A read only view over all devices belonging to a user.
|
||||
@@ -500,8 +545,9 @@ impl UserDevices {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// The local trust state of a device.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
|
||||
pub enum LocalTrust {
|
||||
/// The device has been verified and is trusted.
|
||||
Verified = 0,
|
||||
@@ -651,7 +697,7 @@ impl ReadOnlyDevice {
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Trying to find a Olm session of a device, but the device doesn't have a \
|
||||
"Trying to find an Olm session of a device, but the device doesn't have a \
|
||||
Curve25519 key",
|
||||
);
|
||||
|
||||
@@ -744,7 +790,6 @@ impl ReadOnlyDevice {
|
||||
recipient_device = ?self.device_id(),
|
||||
recipient_key = ?self.curve25519_key(),
|
||||
event_type,
|
||||
session,
|
||||
message_id,
|
||||
))
|
||||
]
|
||||
@@ -772,7 +817,6 @@ impl ReadOnlyDevice {
|
||||
|
||||
if let Some(mut session) = session {
|
||||
let message = session.encrypt(self, event_type, content, message_id).await?;
|
||||
trace!("Successfully encrypted an event");
|
||||
Ok((session, message))
|
||||
} else {
|
||||
trace!("Trying to encrypt an event for a device, but no Olm session is found.");
|
||||
@@ -933,10 +977,10 @@ impl PartialEq for ReadOnlyDevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Testing Facilities for Device Management
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod testing {
|
||||
//! Testing Facilities for Device Management
|
||||
#![allow(dead_code)]
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{identities::ReadOnlyDevice, types::DeviceKeys};
|
||||
|
||||
@@ -1014,8 +1014,8 @@ impl IdentityManager {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod testing {
|
||||
#![allow(dead_code)]
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruma::{
|
||||
|
||||
@@ -699,10 +699,10 @@ impl ReadOnlyOwnUserIdentity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Testing Facilities
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod testing {
|
||||
//! Testing Facilities
|
||||
#![allow(dead_code)]
|
||||
use ruma::{api::client::keys::get_keys::v3::Response as KeyQueryResponse, user_id};
|
||||
|
||||
use super::{ReadOnlyOwnUserIdentity, ReadOnlyUserIdentity};
|
||||
|
||||
@@ -33,7 +33,7 @@ pub mod types;
|
||||
mod utilities;
|
||||
mod verification;
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
/// Testing facilities and helpers for crypto tests
|
||||
pub mod testing {
|
||||
pub use crate::identities::{
|
||||
@@ -70,7 +70,9 @@ impl RoomKeyImportResult {
|
||||
}
|
||||
}
|
||||
|
||||
pub use error::{EventError, MegolmError, OlmError, SessionCreationError, SignatureError};
|
||||
pub use error::{
|
||||
EventError, MegolmError, OlmError, SessionCreationError, SetRoomSettingsError, SignatureError,
|
||||
};
|
||||
pub use file_encryption::{
|
||||
decrypt_room_key_export, encrypt_room_key_export, AttachmentDecryptor, AttachmentEncryptor,
|
||||
DecryptorError, KeyExportError, MediaEncryptionInfo,
|
||||
@@ -105,3 +107,6 @@ pub static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test::init_tracing_for_tests!();
|
||||
|
||||
#[cfg(feature = "uniffi")]
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -40,8 +40,8 @@ use ruma::{
|
||||
AnyToDeviceEvent, MessageLikeEventContent,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceId, DeviceKeyAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedTransactionId, OwnedUserId,
|
||||
RoomId, TransactionId, UInt, UserId,
|
||||
DeviceId, DeviceKeyAlgorithm, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedDeviceKeyId,
|
||||
OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UInt, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -58,7 +58,7 @@ use vodozemac::{
|
||||
use crate::{
|
||||
backups::{BackupMachine, MegolmV1BackupKey},
|
||||
dehydrated_devices::{DehydratedDevices, DehydrationError},
|
||||
error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult},
|
||||
error::{EventError, MegolmError, MegolmResult, OlmError, OlmResult, SetRoomSettingsError},
|
||||
gossiping::GossipMachine,
|
||||
identities::{user::UserIdentities, Device, IdentityManager, UserDevices},
|
||||
olm::{
|
||||
@@ -70,8 +70,8 @@ use crate::{
|
||||
session_manager::{GroupSessionManager, SessionManager},
|
||||
store::{
|
||||
Changes, CryptoStoreWrapper, DeviceChanges, IdentityChanges, IntoCryptoStore, MemoryStore,
|
||||
PendingChanges, Result as StoreResult, RoomKeyInfo, SecretImportError, Store, StoreCache,
|
||||
StoreTransaction,
|
||||
PendingChanges, Result as StoreResult, RoomKeyInfo, RoomSettings, SecretImportError, Store,
|
||||
StoreCache, StoreTransaction,
|
||||
},
|
||||
types::{
|
||||
events::{
|
||||
@@ -86,8 +86,9 @@ use crate::{
|
||||
},
|
||||
ToDeviceEvents,
|
||||
},
|
||||
Signatures,
|
||||
EventEncryptionAlgorithm, Signatures,
|
||||
},
|
||||
utilities::timestamp_to_iso8601,
|
||||
verification::{Verification, VerificationMachine, VerificationRequest},
|
||||
CrossSigningKeyExport, CryptoStoreError, KeysQueryRequest, LocalTrust, ReadOnlyDevice,
|
||||
RoomKeyImportResult, SignatureError, ToDeviceRequest,
|
||||
@@ -345,6 +346,16 @@ impl OlmMachine {
|
||||
&self.inner.device_id
|
||||
}
|
||||
|
||||
/// The time at which the `Account` backing this `OlmMachine` was created.
|
||||
///
|
||||
/// An [`Account`] is created when an `OlmMachine` is first instantiated
|
||||
/// against a given [`Store`], at which point it creates identity keys etc.
|
||||
/// This method returns the timestamp, according to the local clock, at
|
||||
/// which that happened.
|
||||
pub fn device_creation_time(&self) -> MilliSecondsSinceUnixEpoch {
|
||||
self.inner.store.static_account().creation_local_time()
|
||||
}
|
||||
|
||||
/// Get the public parts of our Olm identity keys.
|
||||
pub fn identity_keys(&self) -> IdentityKeys {
|
||||
let account = self.inner.store.static_account();
|
||||
@@ -894,12 +905,17 @@ impl OlmMachine {
|
||||
self.inner.group_session_manager.encrypt(room_id, event_type, content).await
|
||||
}
|
||||
|
||||
/// Invalidate the currently active outbound group session for the given
|
||||
/// room.
|
||||
/// Forces the currently active room key, which is used to encrypt messages,
|
||||
/// to be rotated.
|
||||
///
|
||||
/// A new room key will be crated and shared with all the room members the
|
||||
/// next time a message will be sent. You don't have to call this method,
|
||||
/// room keys will be rotated automatically when necessary. This method is
|
||||
/// still useful for debugging purposes.
|
||||
///
|
||||
/// Returns true if a session was invalidated, false if there was no session
|
||||
/// to invalidate.
|
||||
pub async fn invalidate_group_session(&self, room_id: &RoomId) -> StoreResult<bool> {
|
||||
pub async fn discard_room_key(&self, room_id: &RoomId) -> StoreResult<bool> {
|
||||
self.inner.group_session_manager.invalidate_group_session(room_id).await
|
||||
}
|
||||
|
||||
@@ -1101,7 +1117,7 @@ impl OlmMachine {
|
||||
transaction: &mut StoreTransaction,
|
||||
changes: &mut Changes,
|
||||
mut raw_event: Raw<AnyToDeviceEvent>,
|
||||
) -> OlmResult<Raw<AnyToDeviceEvent>> {
|
||||
) -> Raw<AnyToDeviceEvent> {
|
||||
Self::record_message_id(&raw_event);
|
||||
|
||||
let event: ToDeviceEvents = match raw_event.deserialize_as() {
|
||||
@@ -1110,7 +1126,7 @@ impl OlmMachine {
|
||||
// Skip invalid events.
|
||||
warn!("Received an invalid to-device event: {e}");
|
||||
|
||||
return Ok(raw_event);
|
||||
return raw_event;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1135,7 +1151,7 @@ impl OlmMachine {
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(raw_event);
|
||||
return raw_event;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1172,7 +1188,7 @@ impl OlmMachine {
|
||||
e => self.handle_to_device_event(changes, &e).await,
|
||||
}
|
||||
|
||||
Ok(raw_event)
|
||||
raw_event
|
||||
}
|
||||
|
||||
/// Handle a to-device and one-time key counts from a sync response.
|
||||
@@ -1247,8 +1263,7 @@ impl OlmMachine {
|
||||
|
||||
for raw_event in sync_changes.to_device_events {
|
||||
let raw_event =
|
||||
Box::pin(self.receive_to_device_event(transaction, &mut changes, raw_event))
|
||||
.await?;
|
||||
Box::pin(self.receive_to_device_event(transaction, &mut changes, raw_event)).await;
|
||||
events.push(raw_event);
|
||||
}
|
||||
|
||||
@@ -1392,7 +1407,7 @@ impl OlmMachine {
|
||||
|
||||
let secret_requests = GossipMachine::request_missing_secrets(self.user_id(), secrets);
|
||||
|
||||
// Check if there are already inflight requests for these secrets?
|
||||
// Check if there are already in-flight requests for these secrets?
|
||||
let unsent_request = self.store().get_unsent_secret_requests().await?;
|
||||
let not_yet_requested = secret_requests
|
||||
.into_iter()
|
||||
@@ -1530,7 +1545,7 @@ impl OlmMachine {
|
||||
/// * `event` - The event that should be decrypted.
|
||||
///
|
||||
/// * `room_id` - The ID of the room where the event was sent to.
|
||||
#[instrument(skip_all, fields(?room_id, event_id, sender, algorithm, session_id, sender_key))]
|
||||
#[instrument(skip_all, fields(?room_id, event_id, origin_server_ts, sender, algorithm, session_id, sender_key))]
|
||||
pub async fn decrypt_room_event(
|
||||
&self,
|
||||
event: &Raw<EncryptedEvent>,
|
||||
@@ -1541,6 +1556,11 @@ impl OlmMachine {
|
||||
tracing::Span::current()
|
||||
.record("sender", debug(&event.sender))
|
||||
.record("event_id", debug(&event.event_id))
|
||||
.record(
|
||||
"origin_server_ts",
|
||||
timestamp_to_iso8601(event.origin_server_ts)
|
||||
.unwrap_or_else(|| "<out of range>".to_owned()),
|
||||
)
|
||||
.record("algorithm", debug(event.content.algorithm()));
|
||||
|
||||
let content: SupportedEventEncryptionSchemes<'_> = match &event.content.scheme {
|
||||
@@ -1803,50 +1823,6 @@ impl OlmMachine {
|
||||
self.store().import_room_keys(exported_keys, from_backup, progress_listener).await
|
||||
}
|
||||
|
||||
/// Export the keys that match the given predicate.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `predicate` - A closure that will be called for every known
|
||||
/// `InboundGroupSession`, which represents a room key. If the closure
|
||||
/// returns `true` the `InboundGroupSession` will be included in the export,
|
||||
/// if the closure returns `false` it will not be included.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_room_key_export};
|
||||
/// # use ruma::{device_id, user_id, room_id};
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # async {
|
||||
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
|
||||
/// let room_id = room_id!("!test:localhost");
|
||||
/// let exported_keys = machine.export_room_keys(|s| s.room_id() == room_id).await.unwrap();
|
||||
/// let encrypted_export = encrypt_room_key_export(&exported_keys, "1234", 1);
|
||||
/// # };
|
||||
/// ```
|
||||
pub async fn export_room_keys(
|
||||
&self,
|
||||
mut predicate: impl FnMut(&InboundGroupSession) -> bool,
|
||||
) -> StoreResult<Vec<ExportedRoomKey>> {
|
||||
let mut exported = Vec::new();
|
||||
|
||||
let sessions: Vec<InboundGroupSession> = self
|
||||
.store()
|
||||
.get_inbound_group_sessions()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|s| predicate(s))
|
||||
.collect();
|
||||
|
||||
for session in sessions {
|
||||
let export = session.export().await;
|
||||
exported.push(export);
|
||||
}
|
||||
|
||||
Ok(exported)
|
||||
}
|
||||
|
||||
/// Get the status of the private cross signing keys.
|
||||
///
|
||||
/// This can be used to check which private cross signing keys we have
|
||||
@@ -1961,6 +1937,8 @@ impl OlmMachine {
|
||||
None => 0,
|
||||
};
|
||||
|
||||
tracing::debug!("Initialising crypto store generation at {}", gen);
|
||||
|
||||
self.inner
|
||||
.store
|
||||
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, gen.to_le_bytes().to_vec())
|
||||
@@ -1973,19 +1951,32 @@ impl OlmMachine {
|
||||
|
||||
/// If needs be, update the local and on-disk crypto store generation.
|
||||
///
|
||||
/// Returns true whether another user has modified the internal generation
|
||||
/// counter, and as such we've incremented and updated it in the
|
||||
/// database.
|
||||
///
|
||||
/// ## Requirements
|
||||
///
|
||||
/// - This assumes that `initialize_crypto_store_generation` has been called
|
||||
/// beforehand.
|
||||
/// - This requires that the crypto store lock has been acquired.
|
||||
pub async fn maintain_crypto_store_generation(
|
||||
&self,
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `generation` - The in-memory generation counter (or rather, the
|
||||
/// `Mutex` wrapping it). This defines the "expected" generation on entry,
|
||||
/// and, if we determine an update is needed, is updated to hold the "new"
|
||||
/// generation.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple containing:
|
||||
///
|
||||
/// * A `bool`, set to `true` if another process has updated the generation
|
||||
/// number in the `Store` since our expected value, and as such we've
|
||||
/// incremented and updated it in the database. Otherwise, `false`.
|
||||
///
|
||||
/// * The (possibly updated) generation counter.
|
||||
pub async fn maintain_crypto_store_generation<'a>(
|
||||
&'a self,
|
||||
generation: &Mutex<Option<u64>>,
|
||||
) -> StoreResult<bool> {
|
||||
) -> StoreResult<(bool, u64)> {
|
||||
let mut gen_guard = generation.lock().await;
|
||||
|
||||
// The database value must be there:
|
||||
@@ -2007,10 +1998,10 @@ impl OlmMachine {
|
||||
CryptoStoreError::InvalidLockGeneration("invalid format".to_owned())
|
||||
})?);
|
||||
|
||||
let expected_gen = match gen_guard.as_ref() {
|
||||
let new_gen = match gen_guard.as_ref() {
|
||||
Some(expected_gen) => {
|
||||
if actual_gen == *expected_gen {
|
||||
return Ok(false);
|
||||
return Ok((false, actual_gen));
|
||||
}
|
||||
// Increment the biggest, and store it everywhere.
|
||||
actual_gen.max(*expected_gen).wrapping_add(1)
|
||||
@@ -2026,22 +2017,19 @@ impl OlmMachine {
|
||||
"Crypto store generation mismatch: previously known was {:?}, actual is {:?}, next is {}",
|
||||
*gen_guard,
|
||||
actual_gen,
|
||||
expected_gen
|
||||
new_gen
|
||||
);
|
||||
|
||||
// Update known value.
|
||||
*gen_guard = Some(expected_gen);
|
||||
*gen_guard = Some(new_gen);
|
||||
|
||||
// Update value in database.
|
||||
self.inner
|
||||
.store
|
||||
.set_custom_value(
|
||||
Self::CURRENT_GENERATION_STORE_KEY,
|
||||
expected_gen.to_le_bytes().to_vec(),
|
||||
)
|
||||
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, new_gen.to_le_bytes().to_vec())
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
Ok((true, new_gen))
|
||||
}
|
||||
|
||||
/// Manage dehydrated devices.
|
||||
@@ -2049,6 +2037,89 @@ impl OlmMachine {
|
||||
DehydratedDevices { inner: self.to_owned() }
|
||||
}
|
||||
|
||||
/// Get the stored encryption settings for the given room, such as the
|
||||
/// encryption algorithm or whether to encrypt only for trusted devices.
|
||||
///
|
||||
/// These settings can be modified via [`OlmMachine::set_room_settings`].
|
||||
pub async fn room_settings(&self, room_id: &RoomId) -> StoreResult<Option<RoomSettings>> {
|
||||
// There's not much to do here: it's just exposed for symmetry with
|
||||
// `set_room_settings`.
|
||||
self.inner.store.get_room_settings(room_id).await
|
||||
}
|
||||
|
||||
/// Store encryption settings for the given room.
|
||||
///
|
||||
/// This method checks if the new settings are "safe" -- ie, that they do
|
||||
/// not represent a downgrade in encryption security from any previous
|
||||
/// settings. Attempts to downgrade security will result in a
|
||||
/// [`SetRoomSettingsError::EncryptionDowngrade`].
|
||||
///
|
||||
/// If the settings are valid, they will be persisted to the crypto store.
|
||||
/// These settings are not used directly by this library, but the saved
|
||||
/// settings can be retrieved via [`OlmMachine::room_settings`].
|
||||
pub async fn set_room_settings(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
new_settings: &RoomSettings,
|
||||
) -> Result<(), SetRoomSettingsError> {
|
||||
let store = &self.inner.store;
|
||||
|
||||
// We want to make sure that we do not race against a second concurrent call to
|
||||
// `set_room_settings`. By way of an easy way to do so, we start a
|
||||
// StoreTransaction. There's no need to commit() it: we're just using it as a
|
||||
// lock guard.
|
||||
let _store_transaction = store.transaction().await;
|
||||
|
||||
let old_settings = store.get_room_settings(room_id).await?;
|
||||
|
||||
// We want to make sure that the change to the room settings does not represent
|
||||
// a downgrade in security. The [E2EE implementation guide] recommends:
|
||||
//
|
||||
// > This flag should **not** be cleared if a later `m.room.encryption` event
|
||||
// > changes the configuration.
|
||||
//
|
||||
// (However, it doesn't really address how to handle changes to the rotation
|
||||
// parameters, etc.) For now at least, we are very conservative here:
|
||||
// any new settings are rejected if they differ from the existing settings.
|
||||
// merit improvement (cf https://github.com/element-hq/element-meta/issues/69).
|
||||
//
|
||||
// [E2EE implementation guide]: https://matrix.org/docs/matrix-concepts/end-to-end-encryption/#handling-an-m-room-encryption-state-event
|
||||
if let Some(old_settings) = old_settings {
|
||||
if old_settings != *new_settings {
|
||||
return Err(SetRoomSettingsError::EncryptionDowngrade);
|
||||
} else {
|
||||
// nothing to do here
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that the new settings are valid
|
||||
match new_settings.algorithm {
|
||||
EventEncryptionAlgorithm::MegolmV1AesSha2 => (),
|
||||
|
||||
#[cfg(feature = "experimental-algorithms")]
|
||||
EventEncryptionAlgorithm::MegolmV2AesSha2 => (),
|
||||
|
||||
_ => {
|
||||
warn!(
|
||||
?room_id,
|
||||
"Rejecting invalid encryption algorithm {}", new_settings.algorithm
|
||||
);
|
||||
return Err(SetRoomSettingsError::InvalidSettings);
|
||||
}
|
||||
}
|
||||
|
||||
// The new settings are acceptable, so let's save them.
|
||||
store
|
||||
.save_changes(Changes {
|
||||
room_settings: HashMap::from([(room_id.to_owned(), new_settings.clone())]),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "testing", test))]
|
||||
/// Returns whether this `OlmMachine` is the same another one.
|
||||
///
|
||||
@@ -2108,8 +2179,8 @@ pub struct EncryptionSyncChanges<'a> {
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "testing", test))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod testing {
|
||||
#![allow(dead_code)]
|
||||
use http::Response;
|
||||
|
||||
pub fn response_from_file(json: &serde_json::Value) -> Response<Vec<u8>> {
|
||||
@@ -2156,10 +2227,11 @@ pub(crate) mod tests {
|
||||
},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch,
|
||||
OwnedDeviceKeyId, SecondsSinceUnixEpoch, TransactionId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use vodozemac::{
|
||||
megolm::{GroupSession, SessionConfig},
|
||||
Curve25519PublicKey, Ed25519PublicKey,
|
||||
@@ -2167,10 +2239,10 @@ pub(crate) mod tests {
|
||||
|
||||
use super::{testing::response_from_file, CrossSigningBootstrapRequests};
|
||||
use crate::{
|
||||
error::EventError,
|
||||
error::{EventError, SetRoomSettingsError},
|
||||
machine::{EncryptionSyncChanges, OlmMachine},
|
||||
olm::{InboundGroupSession, OutboundGroupSession, VerifyJson},
|
||||
store::Changes,
|
||||
store::{Changes, RoomSettings},
|
||||
types::{
|
||||
events::{
|
||||
room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent},
|
||||
@@ -2243,9 +2315,9 @@ pub(crate) mod tests {
|
||||
.store()
|
||||
.with_transaction(|mut tr| async {
|
||||
let account = tr.account().await.unwrap();
|
||||
account.generate_fallback_key_helper();
|
||||
account.generate_fallback_key_if_needed();
|
||||
account.update_uploaded_key_count(0);
|
||||
account.generate_one_time_keys();
|
||||
account.generate_one_time_keys_if_needed();
|
||||
let request = machine
|
||||
.keys_for_upload(account)
|
||||
.await
|
||||
@@ -2273,7 +2345,7 @@ pub(crate) mod tests {
|
||||
(machine, otk)
|
||||
}
|
||||
|
||||
async fn get_machine_pair(
|
||||
pub async fn get_machine_pair(
|
||||
alice: &UserId,
|
||||
bob: &UserId,
|
||||
use_fallback_key: bool,
|
||||
@@ -2348,8 +2420,13 @@ pub(crate) mod tests {
|
||||
|
||||
#[async_test]
|
||||
async fn test_create_olm_machine() {
|
||||
let test_start_ts = MilliSecondsSinceUnixEpoch::now();
|
||||
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
|
||||
|
||||
let device_creation_time = machine.device_creation_time();
|
||||
assert!(device_creation_time <= MilliSecondsSinceUnixEpoch::now());
|
||||
assert!(device_creation_time >= test_start_ts);
|
||||
|
||||
let cache = machine.store().cache().await.unwrap();
|
||||
let account = cache.account().await.unwrap();
|
||||
assert!(!account.shared());
|
||||
@@ -2371,7 +2448,7 @@ pub(crate) mod tests {
|
||||
.store()
|
||||
.with_transaction(|mut tr| async {
|
||||
let account = tr.account().await.unwrap();
|
||||
assert!(account.generate_one_time_keys().is_some());
|
||||
assert!(account.generate_one_time_keys_if_needed().is_some());
|
||||
Ok((tr, ()))
|
||||
})
|
||||
.await
|
||||
@@ -2385,7 +2462,7 @@ pub(crate) mod tests {
|
||||
.store()
|
||||
.with_transaction(|mut tr| async {
|
||||
let account = tr.account().await.unwrap();
|
||||
assert!(account.generate_one_time_keys().is_some());
|
||||
assert!(account.generate_one_time_keys_if_needed().is_some());
|
||||
Ok((tr, ()))
|
||||
})
|
||||
.await
|
||||
@@ -2399,7 +2476,7 @@ pub(crate) mod tests {
|
||||
.store()
|
||||
.with_transaction(|mut tr| async {
|
||||
let account = tr.account().await.unwrap();
|
||||
assert!(account.generate_one_time_keys().is_none());
|
||||
assert!(account.generate_one_time_keys_if_needed().is_none());
|
||||
|
||||
Ok((tr, ()))
|
||||
})
|
||||
@@ -2437,7 +2514,7 @@ pub(crate) mod tests {
|
||||
machine.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap();
|
||||
assert!(machine.inner.group_session_manager.get_outbound_group_session(room_id).is_some());
|
||||
|
||||
machine.invalidate_group_session(room_id).await.unwrap();
|
||||
machine.discard_room_key(room_id).await.unwrap();
|
||||
|
||||
assert!(machine
|
||||
.inner
|
||||
@@ -2467,7 +2544,7 @@ pub(crate) mod tests {
|
||||
fn test_one_time_key_signing() {
|
||||
let mut account = Account::with_device_id(user_id(), alice_device_id());
|
||||
account.update_uploaded_key_count(49);
|
||||
account.generate_one_time_keys();
|
||||
account.generate_one_time_keys_if_needed();
|
||||
|
||||
let mut one_time_keys = account.signed_one_time_keys();
|
||||
let ed25519_key = account.identity_keys().ed25519;
|
||||
@@ -4071,4 +4148,192 @@ pub(crate) mod tests {
|
||||
// The waiting should successfully complete.
|
||||
wait.await.unwrap();
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn room_settings_returns_none_for_unknown_room() {
|
||||
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
|
||||
let settings = machine.room_settings(room_id!("!test2:localhost")).await.unwrap();
|
||||
assert!(settings.is_none());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn stores_and_returns_room_settings() {
|
||||
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let settings = RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
only_allow_trusted_devices: true,
|
||||
session_rotation_period: Some(Duration::from_secs(10)),
|
||||
session_rotation_period_messages: Some(1234),
|
||||
};
|
||||
|
||||
machine.set_room_settings(room_id, &settings).await.unwrap();
|
||||
assert_eq!(machine.room_settings(room_id).await.unwrap(), Some(settings));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn set_room_settings_rejects_invalid_algorithms() {
|
||||
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let err = machine
|
||||
.set_room_settings(
|
||||
room_id,
|
||||
&RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_matches!(err, SetRoomSettingsError::InvalidSettings)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn set_room_settings_rejects_changes() {
|
||||
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
// Initial settings
|
||||
machine
|
||||
.set_room_settings(
|
||||
room_id,
|
||||
&RoomSettings { session_rotation_period_messages: Some(100), ..Default::default() },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Now, modifying the settings should be rejected
|
||||
let err = machine
|
||||
.set_room_settings(
|
||||
room_id,
|
||||
&RoomSettings {
|
||||
session_rotation_period_messages: Some(1000),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_matches!(err, SetRoomSettingsError::EncryptionDowngrade);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn set_room_settings_accepts_noop_changes() {
|
||||
let machine = OlmMachine::new(user_id(), alice_device_id()).await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
// Initial settings
|
||||
machine
|
||||
.set_room_settings(
|
||||
room_id,
|
||||
&RoomSettings { session_rotation_period_messages: Some(100), ..Default::default() },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Same again; should be fine.
|
||||
machine
|
||||
.set_room_settings(
|
||||
room_id,
|
||||
&RoomSettings { session_rotation_period_messages: Some(100), ..Default::default() },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_encrypted_to_device() {
|
||||
let (alice, bob) = get_machine_pair_with_session(alice_id(), user_id(), false).await;
|
||||
|
||||
let custom_event_type = "m.new_device";
|
||||
|
||||
let custom_content = json!({
|
||||
"device_id": "XYZABCDE",
|
||||
"rooms": ["!726s6s6q:example.com"]
|
||||
});
|
||||
|
||||
let device = alice.get_device(bob.user_id(), bob.device_id(), None).await.unwrap().unwrap();
|
||||
let raw_encrypted = device
|
||||
.encrypt_event_raw(custom_event_type, &custom_content)
|
||||
.await
|
||||
.expect("Should have encryted the content");
|
||||
|
||||
let request = ToDeviceRequest::new(
|
||||
bob.user_id(),
|
||||
DeviceIdOrAllDevices::DeviceId(bob_device_id().to_owned()),
|
||||
"m.room.encrypted",
|
||||
raw_encrypted.cast(),
|
||||
);
|
||||
|
||||
assert_eq!("m.room.encrypted", request.event_type.to_string());
|
||||
|
||||
let messages = &request.messages;
|
||||
assert_eq!(1, messages.len());
|
||||
assert!(messages.get(bob.user_id()).is_some());
|
||||
let target_devices = messages.get(bob.user_id()).unwrap();
|
||||
assert_eq!(1, target_devices.len());
|
||||
assert!(target_devices
|
||||
.get(&DeviceIdOrAllDevices::DeviceId(bob_device_id().to_owned()))
|
||||
.is_some());
|
||||
|
||||
let event = ToDeviceEvent::new(
|
||||
alice.user_id().to_owned(),
|
||||
to_device_requests_to_content(vec![request.clone().into()]),
|
||||
);
|
||||
|
||||
let event = json_convert(&event).unwrap();
|
||||
|
||||
let sync_changes = EncryptionSyncChanges {
|
||||
to_device_events: vec![event],
|
||||
changed_devices: &Default::default(),
|
||||
one_time_keys_counts: &Default::default(),
|
||||
unused_fallback_keys: None,
|
||||
next_batch_token: None,
|
||||
};
|
||||
|
||||
let (decrypted, _) = bob.receive_sync_changes(sync_changes).await.unwrap();
|
||||
|
||||
assert_eq!(1, decrypted.len());
|
||||
|
||||
let decrypted_event = decrypted[0].deserialize().unwrap();
|
||||
|
||||
assert_eq!(decrypted_event.event_type().to_string(), custom_event_type.to_owned());
|
||||
|
||||
let decrypted_value = to_raw_value(&decrypted[0]).unwrap();
|
||||
let decrypted_value = serde_json::to_value(decrypted_value).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypted_value.get("content").unwrap().get("device_id").unwrap().as_str().unwrap(),
|
||||
custom_content.get("device_id").unwrap().as_str().unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decrypted_value.get("content").unwrap().get("rooms").unwrap().as_array().unwrap(),
|
||||
custom_content.get("rooms").unwrap().as_array().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_send_encrypted_to_device_no_session() {
|
||||
let (alice, bob, _) = get_machine_pair(alice_id(), user_id(), false).await;
|
||||
|
||||
let custom_event_type = "m.new_device";
|
||||
|
||||
let custom_content = json!({
|
||||
"device_id": "XYZABCDE",
|
||||
"rooms": ["!726s6s6q:example.com"]
|
||||
});
|
||||
|
||||
let encryption_result = alice
|
||||
.get_device(bob.user_id(), bob_device_id(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.encrypt_event_raw(custom_event_type, &custom_content)
|
||||
.await;
|
||||
|
||||
assert_matches!(encryption_result, Err(OlmError::MissingSession));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ use std::{
|
||||
fmt,
|
||||
ops::{Deref, Not as _},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use js_option::JsOption;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
dehydrated_device::{DehydratedDeviceData, DehydratedDeviceV1},
|
||||
@@ -161,6 +163,8 @@ pub struct StaticAccountData {
|
||||
pub device_id: OwnedDeviceId,
|
||||
/// The associated identity keys.
|
||||
pub identity_keys: Arc<IdentityKeys>,
|
||||
/// Whether the account is for a dehydrated device.
|
||||
pub dehydrated: bool,
|
||||
// The creation time of the account in milliseconds since epoch.
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch,
|
||||
}
|
||||
@@ -281,13 +285,17 @@ impl StaticAccountData {
|
||||
),
|
||||
]);
|
||||
|
||||
DeviceKeys::new(
|
||||
let mut ret = DeviceKeys::new(
|
||||
(*self.user_id).to_owned(),
|
||||
(*self.device_id).to_owned(),
|
||||
Self::ALGORITHMS.iter().map(|a| (**a).clone()).collect(),
|
||||
keys,
|
||||
Default::default(),
|
||||
)
|
||||
);
|
||||
if self.dehydrated {
|
||||
ret.dehydrated = JsOption::Some(true);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Get the user id of the owner of the account.
|
||||
@@ -327,6 +335,14 @@ pub struct Account {
|
||||
/// needs to set this for us, depending on the count we will suggest the
|
||||
/// client to upload new keys.
|
||||
uploaded_signed_key_count: u64,
|
||||
/// The timestamp of the last time we generated a fallback key. Fallback
|
||||
/// keys are rotated in a time-based manner. This field records when we
|
||||
/// either generated our first fallback key or rotated one.
|
||||
///
|
||||
/// Will be `None` if we never created a fallback key, or if we're migrating
|
||||
/// from a `AccountPickle` that didn't use time-based fallback key
|
||||
/// rotation.
|
||||
fallback_creation_timestamp: Option<MilliSecondsSinceUnixEpoch>,
|
||||
}
|
||||
|
||||
impl Deref for Account {
|
||||
@@ -352,12 +368,18 @@ pub struct PickledAccount {
|
||||
pub pickle: AccountPickle,
|
||||
/// Was the account shared.
|
||||
pub shared: bool,
|
||||
/// Whether this is for a dehydrated device
|
||||
#[serde(default)]
|
||||
pub dehydrated: bool,
|
||||
/// The number of uploaded one-time keys we have on the server.
|
||||
pub uploaded_signed_key_count: u64,
|
||||
/// The local time creation of this account (milliseconds since epoch), used
|
||||
/// as creation time of own device
|
||||
#[serde(default = "default_account_creation_time")]
|
||||
pub creation_local_time: MilliSecondsSinceUnixEpoch,
|
||||
/// The timestamp of the last time we generated a fallback key.
|
||||
#[serde(default)]
|
||||
pub fallback_key_creation_timestamp: Option<MilliSecondsSinceUnixEpoch>,
|
||||
}
|
||||
|
||||
fn default_account_creation_time() -> MilliSecondsSinceUnixEpoch {
|
||||
@@ -399,11 +421,13 @@ impl Account {
|
||||
user_id: user_id.into(),
|
||||
device_id: device_id.into(),
|
||||
identity_keys: Arc::new(identity_keys),
|
||||
dehydrated: false,
|
||||
creation_local_time: MilliSecondsSinceUnixEpoch::now(),
|
||||
},
|
||||
inner: Box::new(account),
|
||||
shared: false,
|
||||
uploaded_signed_key_count: 0,
|
||||
fallback_creation_timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +448,17 @@ impl Account {
|
||||
Self::new_helper(account, user_id, &device_id)
|
||||
}
|
||||
|
||||
/// Create a new random Olm Account for a dehydrated device
|
||||
pub fn new_dehydrated(user_id: &UserId) -> Self {
|
||||
let account = InnerAccount::new();
|
||||
let device_id: OwnedDeviceId =
|
||||
base64_encode(account.identity_keys().curve25519.as_bytes()).into();
|
||||
|
||||
let mut ret = Self::new_helper(account, user_id, &device_id);
|
||||
ret.static_data.dehydrated = true;
|
||||
ret
|
||||
}
|
||||
|
||||
/// Get the immutable data for this account.
|
||||
pub fn static_data(&self) -> &StaticAccountData {
|
||||
&self.static_data
|
||||
@@ -464,7 +499,7 @@ impl Account {
|
||||
}
|
||||
|
||||
/// Generate count number of one-time keys.
|
||||
pub fn generate_one_time_keys_helper(&mut self, count: usize) -> OneTimeKeyGenerationResult {
|
||||
pub fn generate_one_time_keys(&mut self, count: usize) -> OneTimeKeyGenerationResult {
|
||||
self.inner.generate_one_time_keys(count)
|
||||
}
|
||||
|
||||
@@ -493,14 +528,14 @@ impl Account {
|
||||
}
|
||||
|
||||
self.update_uploaded_key_count(count);
|
||||
self.generate_one_time_keys();
|
||||
self.generate_one_time_keys_if_needed();
|
||||
}
|
||||
|
||||
if let Some(unused) = unused_fallback_keys {
|
||||
if !unused.contains(&DeviceKeyAlgorithm::SignedCurve25519) {
|
||||
// Generate a new fallback key if we don't have one.
|
||||
self.generate_fallback_key_helper();
|
||||
}
|
||||
// If the server supports fallback keys or if it did so in the past, shown by
|
||||
// the existence of a fallback creation timestamp, generate a new one if
|
||||
// we don't have one, or if the current fallback key expired.
|
||||
if unused_fallback_keys.is_some() || self.fallback_creation_timestamp.is_some() {
|
||||
self.generate_fallback_key_if_needed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,47 +548,91 @@ impl Account {
|
||||
/// Generally `Some` means that keys should be uploaded, while `None` means
|
||||
/// that keys should not be uploaded.
|
||||
#[instrument(skip_all)]
|
||||
pub fn generate_one_time_keys(&mut self) -> Option<u64> {
|
||||
pub fn generate_one_time_keys_if_needed(&mut self) -> Option<u64> {
|
||||
// Only generate one-time keys if there aren't any, otherwise the caller
|
||||
// might have failed to upload them the last time this method was
|
||||
// called.
|
||||
if self.one_time_keys().is_empty() {
|
||||
let count = self.uploaded_key_count();
|
||||
let max_keys = self.max_one_time_keys();
|
||||
|
||||
if count >= max_keys as u64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let key_count = (max_keys as u64) - count;
|
||||
let key_count: usize = key_count.try_into().unwrap_or(max_keys);
|
||||
|
||||
let result = self.generate_one_time_keys_helper(key_count);
|
||||
|
||||
debug!(
|
||||
count = key_count,
|
||||
discarded_keys = ?result.removed,
|
||||
created_keys = ?result.created,
|
||||
"Generated new one-time keys"
|
||||
);
|
||||
|
||||
Some(key_count as u64)
|
||||
} else {
|
||||
Some(0)
|
||||
if !self.one_time_keys().is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let count = self.uploaded_key_count();
|
||||
let max_keys = self.max_one_time_keys();
|
||||
|
||||
if count >= max_keys as u64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let key_count = (max_keys as u64) - count;
|
||||
let key_count: usize = key_count.try_into().unwrap_or(max_keys);
|
||||
|
||||
let result = self.generate_one_time_keys(key_count);
|
||||
|
||||
debug!(
|
||||
count = key_count,
|
||||
discarded_keys = ?result.removed,
|
||||
created_keys = ?result.created,
|
||||
"Generated new one-time keys"
|
||||
);
|
||||
|
||||
Some(key_count as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_fallback_key_helper(&mut self) {
|
||||
if self.inner.fallback_key().is_empty() {
|
||||
/// Generate a new fallback key iff a unpublished one isn't already inside
|
||||
/// of vodozemac and if the currently active one expired.
|
||||
///
|
||||
/// The former is checked using [`Account::fallback_key().is_empty()`],
|
||||
/// which is a hashmap that gets cleared by the
|
||||
/// [`Account::mark_keys_as_published()`] call.
|
||||
pub(crate) fn generate_fallback_key_if_needed(&mut self) {
|
||||
if self.inner.fallback_key().is_empty() && self.fallback_key_expired() {
|
||||
let removed_fallback_key = self.inner.generate_fallback_key();
|
||||
self.fallback_creation_timestamp = Some(MilliSecondsSinceUnixEpoch::now());
|
||||
|
||||
debug!(
|
||||
?removed_fallback_key,
|
||||
"No unused fallback keys were found on the server, generated a new fallback key.",
|
||||
"The fallback key either expired or we didn't have one: generated a new fallback key.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if our most recent fallback key has expired.
|
||||
///
|
||||
/// We consider the fallback key to be expired if it's older than a week.
|
||||
/// This is the lower bound for the recommended signed pre-key bundle
|
||||
/// rotation interval in the X3DH spec[1].
|
||||
///
|
||||
/// [1]: https://signal.org/docs/specifications/x3dh/#publishing-keys
|
||||
fn fallback_key_expired(&self) -> bool {
|
||||
const FALLBACK_KEY_MAX_AGE: Duration = Duration::from_secs(3600 * 24 * 7);
|
||||
|
||||
if let Some(time) = self.fallback_creation_timestamp {
|
||||
// `to_system_time()` returns `None` if the the UNIX_EPOCH + `time` doesn't fit
|
||||
// into a i64. This will likely never happen, but let's rotate the
|
||||
// key in case the values are messed up for some other reason.
|
||||
let Some(system_time) = time.to_system_time() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
// `elapsed()` errors if the `system_time` is in the future, this should mean
|
||||
// that our clock has changed to the past, let's rotate just in case
|
||||
// and then we'll get to a normal time.
|
||||
let Ok(elapsed) = system_time.elapsed() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Alright, our times are normal and we know how much time elapsed since the
|
||||
// last time we created/rotated a fallback key.
|
||||
//
|
||||
// If the key is older than a week, then we rotate it.
|
||||
elapsed > FALLBACK_KEY_MAX_AGE
|
||||
} else {
|
||||
// We never created a fallback key, or we're migrating to the time-based
|
||||
// fallback key rotation, so let's generate a new fallback key.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_key(&self) -> HashMap<KeyId, Curve25519PublicKey> {
|
||||
self.inner.fallback_key()
|
||||
}
|
||||
@@ -593,8 +672,10 @@ impl Account {
|
||||
device_id: self.device_id().to_owned(),
|
||||
pickle,
|
||||
shared: self.shared(),
|
||||
dehydrated: self.static_data.dehydrated,
|
||||
uploaded_signed_key_count: self.uploaded_key_count(),
|
||||
creation_local_time: self.static_data.creation_local_time,
|
||||
fallback_key_creation_timestamp: self.fallback_creation_timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,11 +727,13 @@ impl Account {
|
||||
user_id: (*pickle.user_id).into(),
|
||||
device_id: (*pickle.device_id).into(),
|
||||
identity_keys: Arc::new(identity_keys),
|
||||
dehydrated: pickle.dehydrated,
|
||||
creation_local_time: pickle.creation_local_time,
|
||||
},
|
||||
inner: Box::new(account),
|
||||
shared: pickle.shared,
|
||||
uploaded_signed_key_count: pickle.uploaded_signed_key_count,
|
||||
fallback_creation_timestamp: pickle.fallback_key_creation_timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -729,9 +812,9 @@ impl Account {
|
||||
self.inner.sign_json(json)
|
||||
}
|
||||
|
||||
/// Generate, sign and prepare one-time keys to be uploaded.
|
||||
/// Sign and prepare one-time keys to be uploaded.
|
||||
///
|
||||
/// If no one-time keys need to be uploaded returns an empty BTreeMap.
|
||||
/// If no one-time keys need to be uploaded, returns an empty `BTreeMap`.
|
||||
pub fn signed_one_time_keys(
|
||||
&self,
|
||||
) -> BTreeMap<OwnedDeviceKeyId, Raw<ruma::encryption::OneTimeKey>> {
|
||||
@@ -900,9 +983,9 @@ impl Account {
|
||||
PrekeyBundle::Olm3DH { key } => {
|
||||
device.verify_one_time_key(&key).map_err(|error| {
|
||||
SessionCreationError::InvalidSignature {
|
||||
signing_key: device.ed25519_key(),
|
||||
one_time_key: key.clone(),
|
||||
error,
|
||||
signing_key: device.ed25519_key().map(Box::new),
|
||||
one_time_key: key.clone().into(),
|
||||
error: error.into(),
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -937,28 +1020,19 @@ impl Account {
|
||||
///
|
||||
/// * `message` - A pre-key Olm message that was sent to us by the other
|
||||
/// account.
|
||||
#[instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
message,
|
||||
session_id = message.session_id(),
|
||||
session,
|
||||
)
|
||||
)]
|
||||
pub fn create_inbound_session(
|
||||
&mut self,
|
||||
their_identity_key: Curve25519PublicKey,
|
||||
message: &PreKeyMessage,
|
||||
) -> Result<InboundCreationResult, SessionCreationError> {
|
||||
debug!("Creating a new Olm session from a pre-key message");
|
||||
Span::current().record("session_id", debug(message.session_id()));
|
||||
trace!("Creating a new Olm session from a pre-key message");
|
||||
|
||||
let result = self.inner.create_inbound_session(their_identity_key, message)?;
|
||||
let now = SecondsSinceUnixEpoch::now();
|
||||
let session_id = result.session.session_id();
|
||||
|
||||
Span::current().record("session", debug(&result.session));
|
||||
|
||||
trace!("Olm session created successfully");
|
||||
debug!(session=?result.session, "Decrypted an Olm message from a new Olm session");
|
||||
|
||||
let session = Session {
|
||||
user_id: self.static_data.user_id.clone(),
|
||||
@@ -980,10 +1054,13 @@ impl Account {
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[allow(dead_code)]
|
||||
/// Testing only helper to create a session for the given Account
|
||||
pub async fn create_session_for(&mut self, other: &mut Account) -> (Session, Session) {
|
||||
pub async fn create_session_for_test_helper(
|
||||
&mut self,
|
||||
other: &mut Account,
|
||||
) -> (Session, Session) {
|
||||
use ruma::events::dummy::ToDeviceDummyEventContent;
|
||||
|
||||
other.generate_one_time_keys_helper(1);
|
||||
other.generate_one_time_keys(1);
|
||||
let one_time_map = other.signed_one_time_keys();
|
||||
let device = ReadOnlyDevice::from_account(other);
|
||||
|
||||
@@ -1140,32 +1217,33 @@ impl Account {
|
||||
|
||||
match message {
|
||||
OlmMessage::Normal(_) => {
|
||||
let session_ids = if let Some(sessions) = existing_sessions {
|
||||
let mut errors_by_olm_session = Vec::new();
|
||||
|
||||
if let Some(sessions) = existing_sessions {
|
||||
let sessions = &mut *sessions.lock().await;
|
||||
|
||||
// Try to decrypt the message using each Session we share with the
|
||||
// given curve25519 sender key.
|
||||
for session in sessions.iter_mut() {
|
||||
if let Ok(p) = session.decrypt(message).await {
|
||||
// success!
|
||||
return Ok((SessionType::Existing(session.clone()), p));
|
||||
} else {
|
||||
// An error here is completely normal, after all we don't know
|
||||
// which session was used to encrypt a message. We will log a
|
||||
// warning if no session was able to decrypt the message.
|
||||
continue;
|
||||
match session.decrypt(message).await {
|
||||
Ok(p) => {
|
||||
// success!
|
||||
return Ok((SessionType::Existing(session.clone()), p));
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
// An error here is completely normal, after all we don't know
|
||||
// which session was used to encrypt a message.
|
||||
// We keep hold of the error, so that if *all* sessions fail to
|
||||
// decrypt, we can log something useful.
|
||||
errors_by_olm_session.push((session.session_id().to_owned(), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decryption wasn't successful with any of the sessions. Collect a list of
|
||||
// session IDs to log.
|
||||
sessions.iter().map(|s| s.session_id().to_owned()).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
}
|
||||
|
||||
warn!(
|
||||
?session_ids,
|
||||
?errors_by_olm_session,
|
||||
"Failed to decrypt a non-pre-key message with all available sessions"
|
||||
);
|
||||
Err(OlmError::SessionWedged(sender.to_owned(), sender_key))
|
||||
@@ -1237,7 +1315,7 @@ impl Account {
|
||||
|
||||
/// Decrypt an Olm message, creating a new Olm session if necessary, and
|
||||
/// parse the result.
|
||||
#[instrument(skip(self, store, message))]
|
||||
#[instrument(skip(self, store), fields(session, session_id))]
|
||||
async fn decrypt_and_parse_olm_message(
|
||||
&mut self,
|
||||
store: &Store,
|
||||
@@ -1248,15 +1326,7 @@ impl Account {
|
||||
let (session, plaintext) =
|
||||
self.decrypt_olm_message(store, sender, sender_key, message).await?;
|
||||
|
||||
{
|
||||
let session_id = match &session {
|
||||
SessionType::New(s) => s.session_id(),
|
||||
SessionType::Existing(s) => s.session_id(),
|
||||
};
|
||||
|
||||
Span::current().record("session_id", session_id);
|
||||
trace!("Successfully decrypted an Olm message");
|
||||
}
|
||||
trace!("Successfully decrypted an Olm message");
|
||||
|
||||
match self.parse_decrypted_to_device_event(store, sender, sender_key, plaintext).await {
|
||||
Ok(result) => Ok((session, result)),
|
||||
@@ -1383,6 +1453,7 @@ mod tests {
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
ops::Deref,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -1427,13 +1498,13 @@ mod tests {
|
||||
|
||||
account.mark_keys_as_published();
|
||||
account.update_uploaded_key_count(50);
|
||||
account.generate_one_time_keys();
|
||||
account.generate_one_time_keys_if_needed();
|
||||
|
||||
let (_, third_one_time_keys, _) = account.keys_for_upload();
|
||||
assert!(third_one_time_keys.is_empty());
|
||||
|
||||
account.update_uploaded_key_count(0);
|
||||
account.generate_one_time_keys();
|
||||
account.generate_one_time_keys_if_needed();
|
||||
|
||||
let (_, fourth_one_time_keys, _) = account.keys_for_upload();
|
||||
assert!(!fourth_one_time_keys.is_empty());
|
||||
@@ -1454,7 +1525,10 @@ mod tests {
|
||||
// We don't create fallback keys since we don't know if the server
|
||||
// supports them, we need to receive a sync response to decide if we're
|
||||
// going to create them or not.
|
||||
assert!(fallback_keys.is_empty());
|
||||
assert!(
|
||||
fallback_keys.is_empty(),
|
||||
"We should not upload fallback keys until we know if the server supports them."
|
||||
);
|
||||
|
||||
let one_time_keys = BTreeMap::from([(DeviceKeyAlgorithm::SignedCurve25519, 50u8.into())]);
|
||||
|
||||
@@ -1462,7 +1536,11 @@ mod tests {
|
||||
// fallback key gets uploaded.
|
||||
account.update_key_counts(&one_time_keys, None);
|
||||
let (_, _, fallback_keys) = account.keys_for_upload();
|
||||
assert!(fallback_keys.is_empty());
|
||||
assert!(
|
||||
fallback_keys.is_empty(),
|
||||
"We should not upload a fallback key if we're certain that the server doesn't support \
|
||||
them."
|
||||
);
|
||||
|
||||
// The empty array means that the server supports fallback keys but
|
||||
// there isn't a unused fallback key on the server. This time we upload
|
||||
@@ -1470,14 +1548,36 @@ mod tests {
|
||||
let unused_fallback_keys = &[];
|
||||
account.update_key_counts(&one_time_keys, Some(unused_fallback_keys.as_ref()));
|
||||
let (_, _, fallback_keys) = account.keys_for_upload();
|
||||
assert!(!fallback_keys.is_empty());
|
||||
assert!(
|
||||
!fallback_keys.is_empty(),
|
||||
"We should upload the initial fallback key if the server supports them."
|
||||
);
|
||||
account.mark_keys_as_published();
|
||||
|
||||
// There's an unused fallback key on the server, nothing to do here.
|
||||
let unused_fallback_keys = &[DeviceKeyAlgorithm::SignedCurve25519];
|
||||
// There's no unused fallback key on the server, but our initial fallback key
|
||||
// did not yet expire.
|
||||
let unused_fallback_keys = &[];
|
||||
account.update_key_counts(&one_time_keys, Some(unused_fallback_keys.as_ref()));
|
||||
let (_, _, fallback_keys) = account.keys_for_upload();
|
||||
assert!(fallback_keys.is_empty());
|
||||
assert!(
|
||||
fallback_keys.is_empty(),
|
||||
"We should not upload new fallback keys unless our current fallback key expires."
|
||||
);
|
||||
|
||||
let fallback_key_timestamp =
|
||||
account.fallback_creation_timestamp.unwrap().to_system_time().unwrap()
|
||||
- Duration::from_secs(3600 * 24 * 30);
|
||||
|
||||
account.fallback_creation_timestamp =
|
||||
Some(MilliSecondsSinceUnixEpoch::from_system_time(fallback_key_timestamp).unwrap());
|
||||
|
||||
account.update_key_counts(&one_time_keys, None);
|
||||
let (_, _, fallback_keys) = account.keys_for_upload();
|
||||
assert!(
|
||||
!fallback_keys.is_empty(),
|
||||
"Now that our fallback key has expired, we should try to upload a new one, even if the \
|
||||
server supposedly doesn't support fallback keys anymore"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -136,6 +136,11 @@ pub struct BackedUpRoomKey {
|
||||
|
||||
/// Chain of Curve25519 keys through which this session was forwarded, via
|
||||
/// m.forwarded_room_key events.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_curve_key_vec",
|
||||
serialize_with = "serialize_curve_key_vec"
|
||||
)]
|
||||
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
|
||||
}
|
||||
|
||||
@@ -245,3 +250,33 @@ impl TryFrom<ForwardedRoomKeyContent> for ExportedRoomKey {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::BackedUpRoomKey;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_backed_up_key() {
|
||||
let data = json!({
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"room_id": "!room:id",
|
||||
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
|
||||
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
|
||||
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": ["DBPC2zr6c9qimo9YRFK3RVr0Two/I6ODb9mbsToZN3Q", "bBc/qzZFOOKshMMT+i4gjS/gWPDoKfGmETs9yfw9430"]
|
||||
});
|
||||
|
||||
let backed_up_room_key: BackedUpRoomKey = serde_json::from_value(data)
|
||||
.expect("We should be able to deserialize the backed up room key.");
|
||||
assert_eq!(
|
||||
backed_up_room_key.forwarding_curve25519_key_chain.len(),
|
||||
2,
|
||||
"The number of forwarding Curve25519 chains should be two."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,10 @@ use crate::{
|
||||
ReadOnlyDevice, ToDeviceRequest,
|
||||
};
|
||||
|
||||
const ROTATION_PERIOD: Duration = Duration::from_millis(604800000);
|
||||
const ONE_HOUR: Duration = Duration::from_secs(60 * 60);
|
||||
const ONE_WEEK: Duration = Duration::from_secs(60 * 60 * 24 * 7);
|
||||
|
||||
const ROTATION_PERIOD: Duration = ONE_WEEK;
|
||||
const ROTATION_MESSAGES: u64 = 100;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -415,15 +418,27 @@ impl OutboundGroupSession {
|
||||
fn elapsed(&self) -> bool {
|
||||
let creation_time = Duration::from_secs(self.creation_time.get().into());
|
||||
let now = Duration::from_secs(SecondsSinceUnixEpoch::now().get().into());
|
||||
|
||||
// Since the encryption settings are provided by users and not
|
||||
// checked someone could set a really low rotation period so
|
||||
// clamp it to an hour.
|
||||
now.checked_sub(creation_time)
|
||||
.map(|elapsed| elapsed >= max(self.settings.rotation_period, Duration::from_secs(3600)))
|
||||
.map(|elapsed| elapsed >= self.safe_rotation_period())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Returns the rotation_period_ms that was set for this session, clamped
|
||||
/// to be no less than one hour.
|
||||
///
|
||||
/// This is to prevent a malicious or careless user causing sessions to be
|
||||
/// rotated very frequently.
|
||||
///
|
||||
/// The feature flag `_disable-minimum-rotation-period-ms` can
|
||||
/// be used to prevent this behaviour (which can be useful for tests).
|
||||
fn safe_rotation_period(&self) -> Duration {
|
||||
if cfg!(feature = "_disable-minimum-rotation-period-ms") {
|
||||
self.settings.rotation_period
|
||||
} else {
|
||||
max(self.settings.rotation_period, ONE_HOUR)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the session has expired and if it should be rotated.
|
||||
///
|
||||
/// A session will expire after some time or if enough messages have been
|
||||
@@ -735,20 +750,16 @@ pub struct PickledOutboundGroupSession {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{sync::atomic::Ordering, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
device_id,
|
||||
events::room::{
|
||||
encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility,
|
||||
message::RoomMessageEventContent,
|
||||
},
|
||||
room_id, uint, user_id, EventEncryptionAlgorithm,
|
||||
uint, EventEncryptionAlgorithm,
|
||||
};
|
||||
|
||||
use super::{EncryptionSettings, ROTATION_MESSAGES, ROTATION_PERIOD};
|
||||
use crate::{Account, MegolmError};
|
||||
|
||||
#[test]
|
||||
fn test_encryption_settings_conversion() {
|
||||
@@ -768,78 +779,208 @@ mod tests {
|
||||
assert_eq!(settings.rotation_period_msgs, 500);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_arch = "wasm32"))]
|
||||
async fn test_expiration() -> Result<(), MegolmError> {
|
||||
use ruma::{serde::Raw, SecondsSinceUnixEpoch};
|
||||
mod expiration {
|
||||
use std::{sync::atomic::Ordering, time::Duration};
|
||||
|
||||
let settings = EncryptionSettings { rotation_period_msgs: 1, ..Default::default() };
|
||||
|
||||
let account =
|
||||
Account::with_device_id(user_id!("@alice:example.org"), device_id!("DEVICEID"))
|
||||
.static_data;
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!session.expired());
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
assert!(session.expired());
|
||||
|
||||
let settings = EncryptionSettings {
|
||||
rotation_period: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
device_id, events::room::message::RoomMessageEventContent, room_id, serde::Raw, uint,
|
||||
user_id, SecondsSinceUnixEpoch,
|
||||
};
|
||||
|
||||
let (mut session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
use crate::{olm::OutboundGroupSession, Account, EncryptionSettings, MegolmError};
|
||||
|
||||
assert!(!session.expired());
|
||||
const TWO_HOURS: Duration = Duration::from_secs(60 * 60 * 2);
|
||||
|
||||
let now = SecondsSinceUnixEpoch::now();
|
||||
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(3600));
|
||||
assert!(session.expired());
|
||||
|
||||
let settings = EncryptionSettings { rotation_period_msgs: 0, ..Default::default() };
|
||||
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!session.expired());
|
||||
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
#[async_test]
|
||||
async fn session_is_not_expired_if_no_messages_sent_and_no_time_passed() {
|
||||
// Given a session that expires after one message
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 1,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
assert!(session.expired());
|
||||
|
||||
let settings = EncryptionSettings { rotation_period_msgs: 100_000, ..Default::default() };
|
||||
// When we send no messages at all
|
||||
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
// Then it is not expired
|
||||
assert!(!session.expired());
|
||||
}
|
||||
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(1000, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(9999, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(10_000, Ordering::SeqCst);
|
||||
assert!(session.expired());
|
||||
#[async_test]
|
||||
async fn session_is_expired_if_we_rotate_every_message_and_one_was_sent(
|
||||
) -> Result<(), MegolmError> {
|
||||
// Given a session that expires after one message
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 1,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
// When we send a message
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Then the session is expired
|
||||
assert!(session.expired());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_with_rotation_period_is_not_expired_after_no_time() {
|
||||
// Given a session with a 2h expiration
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period: TWO_HOURS,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When we don't allow any time to pass
|
||||
|
||||
// Then it is not expired
|
||||
assert!(!session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_is_expired_after_rotation_period() {
|
||||
// Given a session with a 2h expiration
|
||||
let mut session = create_session(EncryptionSettings {
|
||||
rotation_period: TWO_HOURS,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When 3 hours have passed
|
||||
let now = SecondsSinceUnixEpoch::now();
|
||||
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(10800));
|
||||
|
||||
// Then the session is expired
|
||||
assert!(session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(not(feature = "_disable-minimum-rotation-period-ms"))]
|
||||
async fn session_does_not_expire_under_one_hour_even_if_we_ask_for_shorter() {
|
||||
// Given a session with a 100ms expiration
|
||||
let mut session = create_session(EncryptionSettings {
|
||||
rotation_period: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When less than an hour has passed
|
||||
let now = SecondsSinceUnixEpoch::now();
|
||||
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(1800));
|
||||
|
||||
// Then the session is not expired: we enforce a minimum of 1 hour
|
||||
assert!(!session.expired());
|
||||
|
||||
// But when more than an hour has passed
|
||||
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(3601));
|
||||
|
||||
// Then the session is expired
|
||||
assert!(session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "_disable-minimum-rotation-period-ms")]
|
||||
async fn with_disable_minrotperiod_feature_sessions_can_expire_quickly() {
|
||||
// Given a session with a 100ms expiration
|
||||
let mut session = create_session(EncryptionSettings {
|
||||
rotation_period: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When less than an hour has passed
|
||||
let now = SecondsSinceUnixEpoch::now();
|
||||
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(1800));
|
||||
|
||||
// Then the session is expired: the feature flag has prevented us enforcing a
|
||||
// minimum
|
||||
assert!(session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_with_zero_msgs_rotation_is_not_expired_initially() {
|
||||
// Given a session that is supposed to expire after zero messages
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 0,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When we send no messages
|
||||
|
||||
// Then the session is not expired: we are protected against this nonsensical
|
||||
// setup
|
||||
assert!(!session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_with_zero_msgs_rotation_expires_after_one_message(
|
||||
) -> Result<(), MegolmError> {
|
||||
// Given a session that is supposed to expire after zero messages
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 0,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When we send a message
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Then the session is expired: we treated rotation_period_msgs=0 as if it were
|
||||
// =1
|
||||
assert!(session.expired());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_expires_after_10k_messages_even_if_we_ask_for_more() {
|
||||
// Given we asked to expire after 100K messages
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 100_000,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// Sanity: it does not expire after <10K messages
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(1000, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(9999, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
|
||||
// When we have sent >= 10K messages
|
||||
session.message_count.store(10_000, Ordering::SeqCst);
|
||||
|
||||
// Then it is considered expired: we enforce a maximum of 10K messages before
|
||||
// rotation.
|
||||
assert!(session.expired());
|
||||
}
|
||||
|
||||
async fn create_session(settings: EncryptionSettings) -> OutboundGroupSession {
|
||||
let account =
|
||||
Account::with_device_id(user_id!("@alice:example.org"), device_id!("DEVICEID"))
|
||||
.static_data;
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ pub(crate) mod tests {
|
||||
let alice = Account::with_device_id(alice_id(), alice_device_id());
|
||||
let mut bob = Account::with_device_id(bob_id(), bob_device_id());
|
||||
|
||||
bob.generate_one_time_keys_helper(1);
|
||||
bob.generate_one_time_keys(1);
|
||||
let one_time_key = *bob.one_time_keys().values().next().unwrap();
|
||||
let sender_key = bob.identity_keys().curve25519;
|
||||
let session = alice.create_outbound_session_helper(
|
||||
@@ -116,7 +116,7 @@ pub(crate) mod tests {
|
||||
assert!(!one_time_keys.is_empty());
|
||||
assert_ne!(account.max_one_time_keys(), 0);
|
||||
|
||||
account.generate_one_time_keys_helper(10);
|
||||
account.generate_one_time_keys(10);
|
||||
let one_time_keys = account.one_time_keys();
|
||||
|
||||
assert_ne!(one_time_keys.values().len(), 0);
|
||||
@@ -133,7 +133,7 @@ pub(crate) mod tests {
|
||||
let mut alice = Account::with_device_id(alice_id(), alice_device_id());
|
||||
let bob = Account::with_device_id(bob_id(), bob_device_id());
|
||||
let alice_keys = alice.identity_keys();
|
||||
alice.generate_one_time_keys_helper(1);
|
||||
alice.generate_one_time_keys(1);
|
||||
let one_time_keys = alice.one_time_keys();
|
||||
alice.mark_keys_as_published();
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use ruma::{serde::Raw, OwnedDeviceId, OwnedUserId, SecondsSinceUnixEpoch};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{field::debug, instrument, trace, Span};
|
||||
use tracing::{debug, Span};
|
||||
use vodozemac::{
|
||||
olm::{DecryptionError, OlmMessage, Session as InnerSession, SessionConfig, SessionPickle},
|
||||
Curve25519PublicKey,
|
||||
@@ -79,13 +79,12 @@ impl Session {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The Olm message that should be decrypted.
|
||||
#[instrument(skip_all, fields(session))]
|
||||
pub async fn decrypt(&mut self, message: &OlmMessage) -> Result<String, DecryptionError> {
|
||||
let mut inner = self.inner.lock().await;
|
||||
let plaintext = inner.decrypt(message)?;
|
||||
Span::current().record("session_id", inner.session_id());
|
||||
|
||||
Span::current().record("session", debug(inner));
|
||||
trace!("Decrypted a Olm message");
|
||||
let plaintext = inner.decrypt(message)?;
|
||||
debug!(session=?inner, "Decrypted an Olm message");
|
||||
|
||||
let plaintext = String::from_utf8_lossy(&plaintext).to_string();
|
||||
|
||||
@@ -126,11 +125,9 @@ impl Session {
|
||||
/// * `plaintext` - The plaintext that should be encrypted.
|
||||
pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage {
|
||||
let mut session = self.inner.lock().await;
|
||||
|
||||
Span::current().record("session", debug(&session));
|
||||
let message = session.encrypt(plaintext);
|
||||
|
||||
self.last_use_time = SecondsSinceUnixEpoch::now();
|
||||
debug!(?session, "Successfully encrypted an event");
|
||||
message
|
||||
}
|
||||
|
||||
|
||||
@@ -218,8 +218,6 @@ pub enum OutgoingRequests {
|
||||
/// A room message request, usually for sending in-room interactive
|
||||
/// verification events.
|
||||
RoomMessage(RoomMessageRequest),
|
||||
/// A request that will back up a batch of room keys to the server.
|
||||
KeysBackup(KeysBackupRequest),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -235,12 +233,6 @@ impl From<KeysQueryRequest> for OutgoingRequests {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysBackupRequest> for OutgoingRequests {
|
||||
fn from(r: KeysBackupRequest) -> Self {
|
||||
Self::KeysBackup(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysClaimRequest> for OutgoingRequests {
|
||||
fn from(r: KeysClaimRequest) -> Self {
|
||||
Self::KeysClaim(r)
|
||||
|
||||
@@ -245,29 +245,40 @@ impl SecretStorageKey {
|
||||
fn check_zero_message(&self) -> Result<(), DecodeError> {
|
||||
match &self.storage_key_info.algorithm {
|
||||
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(properties) => {
|
||||
if properties.iv.as_bytes().len() != IV_SIZE {
|
||||
Err(DecodeError::IvLength(IV_SIZE, properties.iv.as_bytes().len()))
|
||||
} else {
|
||||
let mut iv_array = [0u8; 16];
|
||||
iv_array.copy_from_slice(properties.iv.as_bytes());
|
||||
|
||||
// I'm not particularly convinced that this couldn't have been done simpler. Why
|
||||
// do we need to reproduce the ciphertext? Couldn't we just generate the MAC tag
|
||||
// using the `ZERO_MESSAGE`?
|
||||
let (Some(iv), Some(mac)) = (&properties.iv, &properties.mac) else {
|
||||
// The IV and/or MAC are missing from the account data
|
||||
// content. As the [spec] says, we have to assume that the
|
||||
// key is valid.
|
||||
//
|
||||
// If someone is reading this and is designing a new secret encryption
|
||||
// algorithm, please consider the above suggestion.
|
||||
let key = AesHmacSha2Key::from_secret_storage_key(&self.secret_key, "");
|
||||
let ciphertext = key.apply_keystream(Self::ZERO_MESSAGE.to_vec(), &iv_array);
|
||||
let expected_mac = HmacSha256Mac::from_slice(properties.mac.as_bytes())
|
||||
.ok_or_else(|| {
|
||||
DecodeError::MacLength(MAC_SIZE, properties.mac.as_bytes().len())
|
||||
})?;
|
||||
// [spec]: https://spec.matrix.org/unstable/client-server-api/#msecret_storagev1aes-hmac-sha2
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
key.verify_mac(&ciphertext, expected_mac.as_bytes())?;
|
||||
let iv = iv.as_bytes();
|
||||
let iv_length = iv.len();
|
||||
|
||||
Ok(())
|
||||
if iv_length != IV_SIZE {
|
||||
return Err(DecodeError::IvLength(IV_SIZE, iv_length));
|
||||
}
|
||||
|
||||
let mut iv_array = [0u8; 16];
|
||||
iv_array.copy_from_slice(iv);
|
||||
|
||||
// I'm not particularly convinced that this couldn't have been done simpler.
|
||||
// Why do we need to reproduce the ciphertext?
|
||||
// Couldn't we just generate the MAC tag
|
||||
// using the `ZERO_MESSAGE`?
|
||||
//
|
||||
// If someone is reading this and is designing a new secret encryption
|
||||
// algorithm, please consider the above suggestion.
|
||||
let key = AesHmacSha2Key::from_secret_storage_key(&self.secret_key, "");
|
||||
let ciphertext = key.apply_keystream(Self::ZERO_MESSAGE.to_vec(), &iv_array);
|
||||
let expected_mac = HmacSha256Mac::from_slice(mac.as_bytes())
|
||||
.ok_or_else(|| DecodeError::MacLength(MAC_SIZE, mac.as_bytes().len()))?;
|
||||
|
||||
key.verify_mac(&ciphertext, expected_mac.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
custom => Err(DecodeError::UnsupportedAlgorithm(custom.algorithm().to_owned())),
|
||||
}
|
||||
@@ -283,7 +294,7 @@ impl SecretStorageKey {
|
||||
SecretStorageKeyEventContent::new(
|
||||
key_id,
|
||||
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
|
||||
SecretStorageV1AesHmacSha2Properties::new(iv, mac),
|
||||
SecretStorageV1AesHmacSha2Properties::new(Some(iv), Some(mac)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -743,6 +754,24 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
/// The `iv` and `mac` properties within the `m.secret_storage.key.*`
|
||||
/// content are optional, and the spec says we must assume the
|
||||
/// passphrase is correct in that case.
|
||||
#[test]
|
||||
fn accepts_any_passphrase_if_mac_and_iv_are_missing() {
|
||||
let mut content = SecretStorageKeyEventContent::new(
|
||||
"my_new_key_id".to_owned(),
|
||||
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
|
||||
SecretStorageV1AesHmacSha2Properties::new(None, None),
|
||||
),
|
||||
);
|
||||
content.passphrase =
|
||||
Some(PassPhrase::new("salty goodness".to_owned(), UInt::new_saturating(100)));
|
||||
|
||||
SecretStorageKey::from_account_data("It's a secret to nobody", content.to_owned())
|
||||
.expect("Should accept any passphrase");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base58_parsing() {
|
||||
const DECODED_KEY: [u8; 32] = [
|
||||
@@ -862,8 +891,8 @@ mod test {
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e".to_owned(),
|
||||
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
|
||||
SecretStorageV1AesHmacSha2Properties::new(
|
||||
Base64::new(vec![0u8; 14]),
|
||||
Base64::new(vec![0u8; 32]),
|
||||
Some(Base64::new(vec![0u8; 14])),
|
||||
Some(Base64::new(vec![0u8; 32])),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -878,8 +907,8 @@ mod test {
|
||||
"bmur2d9ypPUH1msSwCxQOJkuKRmJI55e".to_owned(),
|
||||
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
|
||||
SecretStorageV1AesHmacSha2Properties::new(
|
||||
Base64::new(vec![0u8; 16]),
|
||||
Base64::new(vec![0u8; 10]),
|
||||
Some(Base64::new(vec![0u8; 16])),
|
||||
Some(Base64::new(vec![0u8; 10])),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -343,6 +343,7 @@ impl GroupSessionManager {
|
||||
/// Returns information indicating whether the session needs to be rotated
|
||||
/// and the list of users/devices that should receive or not the session
|
||||
/// (with withheld reason).
|
||||
#[instrument(skip_all)]
|
||||
pub async fn collect_session_recipients(
|
||||
&self,
|
||||
users: impl Iterator<Item = &UserId>,
|
||||
@@ -436,6 +437,12 @@ impl GroupSessionManager {
|
||||
shared.difference(&recipient_device_ids).collect::<BTreeSet<_>>();
|
||||
|
||||
should_rotate = !newly_deleted_or_blacklisted.is_empty();
|
||||
if should_rotate {
|
||||
debug!(
|
||||
"Rotating a room key due to these devices being deleted/blacklisted {:?}",
|
||||
newly_deleted_or_blacklisted,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -443,6 +450,15 @@ impl GroupSessionManager {
|
||||
withheld_devices.extend(withheld_recipients);
|
||||
}
|
||||
|
||||
if should_rotate {
|
||||
debug!(
|
||||
should_rotate,
|
||||
user_left,
|
||||
visibility_changed,
|
||||
algorithm_changed,
|
||||
"Rotating room key to protect room history",
|
||||
);
|
||||
}
|
||||
trace!(should_rotate, "Done calculating group session recipients");
|
||||
|
||||
Ok(CollectRecipientsResult { should_rotate, devices, withheld_devices })
|
||||
|
||||
@@ -718,7 +718,7 @@ mod tests {
|
||||
|
||||
assert!(request.one_time_keys.contains_key(bob.user_id()));
|
||||
|
||||
bob.generate_one_time_keys_helper(1);
|
||||
bob.generate_one_time_keys(1);
|
||||
let one_time = bob.signed_one_time_keys();
|
||||
assert!(!one_time.is_empty());
|
||||
bob.mark_keys_as_published();
|
||||
@@ -854,7 +854,7 @@ mod tests {
|
||||
.store
|
||||
.with_transaction(|mut tr| async {
|
||||
let manager_account = tr.account().await.unwrap();
|
||||
let res = bob.create_session_for(manager_account).await;
|
||||
let res = bob.create_session_for_test_helper(manager_account).await;
|
||||
Ok((tr, res))
|
||||
})
|
||||
.await
|
||||
@@ -882,7 +882,7 @@ mod tests {
|
||||
|
||||
assert!(request.one_time_keys.contains_key(bob.user_id()));
|
||||
|
||||
bob.generate_one_time_keys_helper(1);
|
||||
bob.generate_one_time_keys(1);
|
||||
let one_time = bob.signed_one_time_keys();
|
||||
assert!(!one_time.is_empty());
|
||||
bob.mark_keys_as_published();
|
||||
@@ -1009,7 +1009,7 @@ mod tests {
|
||||
// Since alice is timed out, we won't claim keys for her.
|
||||
assert!(manager.get_missing_sessions(iter::once(alice)).await.unwrap().is_none());
|
||||
|
||||
alice_account.generate_one_time_keys_helper(1);
|
||||
alice_account.generate_one_time_keys(1);
|
||||
let one_time = alice_account.signed_one_time_keys();
|
||||
assert!(!one_time.is_empty());
|
||||
|
||||
|
||||
@@ -4,44 +4,40 @@ macro_rules! cryptostore_integration_tests {
|
||||
() => {
|
||||
mod cryptostore_integration_tests {
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::time::Duration;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
device_id,
|
||||
encryption::SignedKey,
|
||||
events::secret::request::SecretName,
|
||||
room_id,
|
||||
serde::{Base64, Raw},
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
user_id, DeviceId, JsOption, OwnedDeviceId, OwnedUserId, RoomId, TransactionId, UserId
|
||||
device_id, events::secret::request::SecretName, room_id, serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices, user_id, DeviceId, RoomId, TransactionId, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use $crate::{
|
||||
olm::{
|
||||
Curve25519PublicKey, InboundGroupSession, OlmMessageHash,
|
||||
PrivateCrossSigningIdentity, Account, Session,
|
||||
Account, Curve25519PublicKey, InboundGroupSession, OlmMessageHash,
|
||||
PrivateCrossSigningIdentity, Session,
|
||||
},
|
||||
store::{
|
||||
BackupKeys, Changes, CryptoStore, DeviceChanges,
|
||||
GossipRequest, IdentityChanges, BackupDecryptionKey, RoomSettings, PendingChanges,
|
||||
BackupDecryptionKey, Changes, CryptoStore, DeviceChanges, GossipRequest,
|
||||
IdentityChanges, PendingChanges, RoomSettings,
|
||||
},
|
||||
testing::{get_device, get_other_identity, get_own_identity},
|
||||
types::{
|
||||
events::{
|
||||
dummy::DummyEventContent,
|
||||
olm_v1::{DecryptedSecretSendEvent, OlmV1Keys},
|
||||
room_key_request::MegolmV1AesSha2Content,
|
||||
room_key_withheld::{
|
||||
CommonWithheldCodeContent, MegolmV1AesSha2WithheldContent,
|
||||
RoomKeyWithheldContent, WithheldCode,
|
||||
},
|
||||
olm_v1::{DecryptedSecretSendEvent, OlmV1Keys},
|
||||
secret_send::SecretSendContent,
|
||||
ToDeviceEvent,
|
||||
},
|
||||
EventEncryptionAlgorithm,
|
||||
},
|
||||
ReadOnlyDevice, SecretInfo, ToDeviceRequest, TrackedUser, GossippedSecret,
|
||||
GossippedSecret, ReadOnlyDevice, SecretInfo, ToDeviceRequest, TrackedUser,
|
||||
};
|
||||
|
||||
use super::get_store;
|
||||
@@ -79,16 +75,15 @@ macro_rules! cryptostore_integration_tests {
|
||||
let alice = Account::with_device_id(alice_id(), alice_device_id());
|
||||
let mut bob = Account::with_device_id(bob_id(), bob_device_id());
|
||||
|
||||
bob.generate_one_time_keys_helper(1);
|
||||
bob.generate_one_time_keys(1);
|
||||
let one_time_key = *bob.one_time_keys().values().next().unwrap();
|
||||
let sender_key = bob.identity_keys().curve25519;
|
||||
let session = alice
|
||||
.create_outbound_session_helper(
|
||||
Default::default(),
|
||||
sender_key,
|
||||
one_time_key,
|
||||
false,
|
||||
);
|
||||
let session = alice.create_outbound_session_helper(
|
||||
Default::default(),
|
||||
sender_key,
|
||||
one_time_key,
|
||||
false,
|
||||
);
|
||||
|
||||
(alice, session)
|
||||
}
|
||||
@@ -101,7 +96,7 @@ macro_rules! cryptostore_integration_tests {
|
||||
let account = get_account();
|
||||
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account), })
|
||||
.save_pending_changes(PendingChanges { account: Some(account) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
assert!(store.get_static_account().is_some());
|
||||
@@ -114,7 +109,10 @@ macro_rules! cryptostore_integration_tests {
|
||||
assert!(store.load_account().await.unwrap().is_none());
|
||||
let account = get_account();
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account), }).await.expect("Can't save account");
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
assert!(store.get_static_account().is_some());
|
||||
}
|
||||
|
||||
@@ -123,7 +121,10 @@ macro_rules! cryptostore_integration_tests {
|
||||
let store = get_store("load_account", None).await;
|
||||
let account = get_account();
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
let loaded_account = store.load_account().await.expect("Can't load account");
|
||||
let loaded_account = loaded_account.unwrap();
|
||||
@@ -137,7 +138,10 @@ macro_rules! cryptostore_integration_tests {
|
||||
get_store("load_account_with_passphrase", Some("secret_passphrase")).await;
|
||||
let account = get_account();
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
let loaded_account = store.load_account().await.expect("Can't load account");
|
||||
let loaded_account = loaded_account.unwrap();
|
||||
@@ -150,12 +154,18 @@ macro_rules! cryptostore_integration_tests {
|
||||
let store = get_store("save_and_share_account", None).await;
|
||||
let mut account = get_account();
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
account.mark_as_shared();
|
||||
account.update_uploaded_key_count(50);
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
let loaded_account = store.load_account().await.expect("Can't load account");
|
||||
let loaded_account = loaded_account.unwrap();
|
||||
@@ -168,7 +178,10 @@ macro_rules! cryptostore_integration_tests {
|
||||
async fn load_sessions() {
|
||||
let store = get_store("load_sessions", None).await;
|
||||
let (account, session) = get_account_and_session().await;
|
||||
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
|
||||
store
|
||||
.save_pending_changes(PendingChanges { account: Some(account.deep_clone()) })
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
|
||||
|
||||
@@ -187,26 +200,37 @@ macro_rules! cryptostore_integration_tests {
|
||||
#[async_test]
|
||||
async fn add_and_save_session() {
|
||||
let store_name = "add_and_save_session";
|
||||
let store = get_store(store_name, None).await;
|
||||
let (account, session) = get_account_and_session().await;
|
||||
let sender_key = session.sender_key.to_base64();
|
||||
let session_id = session.session_id().to_owned();
|
||||
|
||||
store.save_pending_changes(PendingChanges { account: Some(account.deep_clone()), }).await.expect("Can't save account");
|
||||
// Given we created a session and saved it in the store
|
||||
let (session_id, account, sender_key) = {
|
||||
let store = get_store(store_name, None).await;
|
||||
let (account, session) = get_account_and_session().await;
|
||||
let sender_key = session.sender_key.to_base64();
|
||||
let session_id = session.session_id().to_owned();
|
||||
|
||||
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
|
||||
store.save_changes(changes).await.unwrap();
|
||||
store
|
||||
.save_pending_changes(PendingChanges {
|
||||
account: Some(account.deep_clone()),
|
||||
})
|
||||
.await
|
||||
.expect("Can't save account");
|
||||
|
||||
let sessions = store.get_sessions(&sender_key).await.unwrap().unwrap();
|
||||
let sessions_lock = sessions.lock().await;
|
||||
let session = &sessions_lock[0];
|
||||
let changes = Changes { sessions: vec![session.clone()], ..Default::default() };
|
||||
store.save_changes(changes).await.unwrap();
|
||||
|
||||
assert_eq!(session_id, session.session_id());
|
||||
let sessions = store.get_sessions(&sender_key).await.unwrap().unwrap();
|
||||
let sessions_lock = sessions.lock().await;
|
||||
let session = &sessions_lock[0];
|
||||
|
||||
drop(store);
|
||||
assert_eq!(session_id, session.session_id());
|
||||
|
||||
(session_id, account, sender_key)
|
||||
};
|
||||
|
||||
// When we reload the store
|
||||
let store = get_store(store_name, None).await;
|
||||
|
||||
// Then the same account and session info was reloaded
|
||||
let loaded_account = store.load_account().await.unwrap().unwrap();
|
||||
assert_eq!(account, loaded_account);
|
||||
|
||||
@@ -220,36 +244,50 @@ macro_rules! cryptostore_integration_tests {
|
||||
#[async_test]
|
||||
async fn load_outbound_group_session() {
|
||||
let dir = "load_outbound_group_session";
|
||||
let (account, store) = get_loaded_store(dir.clone()).await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
assert!(store.get_outbound_group_session(&room_id).await.unwrap().is_none());
|
||||
|
||||
let (session, _) = account.create_group_session_pair_with_defaults(&room_id).await;
|
||||
// Given we saved an outbound group session
|
||||
{
|
||||
let (account, store) = get_loaded_store(dir.clone()).await;
|
||||
assert!(
|
||||
store.get_outbound_group_session(&room_id).await.unwrap().is_none(),
|
||||
"Initially there should be no outbound group session"
|
||||
);
|
||||
|
||||
let user_id = user_id!("@example:localhost");
|
||||
let request = ToDeviceRequest::new(
|
||||
user_id,
|
||||
DeviceIdOrAllDevices::AllDevices,
|
||||
"m.dummy",
|
||||
Raw::from_json(to_raw_value(&DummyEventContent::new()).unwrap()),
|
||||
);
|
||||
let (session, _) =
|
||||
account.create_group_session_pair_with_defaults(&room_id).await;
|
||||
|
||||
session.add_request(TransactionId::new(), request.into(), Default::default());
|
||||
let user_id = user_id!("@example:localhost");
|
||||
let request = ToDeviceRequest::new(
|
||||
user_id,
|
||||
DeviceIdOrAllDevices::AllDevices,
|
||||
"m.dummy",
|
||||
Raw::from_json(to_raw_value(&DummyEventContent::new()).unwrap()),
|
||||
);
|
||||
|
||||
let changes = Changes {
|
||||
outbound_group_sessions: vec![session.clone()],
|
||||
..Default::default()
|
||||
};
|
||||
session.add_request(TransactionId::new(), request.into(), Default::default());
|
||||
|
||||
store.save_changes(changes).await.expect("Can't save group session");
|
||||
let changes = Changes {
|
||||
outbound_group_sessions: vec![session.clone()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
drop(store);
|
||||
store.save_changes(changes).await.expect("Can't save group session");
|
||||
assert!(
|
||||
store.get_outbound_group_session(&room_id).await.unwrap().is_some(),
|
||||
"Sanity: after we've saved one, there should be an outbound_group_session"
|
||||
);
|
||||
}
|
||||
|
||||
// When we reload the account
|
||||
let store = get_store(dir, None).await;
|
||||
|
||||
store.load_account().await.unwrap();
|
||||
|
||||
assert!(store.get_outbound_group_session(&room_id).await.unwrap().is_some());
|
||||
// Then the saved session is restored
|
||||
assert!(
|
||||
store.get_outbound_group_session(&room_id).await.unwrap().is_some(),
|
||||
"The outbound_group_session should have been loaded"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -285,10 +323,10 @@ macro_rules! cryptostore_integration_tests {
|
||||
.unwrap();
|
||||
assert_eq!(session, loaded_session);
|
||||
assert_eq!(store.get_inbound_group_sessions().await.unwrap().len(), 1);
|
||||
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 1);
|
||||
assert_eq!(store.inbound_group_session_counts().await.unwrap().backed_up, 0);
|
||||
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 1);
|
||||
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().backed_up, 0);
|
||||
|
||||
let to_back_up = store.inbound_group_sessions_for_backup(1).await.unwrap();
|
||||
let to_back_up = store.inbound_group_sessions_for_backup("bkpver", 1).await.unwrap();
|
||||
assert_eq!(to_back_up, vec![session])
|
||||
}
|
||||
|
||||
@@ -299,19 +337,15 @@ macro_rules! cryptostore_integration_tests {
|
||||
get_loaded_store("mark_inbound_group_sessions_as_backed_up").await;
|
||||
let room_id = &room_id!("!test:localhost");
|
||||
let mut sessions: Vec<InboundGroupSession> = Vec::with_capacity(10);
|
||||
for i in 0..10 {
|
||||
for _i in 0..10 {
|
||||
sessions.push(account.create_group_session_pair_with_defaults(room_id).await.1);
|
||||
}
|
||||
let changes = Changes { inbound_group_sessions: sessions.clone(), ..Default::default() };
|
||||
store.save_changes(changes).await.expect("Can't save group session");
|
||||
assert_eq!(store.inbound_group_sessions_for_backup(100).await.unwrap().len(), 10);
|
||||
|
||||
fn session_info(session: &InboundGroupSession) -> (&RoomId, &str) {
|
||||
(&session.room_id(), &session.session_id())
|
||||
}
|
||||
assert_eq!(store.inbound_group_sessions_for_backup("bkpver", 100).await.unwrap().len(), 10);
|
||||
|
||||
// When I mark some as backed up
|
||||
let x = store.mark_inbound_group_sessions_as_backed_up(&[
|
||||
store.mark_inbound_group_sessions_as_backed_up("bkpver", &[
|
||||
session_info(&sessions[1]),
|
||||
session_info(&sessions[3]),
|
||||
session_info(&sessions[5]),
|
||||
@@ -319,9 +353,8 @@ macro_rules! cryptostore_integration_tests {
|
||||
session_info(&sessions[9]),
|
||||
]).await.expect("Failed to mark sessions as backed up");
|
||||
|
||||
|
||||
// And ask which still need backing up
|
||||
let to_back_up = store.inbound_group_sessions_for_backup(10).await.unwrap();
|
||||
let to_back_up = store.inbound_group_sessions_for_backup("bkpver", 10).await.unwrap();
|
||||
let needs_backing_up = |i: usize| to_back_up.iter().any(|s| s.session_id() == sessions[i].session_id());
|
||||
|
||||
// Then the sessions we said were backed up no longer need backing up
|
||||
@@ -344,27 +377,35 @@ macro_rules! cryptostore_integration_tests {
|
||||
async fn reset_inbound_group_session_for_backup() {
|
||||
let (account, store) =
|
||||
get_loaded_store("reset_inbound_group_session_for_backup").await;
|
||||
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 0);
|
||||
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 0);
|
||||
|
||||
let room_id = &room_id!("!test:localhost");
|
||||
let (_, session) = account.create_group_session_pair_with_defaults(room_id).await;
|
||||
|
||||
session.mark_as_backed_up();
|
||||
|
||||
let changes =
|
||||
Changes { inbound_group_sessions: vec![session.clone()], ..Default::default() };
|
||||
|
||||
store.save_changes(changes).await.expect("Can't save group session");
|
||||
|
||||
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 1);
|
||||
assert_eq!(store.inbound_group_session_counts().await.unwrap().backed_up, 1);
|
||||
// Given we have backed up our session
|
||||
store
|
||||
.mark_inbound_group_sessions_as_backed_up("bkpver1", &[session_info(&session)])
|
||||
.await
|
||||
.expect("Failed to mark_inbound_group_sessions_as_backed_up.");
|
||||
|
||||
let to_back_up = store.inbound_group_sessions_for_backup(1).await.unwrap();
|
||||
assert_eq!(store.inbound_group_session_counts(Some("bkpver1")).await.unwrap().total, 1);
|
||||
assert_eq!(store.inbound_group_session_counts(Some("bkpver1")).await.unwrap().backed_up, 1);
|
||||
|
||||
// Sanity: before resetting, we have nothing to back up
|
||||
let to_back_up = store.inbound_group_sessions_for_backup("bkpver1", 1).await.unwrap();
|
||||
assert_eq!(to_back_up, vec![]);
|
||||
|
||||
// When we reset the backup
|
||||
store.reset_backup_state().await.unwrap();
|
||||
|
||||
let to_back_up = store.inbound_group_sessions_for_backup(1).await.unwrap();
|
||||
// Then after resetting, even if we supply the same backup version number, we need
|
||||
// to back up the session
|
||||
let to_back_up = store.inbound_group_sessions_for_backup("bkpver1", 1).await.unwrap();
|
||||
assert_eq!(to_back_up, vec![session]);
|
||||
}
|
||||
|
||||
@@ -377,7 +418,7 @@ macro_rules! cryptostore_integration_tests {
|
||||
let room_id = &room_id!("!test:localhost");
|
||||
let (_, session) = account.create_group_session_pair_with_defaults(room_id).await;
|
||||
|
||||
let mut export = session.export().await;
|
||||
let export = session.export().await;
|
||||
|
||||
let session = InboundGroupSession::from_export(&export).unwrap();
|
||||
|
||||
@@ -398,10 +439,10 @@ macro_rules! cryptostore_integration_tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(session, loaded_session);
|
||||
let export = loaded_session.export().await;
|
||||
loaded_session.export().await;
|
||||
|
||||
assert_eq!(store.get_inbound_group_sessions().await.unwrap().len(), 1);
|
||||
assert_eq!(store.inbound_group_session_counts().await.unwrap().total, 1);
|
||||
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 1);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -426,13 +467,13 @@ macro_rules! cryptostore_integration_tests {
|
||||
let loaded_alice =
|
||||
loaded.get(alice).expect("Alice should be in the store as a tracked user");
|
||||
let loaded_bob =
|
||||
loaded.get(alice).expect("Bob should be in the store as as tracked user");
|
||||
loaded.get(bob).expect("Bob should be in the store as as tracked user");
|
||||
|
||||
assert!(!loaded.contains_key(candy), "Candy shouldn't be part of the store");
|
||||
assert_eq!(loaded.len(), 2, "Candy shouldn't be part of the store");
|
||||
|
||||
assert!(loaded_alice.dirty, "Alice should be considered to be dirty");
|
||||
assert!(loaded_alice.dirty, "Bob should not be considered to be dirty");
|
||||
assert!(!loaded_bob.dirty, "Bob should not be considered to be dirty");
|
||||
};
|
||||
|
||||
let loaded = store.load_tracked_users().await.unwrap();
|
||||
@@ -849,18 +890,21 @@ macro_rules! cryptostore_integration_tests {
|
||||
|
||||
#[async_test]
|
||||
async fn room_settings_saving() {
|
||||
let (account, store) = get_loaded_store("room_settings_saving").await;
|
||||
let (_, store) = get_loaded_store("room_settings_saving").await;
|
||||
|
||||
let room_1 = room_id!("!test_1:localhost");
|
||||
let settings_1 = RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
only_allow_trusted_devices: true,
|
||||
session_rotation_period: Some(Duration::from_secs(10)),
|
||||
session_rotation_period_messages: Some(123),
|
||||
};
|
||||
|
||||
let room_2 = room_id!("!test_2:localhost");
|
||||
let settings_2 = RoomSettings {
|
||||
algorithm: EventEncryptionAlgorithm::OlmV1Curve25519AesSha2,
|
||||
only_allow_trusted_devices: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let room_3 = room_id!("!test_3:localhost");
|
||||
@@ -887,7 +931,7 @@ macro_rules! cryptostore_integration_tests {
|
||||
|
||||
#[async_test]
|
||||
async fn backup_keys_saving() {
|
||||
let (account, store) = get_loaded_store("backup_keys_saving").await;
|
||||
let (_account, store) = get_loaded_store("backup_keys_saving").await;
|
||||
|
||||
let restored = store.load_backup_keys().await.unwrap();
|
||||
assert!(restored.decryption_key.is_none(), "Initially no backup decryption key should be present");
|
||||
@@ -911,7 +955,7 @@ macro_rules! cryptostore_integration_tests {
|
||||
|
||||
#[async_test]
|
||||
async fn custom_value_saving() {
|
||||
let (account, store) = get_loaded_store("custom_value_saving").await;
|
||||
let (_, store) = get_loaded_store("custom_value_saving").await;
|
||||
store.set_custom_value("A", "Hello".as_bytes().to_vec()).await.unwrap();
|
||||
|
||||
let loaded_1 = store.get_custom_value("A").await.unwrap();
|
||||
@@ -920,6 +964,10 @@ macro_rules! cryptostore_integration_tests {
|
||||
let loaded_2 = store.get_custom_value("B").await.unwrap();
|
||||
assert_eq!(None, loaded_2);
|
||||
}
|
||||
|
||||
fn session_info(session: &InboundGroupSession) -> (&RoomId, &str) {
|
||||
(&session.room_id(), &session.session_id())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap, HashSet},
|
||||
collections::{hash_map::Entry, BTreeMap, HashMap, HashSet},
|
||||
convert::Infallible,
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
time::{Duration, Instant},
|
||||
@@ -25,7 +25,6 @@ use ruma::{
|
||||
OwnedUserId, RoomId, TransactionId, UserId,
|
||||
};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::warn;
|
||||
|
||||
use super::{
|
||||
caches::{DeviceStore, GroupSessionStore, SessionStore},
|
||||
@@ -55,6 +54,9 @@ pub struct MemoryStore {
|
||||
account: StdRwLock<Option<Account>>,
|
||||
sessions: SessionStore,
|
||||
inbound_group_sessions: GroupSessionStore,
|
||||
outbound_group_sessions: StdRwLock<BTreeMap<OwnedRoomId, OutboundGroupSession>>,
|
||||
private_identity: StdRwLock<Option<PrivateCrossSigningIdentity>>,
|
||||
tracked_users: StdRwLock<HashMap<OwnedUserId, TrackedUser>>,
|
||||
olm_hashes: StdRwLock<HashMap<String, HashSet<String>>>,
|
||||
devices: DeviceStore,
|
||||
identities: StdRwLock<HashMap<OwnedUserId, ReadOnlyUserIdentities>>,
|
||||
@@ -66,6 +68,7 @@ pub struct MemoryStore {
|
||||
secret_inbox: StdRwLock<HashMap<String, Vec<GossippedSecret>>>,
|
||||
backup_keys: RwLock<BackupKeys>,
|
||||
next_batch_token: RwLock<Option<String>>,
|
||||
room_settings: StdRwLock<HashMap<OwnedRoomId, RoomSettings>>,
|
||||
}
|
||||
|
||||
impl Default for MemoryStore {
|
||||
@@ -74,6 +77,9 @@ impl Default for MemoryStore {
|
||||
account: Default::default(),
|
||||
sessions: SessionStore::new(),
|
||||
inbound_group_sessions: GroupSessionStore::new(),
|
||||
outbound_group_sessions: Default::default(),
|
||||
private_identity: Default::default(),
|
||||
tracked_users: Default::default(),
|
||||
olm_hashes: Default::default(),
|
||||
devices: DeviceStore::new(),
|
||||
identities: Default::default(),
|
||||
@@ -85,6 +91,7 @@ impl Default for MemoryStore {
|
||||
backup_keys: Default::default(),
|
||||
secret_inbox: Default::default(),
|
||||
next_batch_token: Default::default(),
|
||||
room_settings: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +125,17 @@ impl MemoryStore {
|
||||
self.inbound_group_sessions.add(session);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_outbound_group_sessions(&self, sessions: Vec<OutboundGroupSession>) {
|
||||
self.outbound_group_sessions
|
||||
.write()
|
||||
.unwrap()
|
||||
.extend(sessions.into_iter().map(|s| (s.room_id().to_owned(), s)));
|
||||
}
|
||||
|
||||
fn save_private_identity(&self, private_identity: Option<PrivateCrossSigningIdentity>) {
|
||||
*self.private_identity.write().unwrap() = private_identity;
|
||||
}
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Infallible>;
|
||||
@@ -132,7 +150,7 @@ impl CryptoStore for MemoryStore {
|
||||
}
|
||||
|
||||
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
|
||||
Ok(None)
|
||||
Ok(self.private_identity.read().unwrap().clone())
|
||||
}
|
||||
|
||||
async fn next_batch_token(&self) -> Result<Option<String>> {
|
||||
@@ -150,6 +168,8 @@ impl CryptoStore for MemoryStore {
|
||||
async fn save_changes(&self, changes: Changes) -> Result<()> {
|
||||
self.save_sessions(changes.sessions).await;
|
||||
self.save_inbound_group_sessions(changes.inbound_group_sessions);
|
||||
self.save_outbound_group_sessions(changes.outbound_group_sessions);
|
||||
self.save_private_identity(changes.private_identity);
|
||||
|
||||
self.save_devices(changes.devices.new);
|
||||
self.save_devices(changes.devices.changed);
|
||||
@@ -213,6 +233,11 @@ impl CryptoStore for MemoryStore {
|
||||
*self.next_batch_token.write().await = Some(next_batch_token);
|
||||
}
|
||||
|
||||
if !changes.room_settings.is_empty() {
|
||||
let mut settings = self.room_settings.write().unwrap();
|
||||
settings.extend(changes.room_settings);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -245,7 +270,10 @@ impl CryptoStore for MemoryStore {
|
||||
Ok(self.inbound_group_sessions.get_all())
|
||||
}
|
||||
|
||||
async fn inbound_group_session_counts(&self) -> Result<RoomKeyCounts> {
|
||||
async fn inbound_group_session_counts(
|
||||
&self,
|
||||
_backup_version: Option<&str>,
|
||||
) -> Result<RoomKeyCounts> {
|
||||
let backed_up =
|
||||
self.get_inbound_group_sessions().await?.into_iter().filter(|s| s.backed_up()).count();
|
||||
|
||||
@@ -254,6 +282,7 @@ impl CryptoStore for MemoryStore {
|
||||
|
||||
async fn inbound_group_sessions_for_backup(
|
||||
&self,
|
||||
_backup_version: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<InboundGroupSession>> {
|
||||
Ok(self
|
||||
@@ -267,6 +296,7 @@ impl CryptoStore for MemoryStore {
|
||||
|
||||
async fn mark_inbound_group_sessions_as_backed_up(
|
||||
&self,
|
||||
_backup_version: &str,
|
||||
room_and_session_ids: &[(&RoomId, &str)],
|
||||
) -> Result<()> {
|
||||
for (room_id, session_id) in room_and_session_ids {
|
||||
@@ -291,15 +321,22 @@ impl CryptoStore for MemoryStore {
|
||||
Ok(self.backup_keys.read().await.to_owned())
|
||||
}
|
||||
|
||||
async fn get_outbound_group_session(&self, _: &RoomId) -> Result<Option<OutboundGroupSession>> {
|
||||
Ok(None)
|
||||
async fn get_outbound_group_session(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<OutboundGroupSession>> {
|
||||
Ok(self.outbound_group_sessions.read().unwrap().get(room_id).cloned())
|
||||
}
|
||||
|
||||
async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
|
||||
Ok(Vec::new())
|
||||
Ok(self.tracked_users.read().unwrap().values().cloned().collect())
|
||||
}
|
||||
|
||||
async fn save_tracked_users(&self, _: &[(&UserId, bool)]) -> Result<()> {
|
||||
async fn save_tracked_users(&self, tracked_users: &[(&UserId, bool)]) -> Result<()> {
|
||||
self.tracked_users.write().unwrap().extend(tracked_users.iter().map(|(user_id, dirty)| {
|
||||
let user_id: OwnedUserId = user_id.to_owned().into();
|
||||
(user_id.clone(), TrackedUser { user_id, dirty: *dirty })
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -393,9 +430,8 @@ impl CryptoStore for MemoryStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_room_settings(&self, _room_id: &RoomId) -> Result<Option<RoomSettings>> {
|
||||
warn!("Method not implemented");
|
||||
Ok(None)
|
||||
async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
|
||||
Ok(self.room_settings.read().unwrap().get(room_id).cloned())
|
||||
}
|
||||
|
||||
async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
|
||||
@@ -454,12 +490,15 @@ impl CryptoStore for MemoryStore {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::room_id;
|
||||
use ruma::{room_id, user_id};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use crate::{
|
||||
identities::device::testing::get_device,
|
||||
olm::{tests::get_account_and_session_test_helper, InboundGroupSession, OlmMessageHash},
|
||||
olm::{
|
||||
tests::get_account_and_session_test_helper, InboundGroupSession, OlmMessageHash,
|
||||
PrivateCrossSigningIdentity,
|
||||
},
|
||||
store::{memorystore::MemoryStore, Changes, CryptoStore, PendingChanges},
|
||||
};
|
||||
|
||||
@@ -482,7 +521,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_group_session_store() {
|
||||
async fn test_inbound_group_session_store() {
|
||||
let (account, _) = get_account_and_session_test_helper();
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let curve_key = "Nn0L2hkcCMFKqynTjyGsJbth7QrVmX3lbrksMkrGOAw";
|
||||
@@ -506,6 +545,67 @@ mod tests {
|
||||
assert_eq!(inbound, loaded_session);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_outbound_group_session_store() {
|
||||
// Given an outbound session
|
||||
let (account, _) = get_account_and_session_test_helper();
|
||||
let room_id = room_id!("!test:localhost");
|
||||
let (outbound, _) = account.create_group_session_pair_with_defaults(room_id).await;
|
||||
|
||||
// When we save it to the store
|
||||
let store = MemoryStore::new();
|
||||
store.save_outbound_group_sessions(vec![outbound.clone()]);
|
||||
|
||||
// Then we can get it out again
|
||||
let loaded_session = store.get_outbound_group_session(room_id).await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_string(&outbound.pickle().await).unwrap(),
|
||||
serde_json::to_string(&loaded_session.pickle().await).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_tracked_users_are_stored_once_per_user_id() {
|
||||
// Given a store containing 2 tracked users, both dirty
|
||||
let user1 = user_id!("@user1:s");
|
||||
let user2 = user_id!("@user2:s");
|
||||
let user3 = user_id!("@user3:s");
|
||||
let store = MemoryStore::new();
|
||||
store.save_tracked_users(&[(user1, true), (user2, true)]).await.unwrap();
|
||||
|
||||
// When we mark one as clean and add another
|
||||
store.save_tracked_users(&[(user2, false), (user3, false)]).await.unwrap();
|
||||
|
||||
// Then we can get them out again and their dirty flags are correct
|
||||
let loaded_tracked_users =
|
||||
store.load_tracked_users().await.expect("failed to load tracked users");
|
||||
|
||||
let tracked_contains = |user_id, dirty| {
|
||||
loaded_tracked_users.iter().any(|u| u.user_id == user_id && u.dirty == dirty)
|
||||
};
|
||||
|
||||
assert!(tracked_contains(user1, true));
|
||||
assert!(tracked_contains(user2, false));
|
||||
assert!(tracked_contains(user3, false));
|
||||
assert_eq!(loaded_tracked_users.len(), 3);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_private_identity_store() {
|
||||
// Given a private identity
|
||||
let private_identity = PrivateCrossSigningIdentity::empty(user_id!("@u:s"));
|
||||
|
||||
// When we save it to the store
|
||||
let store = MemoryStore::new();
|
||||
store.save_private_identity(Some(private_identity.clone()));
|
||||
|
||||
// Then we can get it out again
|
||||
let loaded_identity =
|
||||
store.load_identity().await.expect("failed to load private identity").unwrap();
|
||||
|
||||
assert_eq!(loaded_identity.user_id(), user_id!("@u:s"));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_device_store() {
|
||||
let device = get_device();
|
||||
@@ -546,3 +646,272 @@ mod tests {
|
||||
assert!(store.is_message_known(&hash).await.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration_tests {
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ruma::{
|
||||
events::secret::request::SecretName, DeviceId, OwnedDeviceId, RoomId, TransactionId, UserId,
|
||||
};
|
||||
|
||||
use super::MemoryStore;
|
||||
use crate::{
|
||||
cryptostore_integration_tests, cryptostore_integration_tests_time,
|
||||
olm::{
|
||||
InboundGroupSession, OlmMessageHash, OutboundGroupSession, PrivateCrossSigningIdentity,
|
||||
StaticAccountData,
|
||||
},
|
||||
store::{BackupKeys, Changes, CryptoStore, PendingChanges, RoomKeyCounts, RoomSettings},
|
||||
types::events::room_key_withheld::RoomKeyWithheldEvent,
|
||||
Account, GossipRequest, GossippedSecret, ReadOnlyDevice, ReadOnlyUserIdentities,
|
||||
SecretInfo, Session, TrackedUser,
|
||||
};
|
||||
|
||||
/// Holds on to a MemoryStore during a test, and moves it back into STORES
|
||||
/// when this is dropped
|
||||
#[derive(Clone, Debug)]
|
||||
struct PersistentMemoryStore(Arc<MemoryStore>);
|
||||
|
||||
impl PersistentMemoryStore {
|
||||
fn new() -> Self {
|
||||
Self(Arc::new(MemoryStore::new()))
|
||||
}
|
||||
|
||||
fn get_static_account(&self) -> Option<StaticAccountData> {
|
||||
self.0.get_static_account()
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
fn get_static_account(&self) -> Option<StaticAccountData> {
|
||||
self.account.read().unwrap().as_ref().map(|acc| acc.static_data().clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a clone of the store for the test with the supplied name. Note:
|
||||
/// dropping this store won't destroy its data, since
|
||||
/// [PersistentMemoryStore] is a reference-counted smart pointer
|
||||
/// to an underlying [MemoryStore].
|
||||
async fn get_store(name: &str, _passphrase: Option<&str>) -> PersistentMemoryStore {
|
||||
// Holds on to one [PersistentMemoryStore] per test, so even if the test drops
|
||||
// the store, we keep its data alive. This simulates the behaviour of
|
||||
// the other stores, which keep their data in a real DB, allowing us to
|
||||
// test MemoryStore using the same code.
|
||||
static STORES: OnceLock<Mutex<HashMap<String, PersistentMemoryStore>>> = OnceLock::new();
|
||||
let stores = STORES.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
|
||||
stores
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(name.to_owned())
|
||||
.or_insert_with(PersistentMemoryStore::new)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Forwards all methods to the underlying [MemoryStore].
|
||||
#[async_trait]
|
||||
impl CryptoStore for PersistentMemoryStore {
|
||||
type Error = <MemoryStore as CryptoStore>::Error;
|
||||
|
||||
async fn load_account(&self) -> Result<Option<Account>, Self::Error> {
|
||||
self.0.load_account().await
|
||||
}
|
||||
|
||||
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>, Self::Error> {
|
||||
self.0.load_identity().await
|
||||
}
|
||||
|
||||
async fn save_changes(&self, changes: Changes) -> Result<(), Self::Error> {
|
||||
self.0.save_changes(changes).await
|
||||
}
|
||||
|
||||
async fn save_pending_changes(&self, changes: PendingChanges) -> Result<(), Self::Error> {
|
||||
self.0.save_pending_changes(changes).await
|
||||
}
|
||||
|
||||
async fn get_sessions(
|
||||
&self,
|
||||
sender_key: &str,
|
||||
) -> Result<Option<Arc<tokio::sync::Mutex<Vec<Session>>>>, Self::Error> {
|
||||
self.0.get_sessions(sender_key).await
|
||||
}
|
||||
|
||||
async fn get_inbound_group_session(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
session_id: &str,
|
||||
) -> Result<Option<InboundGroupSession>, Self::Error> {
|
||||
self.0.get_inbound_group_session(room_id, session_id).await
|
||||
}
|
||||
|
||||
async fn get_withheld_info(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
session_id: &str,
|
||||
) -> Result<Option<RoomKeyWithheldEvent>, Self::Error> {
|
||||
self.0.get_withheld_info(room_id, session_id).await
|
||||
}
|
||||
|
||||
async fn get_inbound_group_sessions(
|
||||
&self,
|
||||
) -> Result<Vec<InboundGroupSession>, Self::Error> {
|
||||
self.0.get_inbound_group_sessions().await
|
||||
}
|
||||
|
||||
async fn inbound_group_session_counts(
|
||||
&self,
|
||||
backup_version: Option<&str>,
|
||||
) -> Result<RoomKeyCounts, Self::Error> {
|
||||
self.0.inbound_group_session_counts(backup_version).await
|
||||
}
|
||||
|
||||
async fn inbound_group_sessions_for_backup(
|
||||
&self,
|
||||
backup_version: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<InboundGroupSession>, Self::Error> {
|
||||
self.0.inbound_group_sessions_for_backup(backup_version, limit).await
|
||||
}
|
||||
|
||||
async fn mark_inbound_group_sessions_as_backed_up(
|
||||
&self,
|
||||
backup_version: &str,
|
||||
room_and_session_ids: &[(&RoomId, &str)],
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.mark_inbound_group_sessions_as_backed_up(backup_version, room_and_session_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reset_backup_state(&self) -> Result<(), Self::Error> {
|
||||
self.0.reset_backup_state().await
|
||||
}
|
||||
|
||||
async fn load_backup_keys(&self) -> Result<BackupKeys, Self::Error> {
|
||||
self.0.load_backup_keys().await
|
||||
}
|
||||
|
||||
async fn get_outbound_group_session(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<OutboundGroupSession>, Self::Error> {
|
||||
self.0.get_outbound_group_session(room_id).await
|
||||
}
|
||||
|
||||
async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>, Self::Error> {
|
||||
self.0.load_tracked_users().await
|
||||
}
|
||||
|
||||
async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<(), Self::Error> {
|
||||
self.0.save_tracked_users(users).await
|
||||
}
|
||||
|
||||
async fn get_device(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
) -> Result<Option<ReadOnlyDevice>, Self::Error> {
|
||||
self.0.get_device(user_id, device_id).await
|
||||
}
|
||||
|
||||
async fn get_user_devices(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Result<HashMap<OwnedDeviceId, ReadOnlyDevice>, Self::Error> {
|
||||
self.0.get_user_devices(user_id).await
|
||||
}
|
||||
|
||||
async fn get_user_identity(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<ReadOnlyUserIdentities>, Self::Error> {
|
||||
self.0.get_user_identity(user_id).await
|
||||
}
|
||||
|
||||
async fn is_message_known(
|
||||
&self,
|
||||
message_hash: &OlmMessageHash,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.is_message_known(message_hash).await
|
||||
}
|
||||
|
||||
async fn get_outgoing_secret_requests(
|
||||
&self,
|
||||
request_id: &TransactionId,
|
||||
) -> Result<Option<GossipRequest>, Self::Error> {
|
||||
self.0.get_outgoing_secret_requests(request_id).await
|
||||
}
|
||||
|
||||
async fn get_secret_request_by_info(
|
||||
&self,
|
||||
secret_info: &SecretInfo,
|
||||
) -> Result<Option<GossipRequest>, Self::Error> {
|
||||
self.0.get_secret_request_by_info(secret_info).await
|
||||
}
|
||||
|
||||
async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>, Self::Error> {
|
||||
self.0.get_unsent_secret_requests().await
|
||||
}
|
||||
|
||||
async fn delete_outgoing_secret_requests(
|
||||
&self,
|
||||
request_id: &TransactionId,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.delete_outgoing_secret_requests(request_id).await
|
||||
}
|
||||
|
||||
async fn get_secrets_from_inbox(
|
||||
&self,
|
||||
secret_name: &SecretName,
|
||||
) -> Result<Vec<GossippedSecret>, Self::Error> {
|
||||
self.0.get_secrets_from_inbox(secret_name).await
|
||||
}
|
||||
|
||||
async fn delete_secrets_from_inbox(
|
||||
&self,
|
||||
secret_name: &SecretName,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.0.delete_secrets_from_inbox(secret_name).await
|
||||
}
|
||||
|
||||
async fn get_room_settings(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<RoomSettings>, Self::Error> {
|
||||
self.0.get_room_settings(room_id).await
|
||||
}
|
||||
|
||||
async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
self.0.get_custom_value(key).await
|
||||
}
|
||||
|
||||
async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<(), Self::Error> {
|
||||
self.0.set_custom_value(key, value).await
|
||||
}
|
||||
|
||||
async fn remove_custom_value(&self, key: &str) -> Result<(), Self::Error> {
|
||||
self.0.remove_custom_value(key).await
|
||||
}
|
||||
|
||||
async fn try_take_leased_lock(
|
||||
&self,
|
||||
lease_duration_ms: u32,
|
||||
key: &str,
|
||||
holder: &str,
|
||||
) -> Result<bool, Self::Error> {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
|
||||
async fn next_batch_token(&self) -> Result<Option<String>, Self::Error> {
|
||||
self.0.next_batch_token().await
|
||||
}
|
||||
}
|
||||
|
||||
cryptostore_integration_tests!();
|
||||
cryptostore_integration_tests_time!();
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ pub struct Changes {
|
||||
}
|
||||
|
||||
/// A user for which we are tracking the list of devices.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TrackedUser {
|
||||
/// The user ID of the user.
|
||||
pub user_id: OwnedUserId,
|
||||
@@ -833,9 +833,18 @@ pub(crate) enum UserKeyQueryResult {
|
||||
pub struct RoomSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
|
||||
/// Should untrusted devices receive the room key, or should they be
|
||||
/// excluded from the conversation.
|
||||
pub only_allow_trusted_devices: bool,
|
||||
|
||||
/// The maximum time an encryption session should be used for, before it is
|
||||
/// rotated.
|
||||
pub session_rotation_period: Option<Duration>,
|
||||
|
||||
/// The maximum number of messages an encryption session should be used for,
|
||||
/// before it is rotated.
|
||||
pub session_rotation_period_messages: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for RoomSettings {
|
||||
@@ -843,6 +852,8 @@ impl Default for RoomSettings {
|
||||
Self {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
only_allow_trusted_devices: false,
|
||||
session_rotation_period: None,
|
||||
session_rotation_period_messages: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1583,6 +1594,87 @@ impl Store {
|
||||
pub(crate) fn crypto_store(&self) -> Arc<CryptoStoreWrapper> {
|
||||
self.inner.store.clone()
|
||||
}
|
||||
|
||||
/// Export the keys that match the given predicate.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `predicate` - A closure that will be called for every known
|
||||
/// `InboundGroupSession`, which represents a room key. If the closure
|
||||
/// returns `true` the `InboundGroupSession` will be included in the export,
|
||||
/// if the closure returns `false` it will not be included.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_room_key_export};
|
||||
/// # use ruma::{device_id, user_id, room_id};
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # async {
|
||||
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
|
||||
/// let room_id = room_id!("!test:localhost");
|
||||
/// let exported_keys = machine.store().export_room_keys(|s| s.room_id() == room_id).await.unwrap();
|
||||
/// let encrypted_export = encrypt_room_key_export(&exported_keys, "1234", 1);
|
||||
/// # };
|
||||
/// ```
|
||||
pub async fn export_room_keys(
|
||||
&self,
|
||||
predicate: impl FnMut(&InboundGroupSession) -> bool,
|
||||
) -> Result<Vec<ExportedRoomKey>> {
|
||||
let mut exported = Vec::new();
|
||||
|
||||
let mut sessions = self.get_inbound_group_sessions().await?;
|
||||
sessions.retain(predicate);
|
||||
|
||||
for session in sessions {
|
||||
let export = session.export().await;
|
||||
exported.push(export);
|
||||
}
|
||||
|
||||
Ok(exported)
|
||||
}
|
||||
|
||||
/// Export room keys matching a predicate, providing them as an async
|
||||
/// `Stream`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `predicate` - A closure that will be called for every known
|
||||
/// `InboundGroupSession`, which represents a room key. If the closure
|
||||
/// returns `true` the `InboundGroupSession` will be included in the export,
|
||||
/// if the closure returns `false` it will not be included.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::pin::pin;
|
||||
///
|
||||
/// use matrix_sdk_crypto::{olm::ExportedRoomKey, OlmMachine};
|
||||
/// use ruma::{device_id, room_id, user_id};
|
||||
/// use tokio_stream::StreamExt;
|
||||
/// # async {
|
||||
/// let alice = user_id!("@alice:example.org");
|
||||
/// let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
|
||||
/// let room_id = room_id!("!test:localhost");
|
||||
/// let mut keys = pin!(machine
|
||||
/// .store()
|
||||
/// .export_room_keys_stream(|s| s.room_id() == room_id)
|
||||
/// .await
|
||||
/// .unwrap());
|
||||
/// while let Some(key) = keys.next().await {
|
||||
/// println!("{}", key.room_id);
|
||||
/// }
|
||||
/// # };
|
||||
/// ```
|
||||
pub async fn export_room_keys_stream(
|
||||
&self,
|
||||
predicate: impl FnMut(&InboundGroupSession) -> bool,
|
||||
) -> Result<impl Stream<Item = ExportedRoomKey>> {
|
||||
// TODO: if/when there is a get_inbound_group_sessions_stream, use that here.
|
||||
let sessions = self.get_inbound_group_sessions().await?;
|
||||
Ok(futures_util::stream::iter(sessions.into_iter().filter(predicate))
|
||||
.then(|session| async move { session.export().await }))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Store {
|
||||
@@ -1611,3 +1703,96 @@ impl matrix_sdk_common::store_locks::BackingStore for LockableCryptoStore {
|
||||
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::pin::pin;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{room_id, user_id};
|
||||
|
||||
use crate::{machine::tests::get_machine_pair, types::EventEncryptionAlgorithm};
|
||||
|
||||
#[async_test]
|
||||
async fn export_room_keys_provides_selected_keys() {
|
||||
// Given an OlmMachine with room keys in it
|
||||
let (alice, _, _) = get_machine_pair(user_id!("@a:s.co"), user_id!("@b:s.co"), false).await;
|
||||
let room1_id = room_id!("!room1:localhost");
|
||||
let room2_id = room_id!("!room2:localhost");
|
||||
let room3_id = room_id!("!room3:localhost");
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room1_id).await.unwrap();
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room2_id).await.unwrap();
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room3_id).await.unwrap();
|
||||
|
||||
// When I export some of the keys
|
||||
let keys = alice
|
||||
.store()
|
||||
.export_room_keys(|s| s.room_id() == room2_id || s.room_id() == room3_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Then the requested keys were provided
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert_eq!(keys[0].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
assert_eq!(keys[1].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
assert_eq!(keys[0].room_id, "!room2:localhost");
|
||||
assert_eq!(keys[1].room_id, "!room3:localhost");
|
||||
assert_eq!(keys[0].session_key.to_base64().len(), 220);
|
||||
assert_eq!(keys[1].session_key.to_base64().len(), 220);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn export_room_keys_stream_can_provide_all_keys() {
|
||||
// Given an OlmMachine with room keys in it
|
||||
let (alice, _, _) = get_machine_pair(user_id!("@a:s.co"), user_id!("@b:s.co"), false).await;
|
||||
let room1_id = room_id!("!room1:localhost");
|
||||
let room2_id = room_id!("!room2:localhost");
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room1_id).await.unwrap();
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room2_id).await.unwrap();
|
||||
|
||||
// When I export the keys as a stream
|
||||
let mut keys = pin!(alice.store().export_room_keys_stream(|_| true).await.unwrap());
|
||||
|
||||
// And collect them
|
||||
let mut collected = vec![];
|
||||
while let Some(key) = keys.next().await {
|
||||
collected.push(key);
|
||||
}
|
||||
|
||||
// Then all the keys were provided
|
||||
assert_eq!(collected.len(), 2);
|
||||
assert_eq!(collected[0].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
assert_eq!(collected[1].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
assert_eq!(collected[0].room_id, "!room1:localhost");
|
||||
assert_eq!(collected[1].room_id, "!room2:localhost");
|
||||
assert_eq!(collected[0].session_key.to_base64().len(), 220);
|
||||
assert_eq!(collected[1].session_key.to_base64().len(), 220);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn export_room_keys_stream_can_provide_a_subset_of_keys() {
|
||||
// Given an OlmMachine with room keys in it
|
||||
let (alice, _, _) = get_machine_pair(user_id!("@a:s.co"), user_id!("@b:s.co"), false).await;
|
||||
let room1_id = room_id!("!room1:localhost");
|
||||
let room2_id = room_id!("!room2:localhost");
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room1_id).await.unwrap();
|
||||
alice.create_outbound_group_session_with_defaults_test_helper(room2_id).await.unwrap();
|
||||
|
||||
// When I export the keys as a stream
|
||||
let mut keys =
|
||||
pin!(alice.store().export_room_keys_stream(|s| s.room_id() == room1_id).await.unwrap());
|
||||
|
||||
// And collect them
|
||||
let mut collected = vec![];
|
||||
while let Some(key) = keys.next().await {
|
||||
collected.push(key);
|
||||
}
|
||||
|
||||
// Then all the keys matching our predicate were provided, and no others
|
||||
assert_eq!(collected.len(), 1);
|
||||
assert_eq!(collected[0].algorithm, EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
assert_eq!(collected[0].room_id, "!room1:localhost");
|
||||
assert_eq!(collected[0].session_key.to_base64().len(), 220);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,22 +105,43 @@ pub trait CryptoStore: AsyncTraitDeps {
|
||||
|
||||
/// Get the number inbound group sessions we have and how many of them are
|
||||
/// backed up.
|
||||
async fn inbound_group_session_counts(&self) -> Result<RoomKeyCounts, Self::Error>;
|
||||
async fn inbound_group_session_counts(
|
||||
&self,
|
||||
backup_version: Option<&str>,
|
||||
) -> Result<RoomKeyCounts, Self::Error>;
|
||||
|
||||
/// Get all the inbound group sessions we have not backed up yet.
|
||||
/// Return a batch of ['InboundGroupSession'] ("room keys") that have not
|
||||
/// yet been backed up in the supplied backup version.
|
||||
///
|
||||
/// The size of the returned `Vec` is <= `limit`.
|
||||
///
|
||||
/// Note: some implementations ignore `backup_version` and assume the
|
||||
/// current backup version, which is normally the same.
|
||||
async fn inbound_group_sessions_for_backup(
|
||||
&self,
|
||||
backup_version: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<InboundGroupSession>, Self::Error>;
|
||||
|
||||
/// Mark the inbound group sessions with the supplied room and session IDs
|
||||
/// as backed up
|
||||
/// Store the fact that the supplied sessions were backed up into the backup
|
||||
/// with version `backup_version`.
|
||||
///
|
||||
/// Note: some implementations ignore `backup_version` and assume the
|
||||
/// current backup version, which is normally the same.
|
||||
async fn mark_inbound_group_sessions_as_backed_up(
|
||||
&self,
|
||||
backup_version: &str,
|
||||
room_and_session_ids: &[(&RoomId, &str)],
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Reset the backup state of all the stored inbound group sessions.
|
||||
///
|
||||
/// Note: this is mostly implemented by stores that ignore the
|
||||
/// `backup_version` argument on `inbound_group_sessions_for_backup` and
|
||||
/// `mark_inbound_group_sessions_as_backed_up`. Implementations that
|
||||
/// pay attention to the supplied backup version probably don't need to
|
||||
/// update their storage when the current backup version changes, so have
|
||||
/// empty implementations of this method.
|
||||
async fn reset_backup_state(&self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the backup keys we have stored.
|
||||
@@ -133,11 +154,14 @@ pub trait CryptoStore: AsyncTraitDeps {
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<OutboundGroupSession>, Self::Error>;
|
||||
|
||||
/// Load the list of users whose devices we are keeping track of.
|
||||
/// Provide the list of users whose devices we are keeping track of, and
|
||||
/// whether they are considered dirty/outdated.
|
||||
async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>, Self::Error>;
|
||||
|
||||
/// Save a list of users and their respective dirty/outdated flags to the
|
||||
/// store.
|
||||
/// Update the list of users whose devices we are keeping track of, and
|
||||
/// whether they are considered dirty/outdated.
|
||||
///
|
||||
/// Replaces any existing entry with a matching user ID.
|
||||
async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<(), Self::Error>;
|
||||
|
||||
/// Get the device for the given user with the given device ID.
|
||||
@@ -328,23 +352,27 @@ impl<T: CryptoStore> CryptoStore for EraseCryptoStoreError<T> {
|
||||
self.0.get_inbound_group_sessions().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn inbound_group_session_counts(&self) -> Result<RoomKeyCounts> {
|
||||
self.0.inbound_group_session_counts().await.map_err(Into::into)
|
||||
async fn inbound_group_session_counts(
|
||||
&self,
|
||||
backup_version: Option<&str>,
|
||||
) -> Result<RoomKeyCounts> {
|
||||
self.0.inbound_group_session_counts(backup_version).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn inbound_group_sessions_for_backup(
|
||||
&self,
|
||||
backup_version: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<InboundGroupSession>> {
|
||||
self.0.inbound_group_sessions_for_backup(limit).await.map_err(Into::into)
|
||||
self.0.inbound_group_sessions_for_backup(backup_version, limit).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn mark_inbound_group_sessions_as_backed_up(
|
||||
&self,
|
||||
backup_version: &str,
|
||||
room_and_session_ids: &[(&RoomId, &str)],
|
||||
) -> Result<()> {
|
||||
self.0
|
||||
.mark_inbound_group_sessions_as_backed_up(room_and_session_ids)
|
||||
.mark_inbound_group_sessions_as_backed_up(backup_version, room_and_session_ids)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use js_option::JsOption;
|
||||
use ruma::{
|
||||
serde::Raw, DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceId, OwnedDeviceKeyId, OwnedUserId,
|
||||
};
|
||||
@@ -52,6 +53,10 @@ pub struct DeviceKeys {
|
||||
/// Signatures for the device key object.
|
||||
pub signatures: Signatures,
|
||||
|
||||
/// Whether the device is a dehydrated device or not
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
|
||||
pub dehydrated: JsOption<bool>,
|
||||
|
||||
/// Additional data added to the device key information by intermediate
|
||||
/// servers, and not covered by the signatures.
|
||||
#[serde(default, skip_serializing_if = "UnsignedDeviceInfo::is_empty")]
|
||||
@@ -77,6 +82,7 @@ impl DeviceKeys {
|
||||
algorithms,
|
||||
keys,
|
||||
signatures,
|
||||
dehydrated: JsOption::Undefined,
|
||||
unsigned: Default::default(),
|
||||
other: BTreeMap::new(),
|
||||
}
|
||||
@@ -182,6 +188,8 @@ struct DeviceKeyHelper {
|
||||
pub device_id: OwnedDeviceId,
|
||||
pub algorithms: Vec<EventEncryptionAlgorithm>,
|
||||
pub keys: BTreeMap<OwnedDeviceKeyId, String>,
|
||||
#[serde(default, skip_serializing_if = "JsOption::is_undefined")]
|
||||
pub dehydrated: JsOption<bool>,
|
||||
pub signatures: Signatures,
|
||||
#[serde(default, skip_serializing_if = "UnsignedDeviceInfo::is_empty")]
|
||||
pub unsigned: UnsignedDeviceInfo,
|
||||
@@ -216,6 +224,7 @@ impl TryFrom<DeviceKeyHelper> for DeviceKeys {
|
||||
device_id: value.device_id,
|
||||
algorithms: value.algorithms,
|
||||
keys: keys?,
|
||||
dehydrated: value.dehydrated,
|
||||
signatures: value.signatures,
|
||||
unsigned: value.unsigned,
|
||||
other: value.other,
|
||||
@@ -233,6 +242,7 @@ impl From<DeviceKeys> for DeviceKeyHelper {
|
||||
device_id: value.device_id,
|
||||
algorithms: value.algorithms,
|
||||
keys,
|
||||
dehydrated: value.dehydrated,
|
||||
signatures: value.signatures,
|
||||
unsigned: value.unsigned,
|
||||
other: value.other,
|
||||
|
||||
@@ -246,10 +246,10 @@ impl<T: Ord> SigningKeys<T> {
|
||||
}
|
||||
|
||||
/// Get a `SigningKey` with the given `DeviceKeyId`.
|
||||
pub fn get<Q: ?Sized>(&self, key_id: &Q) -> Option<&SigningKey>
|
||||
pub fn get<Q>(&self, key_id: &Q) -> Option<&SigningKey>
|
||||
where
|
||||
T: Borrow<Q>,
|
||||
Q: Ord,
|
||||
Q: Ord + ?Sized,
|
||||
{
|
||||
self.0.get(key_id)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::num::NonZeroU8;
|
||||
|
||||
use ruma::MilliSecondsSinceUnixEpoch;
|
||||
use time::{
|
||||
format_description::well_known::{iso8601, Iso8601},
|
||||
OffsetDateTime,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn json_convert<T, U>(value: &T) -> serde_json::Result<U>
|
||||
where
|
||||
@@ -21,3 +29,57 @@ where
|
||||
let json = serde_json::to_string(value)?;
|
||||
serde_json::from_str(&json)
|
||||
}
|
||||
|
||||
const ISO8601_WITH_MILLIS: iso8601::EncodedConfig = iso8601::Config::DEFAULT
|
||||
.set_time_precision(iso8601::TimePrecision::Second { decimal_digits: NonZeroU8::new(3) })
|
||||
.encode();
|
||||
|
||||
/// Format the given timestamp into a human-readable timestamp.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Provided the timestamp fits within an `OffsetDateTime` (ie, it is on or
|
||||
/// before year 9999), a string that looks like `1970-01-01T00:00:00.000Z`.
|
||||
/// Otherwise, `None`.
|
||||
pub fn timestamp_to_iso8601(ts: MilliSecondsSinceUnixEpoch) -> Option<String> {
|
||||
let nanos_since_epoch = i128::from(ts.get()) * 1_000_000;
|
||||
|
||||
// OffsetDateTime has a max year of 9999, whereas MilliSecondsSinceUnixEpoch has
|
||||
// a max year of 285427, so `from_unix_timestamp_nanos` can overflow for very
|
||||
// large timestamps. (The Y10K problem!)
|
||||
let dt = OffsetDateTime::from_unix_timestamp_nanos(nanos_since_epoch).ok()?;
|
||||
|
||||
// SAFETY: `format` can fail if:
|
||||
// * The input lacks information on a component we have asked it to format
|
||||
// (eg, it is given a `Time` and we ask it for a date), or
|
||||
// * The input contains an invalid component (eg 30th February), or
|
||||
// * An `io::Error` is raised internally.
|
||||
//
|
||||
// The first two cannot occur because we know we are giving it a valid
|
||||
// OffsetDateTime that has all the components we are asking it to print.
|
||||
//
|
||||
// The third should not occur because we are formatting a short string to an
|
||||
// in-memory buffer.
|
||||
|
||||
Some(dt.format(&Iso8601::<ISO8601_WITH_MILLIS>).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
|
||||
use super::timestamp_to_iso8601;
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_to_iso8601() {
|
||||
assert_eq!(
|
||||
timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::new_saturating(0))),
|
||||
Some("1970-01-01T00:00:00.000Z".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::new_saturating(1709657033012))),
|
||||
Some("2024-03-05T16:43:53.012Z".to_owned())
|
||||
);
|
||||
assert_eq!(timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::MAX)), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
convert::{TryFrom, TryInto},
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use as_variant::as_variant;
|
||||
use ruma::{
|
||||
@@ -771,7 +768,6 @@ impl TryFrom<OutgoingRequest> for OutgoingContent {
|
||||
match value.request() {
|
||||
crate::OutgoingRequests::KeysUpload(_)
|
||||
| crate::OutgoingRequests::KeysQuery(_)
|
||||
| crate::OutgoingRequests::KeysBackup(_)
|
||||
| crate::OutgoingRequests::SignatureUpload(_)
|
||||
| crate::OutgoingRequests::KeysClaim(_) => Err("Invalid request type".to_owned()),
|
||||
crate::OutgoingRequests::ToDeviceRequest(r) => Self::try_from(r.clone()),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user