Compare commits
1543 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3edf0139a | |||
| d07001a581 | |||
| 62b8169ac2 | |||
| d35063412f | |||
| 1a162e5dd5 | |||
| b8069af8ba | |||
| fa3e192c37 | |||
| efc53569ed | |||
| 5c12132569 | |||
| 251a38285c | |||
| b500fa1daa | |||
| 4430dae421 | |||
| eead09984c | |||
| dcfcac0bd3 | |||
| dc32fc282d | |||
| b6eed09564 | |||
| 29ba171953 | |||
| 9e9152745c | |||
| 12d1607cdc | |||
| 6c8b520f14 | |||
| fedcdb1e63 | |||
| 1cfd69a880 | |||
| dad035d170 | |||
| 3c14acf163 | |||
| 253affeb0c | |||
| fe4ddfde89 | |||
| 8a332ca9e1 | |||
| 506f57a22c | |||
| 8b05f9276f | |||
| e8b2655d52 | |||
| 537ef1409b | |||
| e2bf3d0d18 | |||
| d188ef3386 | |||
| ba7ccb40cc | |||
| 1b2c644277 | |||
| 6a853d0173 | |||
| 7487b24fe3 | |||
| 5c4f2b3430 | |||
| 901b670a22 | |||
| 28d6e2821b | |||
| 7f0b74a39d | |||
| 7b6869310f | |||
| 445b2e627d | |||
| 0e206506d9 | |||
| 0fb1d72100 | |||
| 6d8c54deb5 | |||
| 07620452df | |||
| 099db20555 | |||
| 75571c2c30 | |||
| 8c95b1c4cf | |||
| 530c268e61 | |||
| 99dcf84340 | |||
| 2a76d17bd9 | |||
| 15af364c97 | |||
| ec7724f393 | |||
| 1e7d509920 | |||
| 520758bf1e | |||
| 7f49618d35 | |||
| a308771a7a | |||
| 2c4379909c | |||
| d4f49ca334 | |||
| c97bb83af9 | |||
| 12c53bb2bc | |||
| 7fd973c563 | |||
| f8dae723c3 | |||
| 8ccf78c025 | |||
| 47bb73cf89 | |||
| f3d952839e | |||
| 85ea9279dc | |||
| be159356cd | |||
| 571b5e61cf | |||
| 77af11bcbc | |||
| 335251695a | |||
| bf17012d6b | |||
| 328ebdba9c | |||
| c359b011fa | |||
| 07fed7f4df | |||
| 0ed74d8c31 | |||
| 35d7cab330 | |||
| 407e27d176 | |||
| 0f758a643c | |||
| fd2ae1ed8f | |||
| 2b13c0832f | |||
| b3fbd15270 | |||
| 4e3e393596 | |||
| 7f81e7c61b | |||
| a75ae16b79 | |||
| 9ebc61ad0f | |||
| 0d66480cd6 | |||
| 02802e9088 | |||
| 1a6adc6d41 | |||
| 6d10d6150c | |||
| fb1a2f6d94 | |||
| bdee4e0547 | |||
| 42d5eec8b7 | |||
| e50c1465ea | |||
| 5827cc9a39 | |||
| c95fd93666 | |||
| d96fcf7199 | |||
| e2348471db | |||
| 8e120eb648 | |||
| c573985c64 | |||
| 6a996c7657 | |||
| 7bb96e0ece | |||
| dc7f0389b1 | |||
| 5d4b3a0457 | |||
| 888ffb0a56 | |||
| 475fffb64d | |||
| a8b44f26ea | |||
| a7697fbd32 | |||
| 02e63ab9c1 | |||
| 8d909ccabe | |||
| 2e22f6b6c0 | |||
| 7c85fdaf28 | |||
| 1ecf90bb42 | |||
| e2ecba7d45 | |||
| 8a0e0a7a4d | |||
| c562d91533 | |||
| 3cf4150df2 | |||
| b3d52ca68c | |||
| e673954b98 | |||
| 6d7573dced | |||
| 2d14452398 | |||
| 7931c4a589 | |||
| 4db1ad350b | |||
| 0debbf24d7 | |||
| bb8217d10f | |||
| 6ff5c8e918 | |||
| 7fa89f76aa | |||
| 0335fdd07f | |||
| 3f8e3b61ff | |||
| efe5ea6a9c | |||
| 6afeeea56c | |||
| bef1dfbf79 | |||
| 51488b40d0 | |||
| b6a637893e | |||
| 2a4dce30b2 | |||
| 3194ad1f9a | |||
| ee648144a2 | |||
| afef9103a3 | |||
| 4df5ee6087 | |||
| 1a731ec385 | |||
| 84fe78bab3 | |||
| bce11b209e | |||
| 6c472f873f | |||
| 250b85dc79 | |||
| f3ab1ae276 | |||
| 6b2df7afdd | |||
| cdb252be5e | |||
| db30ef6ee4 | |||
| da8699fe40 | |||
| 16b5eebe23 | |||
| 8161360852 | |||
| f01cfe42b4 | |||
| a024c9b268 | |||
| c20349f46f | |||
| e9d37d7c4e | |||
| 8b94aed27f | |||
| dc5b15e799 | |||
| 30e9189f7b | |||
| b6a5d4962b | |||
| ee713f928f | |||
| 444ff9936b | |||
| 2451815226 | |||
| 86c6e601bd | |||
| 9f159ff5a4 | |||
| 056f34883f | |||
| 2d15f758da | |||
| acd5de3cf3 | |||
| f0bb35a96c | |||
| 1d38e54739 | |||
| 08665dcd1c | |||
| 569adb7ceb | |||
| fd4dff79d4 | |||
| fdb9fe21c6 | |||
| 7e5eec82c5 | |||
| 901715bcf3 | |||
| 3aa46fa746 | |||
| 3318789a8b | |||
| 0fe0910fea | |||
| a11633381b | |||
| 2ff6497604 | |||
| 7f87abf408 | |||
| dd49a8bd43 | |||
| f8c3a1d03b | |||
| 808c4c6f3c | |||
| a14549d5cc | |||
| 9b5b4ab2b2 | |||
| 5756057719 | |||
| c5ef3e7c94 | |||
| fa7c1b60f6 | |||
| 39b1fff218 | |||
| c6df6b421c | |||
| f9b4fd2d12 | |||
| e5390e18de | |||
| 28cd0c19e9 | |||
| 15ebc95cd0 | |||
| 39e12a86ca | |||
| ec9ec2567f | |||
| fa9c91fb0e | |||
| 24b71bf869 | |||
| dd83cdd74b | |||
| 453cae641b | |||
| f838b5fae8 | |||
| 1f722f224f | |||
| a992a4e831 | |||
| 017359b081 | |||
| 08718acd57 | |||
| dd1f817701 | |||
| 81db9ef4ca | |||
| e927834108 | |||
| f3045dbf99 | |||
| d6c6211c00 | |||
| 85949081ce | |||
| cbcc5feef2 | |||
| 1b569a8fd4 | |||
| 813f388812 | |||
| 552de33dbc | |||
| c3f2003eb7 | |||
| 3b70d7f9ba | |||
| c00c9d7b81 | |||
| 126e8f1bd9 | |||
| 9e6e76e5ed | |||
| d1410fcded | |||
| b955e7aad9 | |||
| a364018c0e | |||
| 0f910b6229 | |||
| 736f0e7687 | |||
| 6a1b85c560 | |||
| e3503fe102 | |||
| 4522b4e8f5 | |||
| 886809b579 | |||
| 97b91a47f1 | |||
| 4b8b9db075 | |||
| bc207f1e5c | |||
| d863b4eb75 | |||
| 45cb162e5a | |||
| ba2cef62b0 | |||
| 2d7fb1b61c | |||
| d6f9e5f6b6 | |||
| 9c07ff1166 | |||
| 5bd7d17234 | |||
| 7d439b1697 | |||
| 39f1b9d464 | |||
| 8a86369e68 | |||
| a19999b240 | |||
| a72a05edc3 | |||
| d0cc3d9c2b | |||
| 2862934b87 | |||
| 3f3f9d653b | |||
| 83fc91f87e | |||
| 1e77c39498 | |||
| 1de27bcc0c | |||
| 248fff370a | |||
| 26dd2d0e62 | |||
| 4bbfa4345f | |||
| 5f92113627 | |||
| 426a93f07c | |||
| 0949979a1b | |||
| 76144de881 | |||
| bd4763235a | |||
| 911b5415b9 | |||
| c3fc6ff58f | |||
| 4c84f252d2 | |||
| 70c0626882 | |||
| 5570181bf8 | |||
| 7088ff89b5 | |||
| 2561ed1df9 | |||
| 3f36528a98 | |||
| 34da27222f | |||
| aa69bda05b | |||
| 8bdd0ffc5d | |||
| c21408934c | |||
| daa0768a04 | |||
| 706ea248eb | |||
| d120d9b70f | |||
| ef2754736a | |||
| bc78095611 | |||
| 45ea472c83 | |||
| 583810726d | |||
| 5fef975ef2 | |||
| 86d2a74c25 | |||
| aa5e28e58f | |||
| 0080a74d3b | |||
| 44c5244b9a | |||
| 339d701477 | |||
| 74a01376c7 | |||
| 379e3f27af | |||
| 89e0af97af | |||
| fbb369da8d | |||
| 1fdd1ab23a | |||
| 40dcc988d6 | |||
| 9909cb597a | |||
| 56cfe8f231 | |||
| ab9d8eb52b | |||
| af01c64712 | |||
| c8233b6c85 | |||
| b72bdcd7d4 | |||
| ab87013125 | |||
| e146daf2a3 | |||
| 5501378a77 | |||
| b586f1690b | |||
| 1c967669d9 | |||
| 89a8245ce6 | |||
| d2f251b2d5 | |||
| c72b769389 | |||
| 41799a08ed | |||
| 4b2bac965d | |||
| c61215630b | |||
| 23a13a77a4 | |||
| cea57724e7 | |||
| 1f714e9cc5 | |||
| 02587bf6bf | |||
| 320a20b787 | |||
| 844b22fd8e | |||
| 26c82b3010 | |||
| 1de4a6cdd6 | |||
| 08f180cdb7 | |||
| 8c3843f392 | |||
| b396232756 | |||
| 9e3089074f | |||
| fa37a2dea2 | |||
| 3910122fd5 | |||
| 2919c2d5d1 | |||
| 0fad7b60a9 | |||
| a2fb420635 | |||
| 6088993706 | |||
| 41bcc9bdd7 | |||
| dc08d5f62b | |||
| 985c7d7b36 | |||
| 58bcd354f6 | |||
| 5c6a6464c4 | |||
| d3c20b2a13 | |||
| 7817af9aa6 | |||
| 7569c08ada | |||
| be2a818c84 | |||
| 8b805f9f01 | |||
| 86b9dc5e57 | |||
| 260f888a89 | |||
| 5907fac248 | |||
| 3c8da5f7e1 | |||
| 8043b6c1e1 | |||
| 4e16f79ad2 | |||
| f93baf7ed4 | |||
| 9e058b6202 | |||
| a5e81ebcc9 | |||
| aa7fc8ff47 | |||
| 849fbe94f1 | |||
| c9527e7263 | |||
| 9a3b81178f | |||
| 1e9e13ab63 | |||
| 2545e1bd72 | |||
| 1b8337cdc2 | |||
| b95aea9cf0 | |||
| 12cdf8c159 | |||
| 2054f5deef | |||
| 0a6983512a | |||
| 977438207c | |||
| 87b849a95b | |||
| 3a4656cedd | |||
| 7e399e1e8d | |||
| b9e92886de | |||
| 2c415abce8 | |||
| 3de2a9ad22 | |||
| 0f95687bec | |||
| 2a5f17005d | |||
| 5fce18f4dd | |||
| 93b42236fe | |||
| 26935ee1c0 | |||
| ab08ce6ff1 | |||
| ae3cd70904 | |||
| 7b21166784 | |||
| 403ce9b711 | |||
| 98fa629f38 | |||
| 75f0fe94e8 | |||
| c0c4c603aa | |||
| 0abdebfe8b | |||
| 8e2e4e6058 | |||
| aaee28b34f | |||
| 335a7d03c3 | |||
| f61248cbbd | |||
| 83d6e8ea82 | |||
| fdcbd59557 | |||
| 3a06908755 | |||
| 5f6e0ae033 | |||
| e1e919dd2c | |||
| 98d2b8a0fe | |||
| e60d2fde7e | |||
| 5d3351f5cc | |||
| b2273ab7a1 | |||
| 17a26bfe68 | |||
| d44f2657f4 | |||
| 1c3890651d | |||
| 92f3737be6 | |||
| 3cc5e48aac | |||
| c01cbdd3f2 | |||
| 84d1af1332 | |||
| e3016482e1 | |||
| a58a58f30d | |||
| 7dd7f5a611 | |||
| aad0cbe496 | |||
| 68d5097d96 | |||
| 06085a0c74 | |||
| f416ebfdd8 | |||
| 4de29b8178 | |||
| 12894aff99 | |||
| f20390036b | |||
| 9763c13a34 | |||
| 54c93e4982 | |||
| b6d71cbeba | |||
| a4ec6cb6d6 | |||
| 1ea377bdbf | |||
| 827f8c3e17 | |||
| 32cd372f49 | |||
| b592fc5cac | |||
| d077aa6a90 | |||
| 392d48ba01 | |||
| d1aa463b7c | |||
| fb8ddfa07d | |||
| 2803694cdf | |||
| a7b8cc5810 | |||
| f8b9ec9626 | |||
| c3d9a60067 | |||
| a70d05cb63 | |||
| 5682b3bc09 | |||
| 29674213e8 | |||
| 9b6fe94bc8 | |||
| c0c30438ab | |||
| 814e415374 | |||
| dfd193e3b0 | |||
| 82164b098a | |||
| 4b1c77ec5a | |||
| d9d143eba7 | |||
| 097cb99ae5 | |||
| aae8989f99 | |||
| 70174e1fea | |||
| 301cf3561f | |||
| 18864d9722 | |||
| 9c6bc066f2 | |||
| 83b5d8f20e | |||
| ebd85ebaa2 | |||
| 1962e7ec1d | |||
| 73bd3a0598 | |||
| 698ab5328f | |||
| 87ae677fc3 | |||
| 5d3c14f309 | |||
| 33425a43d0 | |||
| 608eca6166 | |||
| a49dfa8aa2 | |||
| 3caed26f93 | |||
| 7aeb8cf482 | |||
| 16ec5fe9ff | |||
| aff106f0d6 | |||
| 4b575d3d2b | |||
| 0f2424d140 | |||
| 9c80774140 | |||
| d46b5ab0b6 | |||
| 5b13cf4e92 | |||
| 649aff2c92 | |||
| 5d8c485a51 | |||
| d8ff18322e | |||
| 4780c560e1 | |||
| d94f415db4 | |||
| 4b6ed22476 | |||
| 868afffc4d | |||
| 6a81ec226e | |||
| c0fc7c9f7e | |||
| 1bf1147d18 | |||
| 2bdd44ce1d | |||
| 910bf531fe | |||
| 66fcc1e486 | |||
| 628de09938 | |||
| 703a95c486 | |||
| 48cb625f19 | |||
| 266d7c6da7 | |||
| b5ab076546 | |||
| e5c2ba4bd8 | |||
| d62cb9650f | |||
| dfdaea786b | |||
| 6e89ecfbf0 | |||
| 742c1944eb | |||
| e1baa25713 | |||
| 189ae93e90 | |||
| 4431a2d48a | |||
| 3cdee30fc1 | |||
| 592a9338eb | |||
| 55b7fd617c | |||
| ca2f598d9e | |||
| 8cdfd43863 | |||
| e9e21c1b25 | |||
| 61ea1c9003 | |||
| 39ef1d550d | |||
| a425ddf97f | |||
| d38995b3f9 | |||
| 3490a9f726 | |||
| 610e3ffc5f | |||
| cfeda54fc6 | |||
| 2a11df01c7 | |||
| 08fd85ea09 | |||
| 32d1997dfc | |||
| 00249ab96e | |||
| 7710b0b14f | |||
| cc6e5b868e | |||
| 5f83184512 | |||
| 16a35081ed | |||
| ddb6d6af90 | |||
| 1f8f7c853b | |||
| a25a88bd25 | |||
| 94832d1a8b | |||
| 2552c0f393 | |||
| 797fbb2fda | |||
| 35d4749ba7 | |||
| c5ddca3bc4 | |||
| a6b7c98002 | |||
| 84b721abb0 | |||
| 7ea6cb3e95 | |||
| 15e76b90b1 | |||
| 5c41b992b7 | |||
| 5ce6c07d9c | |||
| 8090220963 | |||
| 314a9dec6c | |||
| 48fc26a35b | |||
| b420d7022a | |||
| 79e543eceb | |||
| 375e976289 | |||
| 071d440567 | |||
| d4053e2c54 | |||
| a0ba610e0b | |||
| c995331f57 | |||
| 577b45f8ce | |||
| 0c7e0167c7 | |||
| 9761934bf5 | |||
| 04d62bbe1e | |||
| 115300fa5e | |||
| ae8943e050 | |||
| 11292b5e5a | |||
| 559fe35e1b | |||
| 32b5e5d48b | |||
| 3de9c54552 | |||
| b29a50bed5 | |||
| d36220ed7c | |||
| 117fd1612b | |||
| 2c506717c8 | |||
| e57721be09 | |||
| 954bba6fdf | |||
| edbf831a0f | |||
| c0172c4858 | |||
| 06f5b67b23 | |||
| 07c5f625d4 | |||
| 805044ed41 | |||
| 4da7d31770 | |||
| cdab735ce5 | |||
| e989b9aff7 | |||
| a8466f5069 | |||
| 95a3fe136d | |||
| 88d1aa615c | |||
| ac8500759c | |||
| 07f01eb985 | |||
| 953d759238 | |||
| 9d09401ede | |||
| 872f32a7e0 | |||
| 05b3077a7a | |||
| 8fdd8648c7 | |||
| 4e8383ea4f | |||
| 80791860fe | |||
| 9698a50ad8 | |||
| a390c3b40b | |||
| 70a5517c5c | |||
| 262bee51b1 | |||
| 897bb5aed2 | |||
| 24266fe254 | |||
| 5d6c4852b1 | |||
| 64e0356756 | |||
| b611aa2503 | |||
| 1cdde23bc4 | |||
| b3c8f80b6e | |||
| 35f598a095 | |||
| ad538f3f28 | |||
| 073d9db29f | |||
| cebe7bee92 | |||
| 7bd0f2c50c | |||
| fb8f123616 | |||
| 87632e7ead | |||
| 88be0ee9c1 | |||
| 000be73e80 | |||
| 551711261d | |||
| 918d95a672 | |||
| f79e6b87e2 | |||
| 529d1e0117 | |||
| 0dafd8cd65 | |||
| 1dbb0226ad | |||
| 05826de639 | |||
| 4e05fe9f4a | |||
| 26172ded49 | |||
| 814d4dcb8b | |||
| b9f415e81e | |||
| 124efe6f67 | |||
| f2e2976496 | |||
| 44be0cf12a | |||
| 64d9a1f633 | |||
| 6f9664c9f4 | |||
| f1c1b3d319 | |||
| 6007aa7056 | |||
| acd8ecbd22 | |||
| eab68131e7 | |||
| 8807a2abaf | |||
| e2aab504fb | |||
| 9567f8f28f | |||
| 1e09577a8a | |||
| 155abb262b | |||
| 8782e0068f | |||
| 66e4ea979c | |||
| a957a02d5d | |||
| 8079da7b0a | |||
| 8793efebc7 | |||
| f5d2ea0efa | |||
| e720b4d5e3 | |||
| d3f4db258f | |||
| 13d1048d7e | |||
| d9001409a2 | |||
| 890c191884 | |||
| 8d3441b51b | |||
| 1912a63f17 | |||
| e4c6040de4 | |||
| b8c7ab9867 | |||
| d56d50e96f | |||
| f375d8c141 | |||
| 7f9e1c8799 | |||
| ed52cfc43c | |||
| 09893651a4 | |||
| 7e48034cc8 | |||
| 8815e77576 | |||
| db9b3febd3 | |||
| 36d1ba34a9 | |||
| 105961a78d | |||
| 23040a1bf7 | |||
| 07066e1c1f | |||
| 4b58017951 | |||
| 5db5341974 | |||
| 9a3d097331 | |||
| c50b369d72 | |||
| 28893ec388 | |||
| 6968edd704 | |||
| f4010b597f | |||
| 2bacbf1b53 | |||
| 0ce74b956c | |||
| e2725483e4 | |||
| 03990ab19e | |||
| 8b89023aff | |||
| 75de41bb2e | |||
| 54e555f295 | |||
| dbb500c4b8 | |||
| 61ca5c7aab | |||
| ae3b4f6640 | |||
| 9ff6c1bc16 | |||
| 68a6655cb4 | |||
| ec97c354e2 | |||
| db079065e4 | |||
| cbebf8c4fe | |||
| 92a532a4ba | |||
| 4601b43651 | |||
| 9ebabf818d | |||
| 68385f0ff7 | |||
| 3b1400b6f8 | |||
| 5b5e13fe2c | |||
| 7b8568f5c8 | |||
| ea3ca1a8ca | |||
| baa869552f | |||
| c9c31fa1c2 | |||
| 5ff38eff00 | |||
| f9420acdfd | |||
| c83f8f2a45 | |||
| 4741df627d | |||
| 04e5434c9a | |||
| adf463a4be | |||
| a455f23520 | |||
| 3af08059da | |||
| bf3d2ebea3 | |||
| 8ab7bfd5fa | |||
| d03eefc49a | |||
| 5766b65eeb | |||
| 11d27007fc | |||
| 4ed06be10d | |||
| 6a0bda47ae | |||
| 41466eae66 | |||
| 440b49ce5c | |||
| 7b8c52b043 | |||
| 3a94c2115f | |||
| 6785c07793 | |||
| e23b22857f | |||
| 3bf5388da0 | |||
| 15df8fef95 | |||
| 8df7e1cc8b | |||
| 7b2dfa39cf | |||
| fdc8f79728 | |||
| f76bcda64f | |||
| 718fa18e7f | |||
| b8b61ce214 | |||
| a2ad774112 | |||
| d94ba8c64f | |||
| 81d07d886c | |||
| 147e948fe6 | |||
| d8c7fbd9dc | |||
| bd7fbea155 | |||
| 628e2fb716 | |||
| 503f4d73a0 | |||
| 8a927df2b3 | |||
| 14ddcfb06e | |||
| 932ef6270b | |||
| 1be68f57a5 | |||
| cfdf4a032e | |||
| f5a7a1bcf0 | |||
| e60b07336d | |||
| 48660c482f | |||
| 27ee7c32ca | |||
| 88f1552a70 | |||
| faa5cf54eb | |||
| 2bdc754140 | |||
| 597999acba | |||
| 81605b731d | |||
| d7673257b4 | |||
| 54c8b4f8bb | |||
| ad31540b10 | |||
| 8c7bbb0e07 | |||
| 0f60dfcfc5 | |||
| 0327b4f8fc | |||
| 4b7f05e913 | |||
| 0ea12b3b4a | |||
| 28a83da421 | |||
| 4d41f94199 | |||
| 692f95da79 | |||
| 872c35efd2 | |||
| 65e7688a3f | |||
| b1f2adf9c0 | |||
| 81cea84a37 | |||
| 5f8d3dec82 | |||
| e5359826e1 | |||
| 1dec224210 | |||
| 691ea2d138 | |||
| 4af4faacef | |||
| 54a253c0bf | |||
| 89ff804333 | |||
| 4d16370dba | |||
| 30b3bd1c3d | |||
| ff15ccaf28 | |||
| 6af9285874 | |||
| 9154e93089 | |||
| ea2c9a2986 | |||
| c01475701d | |||
| 0721c7ddb2 | |||
| 36e4836c06 | |||
| b777617515 | |||
| 8a60154882 | |||
| d2e70c16b4 | |||
| e6d20265bf | |||
| 5602deb226 | |||
| e4289405f5 | |||
| 76973fdc30 | |||
| d5d6b80e08 | |||
| 1ed2b8841d | |||
| a33898847b | |||
| bc1eb9a792 | |||
| 3b3fede756 | |||
| 9f72eb9490 | |||
| 2c181bca4f | |||
| b67c51c267 | |||
| 7fee5b59e8 | |||
| 5d6f66be54 | |||
| 5075c2730c | |||
| 1d5ba87b72 | |||
| 4fa44f90bb | |||
| a86da98cc2 | |||
| 6dff06579c | |||
| 38cbf24e8e | |||
| 642c4f5cfc | |||
| 89e4d47ee1 | |||
| 896d451c47 | |||
| abe0bf0a29 | |||
| 804c8e6c43 | |||
| b64d855b25 | |||
| beb4ecb581 | |||
| ed7667dc8d | |||
| e136199bfd | |||
| 95665c8bc0 | |||
| 6bb8fd7853 | |||
| b3038545f5 | |||
| 44fe3a62a1 | |||
| ba1b3e0c7c | |||
| 6435ea655b | |||
| 193a5ed3c9 | |||
| 4e6b80d7df | |||
| e7fb512ae6 | |||
| ab57fb329f | |||
| fd6f4385d4 | |||
| e03ee913f8 | |||
| 8aed72a085 | |||
| 2182ebdd16 | |||
| e575c27601 | |||
| 439081e1db | |||
| 1968ae60aa | |||
| b7a4ca4cff | |||
| b276087969 | |||
| 6e091dd5b8 | |||
| ea5478c61c | |||
| 1f2fd380b2 | |||
| 012954e7f4 | |||
| bc0c8d529e | |||
| ba3ccc7d3d | |||
| b3f6d1cf63 | |||
| af0375ca99 | |||
| a1a0ae62ba | |||
| dec4772740 | |||
| 85dd7a03e2 | |||
| cf674b6a46 | |||
| 102f960d25 | |||
| 7e2e43c51c | |||
| 161d79eaa1 | |||
| eacffb5126 | |||
| 40fa17f5b7 | |||
| 9eee50da66 | |||
| 2b9bb422cf | |||
| ca18fca20b | |||
| 33be17d5d0 | |||
| c2bb1b9ad1 | |||
| 70490277eb | |||
| 48fff04ffd | |||
| 6bf7b212cf | |||
| 01e080842a | |||
| 25e98db5aa | |||
| fce78ea84e | |||
| e414c88347 | |||
| 56b3b2cec6 | |||
| 13c39dbeab | |||
| 5e05411036 | |||
| a6514b0cff | |||
| 2e9ca5642a | |||
| 06c9b1abe3 | |||
| c97f7e9ed0 | |||
| e1f898e6a5 | |||
| 75953d6022 | |||
| 6c90af1bd7 | |||
| 8706ab68f8 | |||
| 9386e9df51 | |||
| 418fac8574 | |||
| 4c1e3ac03a | |||
| 4d6d13b9c8 | |||
| cccad6104c | |||
| 259ba3b794 | |||
| 9c5cb08ac0 | |||
| d7f30167a0 | |||
| e20bdfa2b5 | |||
| 12cf187e51 | |||
| ed0974eba3 | |||
| 692f9d893a | |||
| 21887877da | |||
| 0dd623970b | |||
| 73227623e6 | |||
| eeea757719 | |||
| f6a3c765e9 | |||
| 8414718371 | |||
| 78af96dab3 | |||
| 2598001c8f | |||
| c22b4fcfb2 | |||
| 2b20055eb5 | |||
| e846fedc3f | |||
| 980076fd73 | |||
| 3378d18ca8 | |||
| 70e826ff25 | |||
| 890a8675c9 | |||
| 7627c142c4 | |||
| 84c46affa3 | |||
| 5b1746e451 | |||
| d7592cce29 | |||
| 088086715f | |||
| 7c9d139031 | |||
| 28ac6f9f52 | |||
| b39a72ceac | |||
| 6aeb382706 | |||
| 87cd9a3afc | |||
| 95384f0f33 | |||
| 00ea5d9cad | |||
| 1563ecdf1a | |||
| 7ab8872dd5 | |||
| 5d812d4aa7 | |||
| 0ca6ec1377 | |||
| 1d0d32858b | |||
| 8e80e02a78 | |||
| 9e7bee89c8 | |||
| fb3edeb206 | |||
| 516aa26589 | |||
| 7d19e9aa34 | |||
| 64c49cd137 | |||
| c5950ac929 | |||
| ea92bc676c | |||
| 839136118f | |||
| 6751413673 | |||
| 094fb176b6 | |||
| 75f45c8df6 | |||
| e9e0ba216b | |||
| bbc92d912f | |||
| 81e90c5699 | |||
| 54303a03bf | |||
| 78ea330d3a | |||
| 93c46280ab | |||
| c119d71d8b | |||
| 1e0a240701 | |||
| 2f7d271c16 | |||
| 9e545c34ba | |||
| 69b0b04e70 | |||
| de8aa7b4f7 | |||
| ebd913f50f | |||
| 409afc6684 | |||
| c2c9d5ecc0 | |||
| e926d7e928 | |||
| 78489391d2 | |||
| 5dc7dfd4c8 | |||
| 0436780292 | |||
| 6389749931 | |||
| abed1e2986 | |||
| 9449b3ef23 | |||
| 30d3cafa0c | |||
| 467005b603 | |||
| c67d5afaf4 | |||
| 393cfb1851 | |||
| a4ccbf0386 | |||
| f220f2e743 | |||
| 5cae4e08af | |||
| e928d02c6f | |||
| 95e06ec732 | |||
| 0570cd9439 | |||
| c3d9c73d00 | |||
| a7dcd26588 | |||
| 735d9e5894 | |||
| 8e41bccf8b | |||
| 4b5aae1123 | |||
| 8538bdcce2 | |||
| 7aa99448c3 | |||
| 29c851f463 | |||
| d20abe05ed | |||
| 560ef86747 | |||
| e68c75451e | |||
| 355bb0a091 | |||
| 2a8c6dabaf | |||
| 772a585b80 | |||
| 51bf07fc34 | |||
| 59d21d9683 | |||
| 1286357bcf | |||
| fa60881e2d | |||
| 990b897e94 | |||
| 64709f1d6b | |||
| b8d93d0179 | |||
| 666bec48bc | |||
| 7e008d00e4 | |||
| c07c284c5e | |||
| b4d5ad95cf | |||
| ea959a17e2 | |||
| e2c6dc33fb | |||
| 261772c4e2 | |||
| 27eed9d9bd | |||
| 8a97936228 | |||
| 0df1f6007b | |||
| 8270304485 | |||
| 93c75c1e4d | |||
| 2f67b62509 | |||
| 74172dac32 | |||
| 96d9fd80eb | |||
| 8dec390258 | |||
| 6baffc4b0b | |||
| c6594edc92 | |||
| 80e8d4fbd5 | |||
| aa4a31d81b | |||
| df93cfde8b | |||
| f56f27a2e1 | |||
| 3e496dba50 | |||
| b55362a35b | |||
| 88b64950dc | |||
| a0f2e38b89 | |||
| fe2323cd33 | |||
| d09792e402 | |||
| ac7f2c94fb | |||
| 906dcfe423 | |||
| b5ce7e7bd0 | |||
| c9c03fd9b1 | |||
| 960c90cdff | |||
| e1d78c01f0 | |||
| 76d1b6e4d2 | |||
| 94e5ee71c7 | |||
| d455b0d318 | |||
| 535d1ec30f | |||
| 52deff1db2 | |||
| 4603ee136d | |||
| da657b8643 | |||
| c2888e56a4 | |||
| 92044cedb9 | |||
| fb81ebf301 | |||
| 278d934973 | |||
| 0937c2e6f6 | |||
| e1ad8fe8e0 | |||
| 27f2cd3db0 | |||
| 038adea6d7 | |||
| b1501fcb1d | |||
| b877d15616 | |||
| 1c72472706 | |||
| 0e4057f3a3 | |||
| c79e62d80e | |||
| f4249c591a | |||
| 37cadc7c29 | |||
| a1cf8b613d | |||
| 798464b68e | |||
| 87de0b5883 | |||
| 5003ed1493 | |||
| f12f03b8da | |||
| 9856270e75 | |||
| c22fa4e3fd | |||
| f7f30f1636 | |||
| ae9a554808 | |||
| 5ecd74b2e4 | |||
| dd29e2087c | |||
| 99ce9419aa | |||
| 0d771565f9 | |||
| c35f8c55ac | |||
| 703e965172 | |||
| 046bfa9dad | |||
| 035d319122 | |||
| 81b730d8c3 | |||
| 6170be95e1 | |||
| 34687733e5 | |||
| 8ce622d11e | |||
| 9ff4609f3d | |||
| 15cdaead50 | |||
| 3cde28a438 | |||
| d92d7771d9 | |||
| f6d7933601 | |||
| abf525390e | |||
| fea80843eb | |||
| 1a7baf631e | |||
| cbd4aa0a37 | |||
| 98beff4455 | |||
| 574893368b | |||
| 1f5a084ccf | |||
| 8dda7ea8fa | |||
| e619cc1eb7 | |||
| 1a206ff36c | |||
| afe7d055a1 | |||
| 9d4d8667ff | |||
| 8feab722f8 | |||
| 7ad5bc7dee | |||
| 71bc043d19 | |||
| 171c334cb1 | |||
| 4f93e739f5 | |||
| fa00cba8bb | |||
| 678527e1e6 | |||
| 7fc040d86c | |||
| ba05dd5b8c | |||
| b697bcb4fd | |||
| 2982741dd1 | |||
| 3f9b5511d1 | |||
| 35f2a004e1 | |||
| e02407ef37 | |||
| bf9ad7a5c0 | |||
| 186bd975c1 | |||
| 007b7e2403 | |||
| a6dbd6bcbb | |||
| 009ead2eea | |||
| 3de6f60f90 | |||
| 2ccb1d2813 | |||
| 89523241fd | |||
| 5734afc7ab | |||
| 4e18286c1c | |||
| 10e27536ba | |||
| 9883ca0d84 | |||
| aab1fd2147 | |||
| 652ef6e160 | |||
| 53ad5810ef | |||
| d0838d6784 | |||
| 1bbe38e376 | |||
| d155f9a020 | |||
| 7e6ac4222f | |||
| d0395906ab | |||
| ac2771b997 | |||
| 470c81b8ad | |||
| f5485088a0 | |||
| 2ee7a95fc4 | |||
| 6b3f008b32 | |||
| 8ab5df490a | |||
| 787f8265d3 | |||
| 293700ce69 | |||
| f97aed0e41 | |||
| afaeae8e6b | |||
| b150dea329 | |||
| 4b12260af3 | |||
| 9d5c1fc14c | |||
| c7a9fc6c8e | |||
| a1b62a1f4b | |||
| 1a20e62de7 | |||
| 4fdada55b8 | |||
| c5e7ca0c5e | |||
| 97d675f216 | |||
| 9a0cf2b507 | |||
| 9b7c1d7c4d | |||
| 968f80cc1b | |||
| f185aa2eae | |||
| 6519aaff4d | |||
| 9f0fe18dfc | |||
| 954cce463e | |||
| e3c62825b6 | |||
| 1e654acc29 | |||
| 59e3baf819 | |||
| 8494f10583 | |||
| d509eaa959 | |||
| 85dde94dd5 | |||
| c9a10353f2 | |||
| 41faa59121 | |||
| 833802677b | |||
| 5824cb649e | |||
| fe257effce | |||
| 47168ca3da | |||
| dfd59f42a9 | |||
| 557e038869 | |||
| a628e84b63 | |||
| 36db5e47dd | |||
| 56d80b91c9 | |||
| b0f48c9660 | |||
| 68a2020516 | |||
| 13db642143 | |||
| f2c344b39a | |||
| b42b04e887 | |||
| 229a81bfd6 | |||
| 6d9920c239 | |||
| 7b6132b71e | |||
| 47dff21eda | |||
| 32a8ec7782 | |||
| 4c60db94a2 | |||
| 42680164ea | |||
| 5cf56adce7 | |||
| 10173b990d | |||
| fc74526699 | |||
| 55973b5bf1 | |||
| 406aaa0b50 | |||
| 33b198d505 | |||
| be70d38e2a | |||
| 1e70b4f804 | |||
| 4c0bbebb07 | |||
| 9835c4ad92 | |||
| a49a7fe1f9 | |||
| fa42373cf8 | |||
| a8c36b4041 | |||
| b91773a276 | |||
| fbc01f015d | |||
| 5d26e36e1f | |||
| c4f60dd163 | |||
| b4220bd824 | |||
| 73c5bfed28 | |||
| 2456beaf88 | |||
| cc6f97bee9 | |||
| 8f47e6ffe9 | |||
| 544b24b0ef | |||
| 94d3ffa18d | |||
| 1e13e06e34 | |||
| 9e83bcb6ce | |||
| 917e9016cd | |||
| 5a4aa71f6a | |||
| 6606703001 | |||
| 0c35d8890d | |||
| 76454e6b8b | |||
| 7d7bd97812 | |||
| 10d4fe53f2 | |||
| 3fcfe98de2 | |||
| b16c660258 | |||
| 7df36e3d49 | |||
| 3e11e0dbb4 | |||
| a2b80f03d3 | |||
| e60349604d | |||
| f5ba9a5eea | |||
| e7ec86140b | |||
| 5bba24ce77 | |||
| b8af613aef | |||
| 2e4d5f25cb | |||
| 534e50da57 | |||
| 1ffa06884a | |||
| 670e212d9b | |||
| 6fc703b4a3 | |||
| 33b7062515 | |||
| a7bbeb7ef5 | |||
| 34ec4ac3c9 | |||
| 3f227de023 | |||
| a417aa23fa | |||
| f8fdfd1613 | |||
| 1fdd560ff0 | |||
| 878969a5c4 | |||
| c6d073ebd7 | |||
| 2b5ae869cd | |||
| 2db52eed7c | |||
| 656edce75a | |||
| 4c021dea5e | |||
| bbd76dafa0 | |||
| 4f2c91d848 | |||
| 6f5b6900d6 | |||
| eddc7bd6af | |||
| dfef6370b6 | |||
| b5babdb6c4 | |||
| df53eb00ab | |||
| 63915171fc | |||
| 4873914d4d | |||
| 8a2732ab5d | |||
| f069387d58 | |||
| 8815c05108 | |||
| dfc2ed7a33 | |||
| 38a8253aa8 | |||
| 5fed5cdeed | |||
| df50d59de2 | |||
| 1651952903 | |||
| 33a43af20e | |||
| 6a66a67454 | |||
| 83b908754e | |||
| 02b0ed2b42 | |||
| c124cf5b62 | |||
| dfdf794199 | |||
| df2c0bd6e9 | |||
| 2e04a3753d | |||
| e1e864c606 | |||
| 7f3067553b | |||
| 253d6c9af8 | |||
| 1f1ae0059e | |||
| 05a190ee5a | |||
| ed561f7b9e | |||
| fba12b3926 | |||
| 1b2ef50ded | |||
| c79481c6fa | |||
| f5b98a62ab | |||
| f5a6972feb | |||
| 1faaba39c7 | |||
| e58eb70924 | |||
| 4bfc1041cc | |||
| b6f7e85964 | |||
| b9b02ff167 | |||
| 1aba90cd0b | |||
| 3f689e9c74 | |||
| d744948a27 | |||
| ccc3a005cb | |||
| 605c3b056f | |||
| 387d25c438 | |||
| 050f62baa7 | |||
| afe4f628c3 | |||
| 3964eea2e2 | |||
| 687ebabca9 | |||
| 635ee5f130 | |||
| b27795c0d1 | |||
| f21faa9e2e | |||
| 77de8383fe | |||
| cdfd5f9f0c | |||
| 5eb7029568 | |||
| 52910d50b4 | |||
| 8dcfd1176f | |||
| a85f29d904 | |||
| 292425fd80 | |||
| 19266f643c | |||
| 3853d0c9d7 | |||
| 3c59e186d9 | |||
| e448a76484 | |||
| 0ee8d2733a | |||
| 38ae79ba3c | |||
| d06371d458 | |||
| 8b200c8200 | |||
| 2a285de1b0 | |||
| 093588d5a3 | |||
| a9e990142a | |||
| 42bec97b68 | |||
| bc8641666c | |||
| 74342a8775 | |||
| bc92c02772 | |||
| f434b4d970 | |||
| 86657736e8 | |||
| a037e6b1a7 | |||
| 4570d7194e | |||
| a7c97e731d | |||
| c093b68078 | |||
| c824278565 | |||
| 8ee3b6f909 | |||
| 9e1024f4b5 | |||
| 8cab5b811f | |||
| 1c36e62e2c | |||
| 0a92f7161a | |||
| 8af768bf30 | |||
| 61375a6245 | |||
| 3c08b5cf09 | |||
| 498d7b4666 | |||
| 7cfa9dbf58 | |||
| 5aa6aeac16 | |||
| b6261d014c | |||
| 8455164a69 | |||
| b959d4be29 | |||
| 39d6ebdd53 | |||
| 0f52cd8039 | |||
| 4a50561867 | |||
| cb8a42a529 | |||
| d16d21c54f | |||
| 339dfefb53 | |||
| b8e8cfd149 | |||
| a40cf26aa7 | |||
| fe8ecbbeb8 | |||
| 2686e987a6 | |||
| a3f078cb96 | |||
| 82659142c5 | |||
| 3c03c64778 | |||
| fdbed7fbf8 | |||
| 759bc76f04 | |||
| ab1657b811 | |||
| b219a347f6 | |||
| bd82738630 | |||
| 9829b45ce8 | |||
| f2c0abeb58 | |||
| 55731922e8 | |||
| 671efb2313 | |||
| da052ed9aa | |||
| 49e722ffbf | |||
| de2e2539b7 | |||
| 5f87ccdcd9 | |||
| bf2195b999 | |||
| 3042209cf6 | |||
| 87728268e2 | |||
| bbbd5648e0 | |||
| b813735dd2 | |||
| 871a245702 | |||
| f783fb9830 | |||
| fa5aea2ca1 | |||
| 2128d0bfcc | |||
| 42061c9bf5 | |||
| d0851c8a9e | |||
| 0817bc490e | |||
| 3621b62418 | |||
| cbc0739041 | |||
| f752bded4b | |||
| 6cbb07bc2d | |||
| 4d048934e6 | |||
| 544a66fa34 | |||
| 533c86d285 | |||
| 15e9d03c2c | |||
| ef3f827769 | |||
| d34b83851f | |||
| 61eefbbadd | |||
| 544d15c574 | |||
| 8ed06883f6 | |||
| 39edd32072 | |||
| 051d935b28 | |||
| 37904007bb | |||
| 3dc9f78051 | |||
| 729a29352b | |||
| 626f4b92b5 | |||
| fa6230a08a | |||
| 866ab33c45 | |||
| 7764f01b59 | |||
| ff9bb94ab4 | |||
| 28412344d5 | |||
| bae6b33497 | |||
| 7a21bdd573 | |||
| 43011261a8 | |||
| d8b60dfe55 | |||
| 70ab0f446d | |||
| af74988a83 | |||
| 31b7063e68 | |||
| f42883eaad | |||
| 7de782d3a2 | |||
| cf26557cc2 | |||
| 29d11db73a | |||
| e8c5b0766e | |||
| 041ef66c01 | |||
| b3bbcd1729 | |||
| 69ccc5a9f9 | |||
| f9f83eaa39 | |||
| f243a684d4 | |||
| a82de88963 | |||
| 97e15feb0d | |||
| 51072a5dab | |||
| 500bb6e940 | |||
| 140630745f | |||
| 1ea9c9a915 | |||
| b8001b78b4 | |||
| a6916dd9dd | |||
| b96624890e | |||
| 7e9090399b | |||
| 9c53c478d9 | |||
| e0447c8190 | |||
| 4bcbcb146e | |||
| ae0f4c4cc5 | |||
| 40b465a093 | |||
| a4de38ce28 | |||
| d6d51ef4b1 | |||
| 92b0ba21e4 | |||
| 82a99b5267 | |||
| 349e3cae06 | |||
| 48b3307934 | |||
| 24253128ae | |||
| 018544b775 | |||
| 44dcd1abdf | |||
| 9c62908162 | |||
| 913cb39d28 | |||
| 804a941762 | |||
| a7b83d9cd1 | |||
| 924405270a | |||
| 4c8412f4a1 | |||
| 8f46a87f52 | |||
| c47ac8d6b1 | |||
| 3f09a6abdd | |||
| b94faa3121 | |||
| 56a30b3a02 | |||
| ed198a22b6 | |||
| 13b97f6dba | |||
| b09b667782 | |||
| 85fde6796a | |||
| db1efcd1fc | |||
| f80e4b3f06 | |||
| 5049e8bfbe | |||
| 6e4a57046e | |||
| afc8597d3b | |||
| 5ff69ad1a9 | |||
| 05663a42ed | |||
| 7c2b6ede1f | |||
| c6100404e5 | |||
| df65081785 | |||
| 1bcc74738e | |||
| 0e9a1b42ec | |||
| c3081de611 | |||
| 97731d1240 | |||
| e2432d476e | |||
| 6e571c579d | |||
| 8c499a63dc | |||
| ce0cb273bb | |||
| 971cdf37f6 | |||
| 699039f788 | |||
| c3e25bda1a | |||
| 606a57203e | |||
| 3c38b26770 | |||
| 67912f3768 | |||
| bf8945430f | |||
| cb33f26d5c | |||
| ddea1bdb9e | |||
| 9b90175500 | |||
| 44f6e7a0cf | |||
| 55692802c0 | |||
| 5be7bb980d | |||
| 33c49c5cd8 | |||
| d348e77468 | |||
| a5ff4fbef7 | |||
| cfa69e74f4 | |||
| 36ceb66ba5 | |||
| 2fdad12521 | |||
| 623408913c | |||
| 9bae87b0ac | |||
| 00e11d33f1 | |||
| 680f77beb9 | |||
| 356506060c | |||
| 4e9fe79619 | |||
| e4a9cf0bba | |||
| f9de77a75d | |||
| 315e77ebf2 | |||
| da82fbab4f | |||
| 4f46212d1a | |||
| d4fe6f5133 | |||
| ee838087ca | |||
| b540b8df62 | |||
| 1157594530 | |||
| 8d7fe5e575 | |||
| 0598bdebc7 | |||
| 7d851a10b5 | |||
| e401c87246 | |||
| 2cf6ad21d3 | |||
| adca302dfe | |||
| 68df9b6ed2 | |||
| e57d70b089 | |||
| 5f1e972b3d | |||
| 3726492cf9 | |||
| bc8c2752e4 | |||
| d27a08bc91 | |||
| 5192feb836 | |||
| a916288d03 | |||
| 96e26651bc | |||
| 725fd817c2 | |||
| 79dd53639b | |||
| 578ddd2698 | |||
| e175ab79c7 | |||
| 937d0aca79 | |||
| 0bd438e617 | |||
| 1fd1570e07 | |||
| 91e9a5f9f3 | |||
| aa5f532f86 | |||
| c499d2e465 | |||
| 3d734a120d | |||
| 05eeba067c | |||
| 778867e3ef | |||
| 4432be48e0 | |||
| f3620e7072 | |||
| 46f9c292ab | |||
| e919a82b2c | |||
| 3a8ff2f6b4 | |||
| 26310def0a | |||
| 24377a45ff | |||
| 5566886f20 | |||
| 8f03679935 | |||
| 0053d2a874 | |||
| 55a9e6836d | |||
| cf30c42563 | |||
| ff8089912e | |||
| 909cd42ac1 | |||
| 7433003ffa | |||
| ead91a1e6b | |||
| b53518d1b8 | |||
| 71c89c2670 | |||
| cca73b2622 | |||
| 7644ceea8a | |||
| b0e8f12426 | |||
| 4e5cc03673 | |||
| 76d57baa11 | |||
| 5c9840daf8 | |||
| ae37e6ec9d | |||
| c5df7c5356 | |||
| 27ad1f42da | |||
| 69225ad00b | |||
| db0843a47a | |||
| 9052843acb | |||
| 113587247e | |||
| ee6b804804 | |||
| 63659c9604 | |||
| 100a936f1b | |||
| 80a30bcdd6 | |||
| 728d298810 | |||
| af084528c8 | |||
| d4e80883dc | |||
| 55690ddd54 | |||
| 9a685d685c | |||
| c78406ceb9 | |||
| f454d407af | |||
| df71de5af2 | |||
| 52fd551890 | |||
| 4d5768111d | |||
| aabda60e9f | |||
| 089ab20e7c | |||
| 6e29251886 | |||
| 4a7010bab1 | |||
| b181125e6f | |||
| c4e4830f32 | |||
| 4cc7237db3 | |||
| cb6269d5ec |
@@ -0,0 +1,41 @@
|
||||
# Pass the rustflags specified to host dependencies (build scripts, proc-macros)
|
||||
# when a `--target` is passed to Cargo. Historically this was not the case, and
|
||||
# because of that, cross-compilation would not set the rustflags configured
|
||||
# below in `target.'cfg(all())'` for them, resulting in cache invalidation.
|
||||
#
|
||||
# Since this is an unstable feature (enabled at the bottom of the file), this
|
||||
# setting is unfortunately ignored on stable toolchains, but it's still better
|
||||
# to have it apply on nightly than using the old behavior for all toolchains.
|
||||
target-applies-to-host = false
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[doc.extern-map.registries]
|
||||
crates-io = "https://docs.rs/"
|
||||
|
||||
[target.'cfg(all())']
|
||||
rustflags = [
|
||||
"-Wrust_2018_idioms",
|
||||
"-Wsemicolon_in_expressions_from_macros",
|
||||
"-Wunused_extern_crates",
|
||||
"-Wunused_import_braces",
|
||||
"-Wunused_qualifications",
|
||||
"-Wtrivial_casts",
|
||||
"-Wtrivial_numeric_casts",
|
||||
"-Wclippy::cloned_instead_of_copied",
|
||||
"-Wclippy::dbg_macro",
|
||||
"-Wclippy::inefficient_to_string",
|
||||
"-Wclippy::macro_use_imports",
|
||||
"-Wclippy::mut_mut",
|
||||
"-Wclippy::needless_borrow",
|
||||
"-Wclippy::nonstandard_macro_braces",
|
||||
"-Wclippy::str_to_string",
|
||||
"-Wclippy::todo",
|
||||
]
|
||||
|
||||
# activate the target-applies-to-host feature.
|
||||
# Required for `target-applies-to-host` at the top to take effect.
|
||||
[unstable]
|
||||
rustdoc-map = true
|
||||
target-applies-to-host = true
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Appservice
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test-appservice:
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
name: ${{ matrix.os }} / appservice / stable
|
||||
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu, macOS]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Run checks
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: run
|
||||
args: -p xtask -- ci test-appservice
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Security audit
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Benchmarks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
benchmarks:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
environment: matrix-rust-bot
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench | tee benchmark-output.txt
|
||||
|
||||
- name: Check benchmark result for PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
auto-push: false
|
||||
# comment to alert the user this has gone bad
|
||||
github-token: ${{ secrets.MRB_ACCESS_TOKEN }}
|
||||
alert-threshold: '120%'
|
||||
comment-on-alert: true
|
||||
fail-threshold: '150%'
|
||||
fail-on-alert: true
|
||||
|
||||
- name: Store benchmark result
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Rust Benchmark
|
||||
tool: 'cargo'
|
||||
output-file-path: benchmark-output.txt
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-push: true
|
||||
# Show alert with commit comment on detecting possible performance regression
|
||||
alert-threshold: '150%'
|
||||
comment-on-alert: true
|
||||
fail-on-alert: true
|
||||
alert-comment-cc-users: '@gnunicornBen,@jplatte,@poljar'
|
||||
+105
-197
@@ -1,225 +1,91 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
style:
|
||||
name: Check style
|
||||
test-features:
|
||||
name: linux / features-${{ matrix.name }}
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
name:
|
||||
- no-encryption
|
||||
- no-sled
|
||||
- no-encryption-and-sled
|
||||
- sled-cryptostore
|
||||
- rustls-tls
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: run
|
||||
args: -p xtask -- ci test-features ${{ matrix.name }}
|
||||
|
||||
test-crypto-features:
|
||||
name: linux / crypto-crate features
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
needs: [style]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets -- -D warnings
|
||||
|
||||
- name: Clippy without default features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
# TODO: add `--all-targets` once all warnings in examples are resolved
|
||||
args: --no-default-features --features native-tls,warp -- -D warnings
|
||||
|
||||
check-wasm:
|
||||
name: linux / WASM
|
||||
needs: [clippy]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install emscripten
|
||||
uses: mymindstorm/setup-emsdk@v7
|
||||
|
||||
- name: Check
|
||||
run: |
|
||||
cd matrix_sdk/examples/wasm_command_bot
|
||||
cargo check --target wasm32-unknown-unknown
|
||||
|
||||
test-appservice:
|
||||
name: ${{ matrix.name }}
|
||||
needs: [clippy]
|
||||
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
matrix:
|
||||
name:
|
||||
- linux / appservice / stable / actix
|
||||
- macOS / appservice / stable / actix
|
||||
- linux / appservice / stable / warp
|
||||
- macOS / appservice / stable / warp
|
||||
|
||||
include:
|
||||
- name: linux / appservice / stable / actix
|
||||
cargo_args: --no-default-features --features actix
|
||||
|
||||
- name: macOS / appservice / stable / actix
|
||||
os: macOS-latest
|
||||
cargo_args: --no-default-features --features actix
|
||||
|
||||
- name: linux / appservice / stable / warp
|
||||
cargo_args: --features warp
|
||||
|
||||
- name: macOS / appservice / stable / warp
|
||||
os: macOS-latest
|
||||
cargo_args: --features warp
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust || 'stable' }}
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path matrix_sdk_appservice/Cargo.toml ${{ matrix.cargo_args }} -- -D warnings
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --manifest-path matrix_sdk_appservice/Cargo.toml ${{ matrix.cargo_args }}
|
||||
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path matrix_sdk_appservice/Cargo.toml ${{ matrix.cargo_args }}
|
||||
|
||||
test-features:
|
||||
name: ${{ matrix.name }}
|
||||
needs: [clippy]
|
||||
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
matrix:
|
||||
name:
|
||||
- linux / features-no-encryption
|
||||
- linux / features-no-sled
|
||||
- linux / features-no-encryption-and-sled
|
||||
- linux / features-sled_cryptostore
|
||||
- linux / features-rustls-tls
|
||||
- linux / features-markdown
|
||||
- linux / features-socks
|
||||
- linux / features-sso_login
|
||||
- linux / features-require_auth_for_profile_requests
|
||||
|
||||
include:
|
||||
- name: linux / features-no-encryption
|
||||
cargo_args: --no-default-features --features "sled_state_store, native-tls"
|
||||
|
||||
- name: linux / features-no-sled
|
||||
cargo_args: --no-default-features --features "encryption, native-tls"
|
||||
|
||||
- name: linux / features-no-encryption-and-sled
|
||||
cargo_args: --no-default-features --features "native-tls"
|
||||
|
||||
- name: linux / features-sled_cryptostore
|
||||
cargo_args: --no-default-features --features "encryption, sled_cryptostore, native-tls"
|
||||
|
||||
- name: linux / features-rustls-tls
|
||||
cargo_args: --no-default-features --features rustls-tls
|
||||
|
||||
- name: linux / features-require_auth_for_profile_requests
|
||||
cargo_args: --no-default-features --features "require_auth_for_profile_requests, native-tls"
|
||||
|
||||
- name: linux / features-markdown
|
||||
cargo_args: --features markdown
|
||||
|
||||
- name: linux / features-socks
|
||||
cargo_args: --features socks
|
||||
|
||||
- name: linux / features-sso_login
|
||||
cargo_args: --features sso_login
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust || 'stable' }}
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --manifest-path matrix_sdk/Cargo.toml ${{ matrix.cargo_args }}
|
||||
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path matrix_sdk/Cargo.toml ${{ matrix.cargo_args }}
|
||||
command: run
|
||||
args: -p xtask -- ci test-crypto
|
||||
|
||||
test:
|
||||
name: ${{ matrix.name }}
|
||||
needs: [clippy]
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
name:
|
||||
- linux / stable
|
||||
@@ -239,20 +105,62 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install rust
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust || 'stable' }}
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
test-nodejs:
|
||||
name: linux / node.js (${{ matrix.node-version }})
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node-version: [12.17, 14.0, 15.0, 16.0]
|
||||
include:
|
||||
- node-version: 16.0
|
||||
build-doc: true
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install NPM dependencies
|
||||
run: cd crates/matrix-sdk-crypto-nodejs && npm install
|
||||
|
||||
- name: Build the Node.js binding
|
||||
run: cd crates/matrix-sdk-crypto-nodejs && npm run build
|
||||
|
||||
- name: Test the Node.js binding
|
||||
run: cd crates/matrix-sdk-crypto-nodejs && npm run test
|
||||
|
||||
- if: ${{ matrix.build-doc }}
|
||||
name: Build the documentation
|
||||
run: cd crates/matrix-sdk-crypto-nodejs && npm run doc
|
||||
|
||||
@@ -2,7 +2,9 @@ name: Code coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -16,24 +18,27 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Install tarpaulin
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-tarpaulin -f
|
||||
args: cargo-tarpaulin
|
||||
|
||||
- name: Run tarpaulin
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: tarpaulin
|
||||
args: --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml
|
||||
args: --workspace
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
name: docs
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: docs
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
# Keep in sync with xtask docs
|
||||
- name: Build docs
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options"
|
||||
# Work around https://github.com/rust-lang/cargo/issues/10744
|
||||
CARGO_TARGET_APPLIES_TO_HOST: "true"
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options --cfg docsrs -Dwarnings"
|
||||
with:
|
||||
command: doc
|
||||
args: --no-deps --workspace --features docs
|
||||
args: --no-deps --workspace --exclude matrix-sdk-crypto-js --exclude matrix-sdk-crypto-nodejs --features docsrs
|
||||
|
||||
- name: Deploy docs
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc/
|
||||
force_orphan: true
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: FFI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Apple platform tests
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install targets
|
||||
run: |
|
||||
rustup target add aarch64-apple-ios-sim --toolchain nightly
|
||||
rustup target add x86_64-apple-ios --toolchain nightly
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Install Uniffi
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: uniffi_bindgen
|
||||
|
||||
|
||||
- name: Generate .xcframework
|
||||
run: sh bindings/apple/debug_build_xcframework.sh ci
|
||||
|
||||
- name: Run XCTests
|
||||
run: |
|
||||
xcodebuild test \
|
||||
-project bindings/apple/MatrixRustSDK.xcodeproj \
|
||||
-scheme MatrixRustSDK \
|
||||
-sdk iphonesimulator \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 13,OS=15.4'
|
||||
@@ -0,0 +1,105 @@
|
||||
name: Style
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
style:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: -- --check
|
||||
|
||||
pre-commit-styles:
|
||||
name: Check Style
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: clippy
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Setup Python Env
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Check Styles with pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
with:
|
||||
# only run `commit`-hooks, excludes typos, clippy, etc
|
||||
extra_args: --show-diff-on-failure --hook-stage commit
|
||||
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
needs: [style]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout Actions Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@master
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
needs: [style]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: clippy
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: run
|
||||
args: -p xtask -- ci clippy
|
||||
@@ -0,0 +1,75 @@
|
||||
name: WASM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check-wasm:
|
||||
name: Build test / ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
name:
|
||||
- matrix-sdk-qrcode
|
||||
- matrix-sdk-base
|
||||
- matrix-sdk-common
|
||||
- matrix-sdk-crypto-js
|
||||
- indexeddb-no-crypto
|
||||
- indexeddb-with-crypto
|
||||
|
||||
include:
|
||||
- name: matrix-sdk (no-default, wasm-flags)
|
||||
cmd: matrix-sdk-no-default
|
||||
- name: matrix-sdk / indexeddb_stores
|
||||
cmd: matrix-sdk-indexeddb-stores
|
||||
- name: matrix-sdk / indexeddb_stores / no crypto
|
||||
cmd: matrix-sdk-indexeddb-stores-no-crypto
|
||||
- name: matrix-sdk / wasm-example
|
||||
cmd: matrix-sdk-command-bot
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
components: clippy
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install wasm-pack
|
||||
uses: jetli/wasm-pack-action@v0.3.0
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Rust Check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: run
|
||||
args: -p xtask -- ci wasm ${{ matrix.cmd || matrix.name }}
|
||||
|
||||
- name: Wasm-Pack test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: run
|
||||
args: -p xtask -- ci wasm-pack ${{ matrix.cmd || matrix.name }}
|
||||
+7
-1
@@ -1,4 +1,10 @@
|
||||
Cargo.lock
|
||||
target
|
||||
generated
|
||||
master.zip
|
||||
emsdk-*
|
||||
emsdk-*
|
||||
.idea/
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
.vscode/
|
||||
|
||||
+13
-1
@@ -1,10 +1,13 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.2.3
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-merge-conflict
|
||||
- id: mixed-line-ending
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
@@ -16,6 +19,7 @@ repos:
|
||||
|
||||
- id: clippy
|
||||
name: clippy
|
||||
stages: [push]
|
||||
language: system
|
||||
types: [file, rust]
|
||||
entry: cargo clippy --all-targets --all
|
||||
@@ -23,7 +27,15 @@ repos:
|
||||
|
||||
- id: test
|
||||
name: test
|
||||
stages: [push]
|
||||
language: system
|
||||
files: '\.rs$'
|
||||
entry: cargo test --lib
|
||||
pass_filenames: false
|
||||
|
||||
- id: typos
|
||||
name: typos
|
||||
stages: [push]
|
||||
language: system
|
||||
entry: typos
|
||||
pass_filenames: false
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
edition = "2018"
|
||||
max_width = 100
|
||||
comment_width = 80
|
||||
wrap_comments = true
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
[default.extend-words]
|
||||
# Remove this once base64 gets correctly ignored by typos
|
||||
# Or if we're able to ignore certain lines.
|
||||
Fo = "Fo"
|
||||
BA = "BA"
|
||||
UE = "UE"
|
||||
|
||||
[files]
|
||||
# Our json files contain a bunch of base64 encoded ed25519 keys which aren't
|
||||
# automatically ignored, we ignore them here.
|
||||
extend-exclude = ["*.json"]
|
||||
+12
-10
@@ -1,11 +1,13 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"matrix_sdk",
|
||||
"matrix_qrcode",
|
||||
"matrix_sdk_base",
|
||||
"matrix_sdk_test",
|
||||
"matrix_sdk_test_macros",
|
||||
"matrix_sdk_crypto",
|
||||
"matrix_sdk_common",
|
||||
"matrix_sdk_appservice"
|
||||
]
|
||||
members = ["benchmarks", "crates/*", "labs/*", "xtask"]
|
||||
# xtask and labs should only be compiled when invoked explicitly
|
||||
default-members = ["benchmarks", "crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[profile.dev.package]
|
||||
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
|
||||
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
|
||||
quote = { opt-level = 2 }
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
all: build
|
||||
|
||||
build:
|
||||
cargo build
|
||||
test:
|
||||
cargo test
|
||||
|
||||
coverage:
|
||||
cargo tarpaulin -v
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
format:
|
||||
cargo fmt
|
||||
|
||||
.PHONY: clean test coverage
|
||||
@@ -1,8 +1,8 @@
|
||||

|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
[](https://matrix-org.github.io/matrix-rust-sdk/)
|
||||
[](https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/)
|
||||
[](https://docs.rs/matrix-sdk)
|
||||
|
||||
# matrix-rust-sdk
|
||||
@@ -24,6 +24,10 @@ The rust-sdk consists of multiple crates that can be picked at your convenience:
|
||||
- **matrix-sdk-crypto** - No (network) IO encryption state machine that can be
|
||||
used to add Matrix E2EE support to your client or client library.
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.60`
|
||||
|
||||
## Status
|
||||
|
||||
The library is in an alpha state, things that are implemented generally work but
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# SDK 0.5 - stores and native crypto
|
||||
|
||||
The @matrix-org/rust team is happy to present to the wider public version 0.5 of the [`matrix-rust-sdk`][sdk], now available on crates.io for your convenience. This marks an important milestone after over half a year of work since the latest release (0.4) and with many new team members working on it. It comes with many bug fixes, improvements and updates, but a few we'd like to highlight here:
|
||||
|
||||
## Better, safer, native-er crypto
|
||||
|
||||
One of the biggest improvements under the hood is the replacement of the former crypto core written in C, libolm, with the fully rustic, fully audited [Vodozemac][vodozemac]. [Vodozemac][vodozemac] is a much more robust new implementation of the olm crypto primitives and engines with all the learning included and pitfalls from the previous C implementation avoided in a freshly baked, very robust library. With this release both matrix-sdk-crypto and matrix-sdk (when the `e2e-encryption` is enabled, which it is by default) use vodozemac to handle all crypto needs.
|
||||
|
||||
## Stores, stores, stores
|
||||
|
||||
This release has also seen a major refactoring around the state and crypto store primitives and implementations. From this release forward, the implementation of the storage layer is truly modular and pluggable, with the default implementation (based on `sled`) even being a separate crate. Furthermore this release has also additional support for the [`indexeddb`-storage][indexeddb] layer for in-browser WASM needs. As before, the base still ships with an in-memory store, but if none of them fits your needs, you are very much invited to build your own - we recommend looking at the implementation of the existing [`matrix-sdk-sled`][sled] to understand what is needed.
|
||||
|
||||
We've further extended and cleaned up the storage API and hardened the two existing implementations: both `sled` and `indexeddb` will now encrypt _all metadata_, including all keys, if a passphrase is given. Even a core dump of a database won't show any strings or other identifiable data. Both implementation also hold a database version string now, allowing future migrations of the database schema while we move forward.
|
||||
|
||||
## WebAssembly
|
||||
|
||||
It already came through in the previous paragraph: we now fully support `wasm` as a primary target for the main sdk and its dependencies (browser for now). While it was already possible to build the SDK for wasm before, if you were lucky and had the specific emscripten setup, even crypto didn't always fail to compile, our move to vodozemac frees us from these problems and our CI now makes sure all our PRs will continue to build on WebAssembly, too. And with the `indexeddb`-store, we also have a fully persistent storage layer implementation for all your in-browser matrix needs built in.
|
||||
|
||||
## More features
|
||||
|
||||
Of course a lot more has happened over the last few months as we've ramped up our work on the SDK. Most notably, you can now check whether a room has been created as a `space`, and we offer nice automatic thumbnailing and resizing support if you enable `image`. Additionally, there is a very early event-timeline API behind the `experimental-timeline`-feature-flag. Just to name a few. We really recommend you migrate from the older version to this one.
|
||||
|
||||
## Process and Workflow
|
||||
|
||||
We've also ramped up our CI, parallelized it a lot, while adding more tests. In general we don't merge anything breaking the tests, thus our `main` can also be considered `stable` to build again, though things might be changed without prior notice. We recommend keeping an eye out in our [team chat][chat] to stay current.
|
||||
|
||||
### Versions
|
||||
|
||||
This and all future releases of matrix-sdk adhere to [semver][semver] and we do our best to stick to it for the default-feature-set. As you will see only the main crate `matrix-sdk` is actually versioned at `0.5`, while other crates - especially newer ones - have different versions attached. We recommend to sticking to the high-level matrix-sdk-crate wherever reasonable as lower level crates might change more often, also in breaking fashion.
|
||||
|
||||
With this release all our crates are build with `rust v2021` and need a rust compiler of at least `1.60` (due to our need for weak-dependencies).
|
||||
|
||||
### Conventional Commit
|
||||
|
||||
We also want to make it easier for external parties to follow changes and stay up to date with what's-what, so, with this release, we are implementing the [conventional commits][cc] standard for our commits and will start putting in automatic changelog generation. Which makes it easier for us to create comprehensive changelogs for releases, but also for anyone following `main` to figure out what changed.
|
||||
|
||||
[sdk]: https://crates.io/crates/matrix-sdk/
|
||||
[vodozemac]: https://github.com/matrix-org/vodozemac
|
||||
[sled]: https://crates.io/crates/matrix-sdk-sled/
|
||||
[indexeddb]: https://crates.io/crates/matrix-sdk-indexeddb/
|
||||
[chat]: https://matrix.to/#/#matrix-rust-sdk:matrix.org
|
||||
[semver]: https://semver.org/
|
||||
[cc]: http://conventionalcommits.org/
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "benchmarks"
|
||||
description = "Matrix SDK benchmarks"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.56"
|
||||
version = "1.0.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
criterion = { version = "0.3.5", features = ["async", "async_tokio", "html_reports"] }
|
||||
matrix-sdk-crypto = { path = "../crates/matrix-sdk-crypto", version = "0.5.0" }
|
||||
matrix-sdk-sled = { path = "../crates/matrix-sdk-sled", version = "0.1.0", default-features = false, features = ["crypto-store"] }
|
||||
matrix-sdk-test = { path = "../crates/matrix-sdk-test", version = "0.5.0" }
|
||||
ruma = "0.6.1"
|
||||
serde_json = "1.0.79"
|
||||
tempfile = "3.3.0"
|
||||
tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
pprof = { version = "0.8.0", features = ["flamegraph", "criterion"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
@@ -0,0 +1,80 @@
|
||||
# Benchmarks for the rust-sdk crypto layer
|
||||
|
||||
This directory contains various benchmarks that test critical functionality in
|
||||
the crypto layer in the rust-sdk.
|
||||
|
||||
We're using [Criterion] for the benchmarks, the full documentation for Criterion
|
||||
can be found [here](https://bheisler.github.io/criterion.rs/book/criterion_rs.html).
|
||||
|
||||
## Running the benchmarks
|
||||
|
||||
The benchmark can be simply run by using the `bench` command of `cargo`:
|
||||
|
||||
```bash
|
||||
$ cargo bench
|
||||
```
|
||||
|
||||
This will work from the workspace directory of the rust-sdk.
|
||||
|
||||
If you want to pass options to the benchmark [you'll need to specify the name of
|
||||
the benchmark](https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options):
|
||||
|
||||
```bash
|
||||
$ cargo bench --bench crypto_bench -- # Your options go here
|
||||
```
|
||||
|
||||
If you want to run only a specific benchmark, simply pass the name of the
|
||||
benchmark as an argument:
|
||||
|
||||
```bash
|
||||
$ cargo bench --bench crypto_bench "Room key sharing/"
|
||||
```
|
||||
|
||||
After the benchmarks are done, a HTML report can be found in `target/criterion/report/index.html`.
|
||||
|
||||
### Using a baseline for the benchmark
|
||||
|
||||
The benchmarks will by default compare the results to the previous run of the
|
||||
benchmark. If you are improving the performance of a specific feature and run
|
||||
the benchmark many times, it may be useful to store a baseline to compare
|
||||
against instead.
|
||||
|
||||
The `--save-baseline` switch can be used to create a baseline for the benchmark.
|
||||
|
||||
```bash
|
||||
$ cargo bench --bench crypto_bench -- --save-baseline libolm
|
||||
```
|
||||
|
||||
After you make your changes you can use the baseline to compare the results like
|
||||
so:
|
||||
|
||||
```bash
|
||||
$ cargo bench --bench crypto_bench -- --baseline libolm
|
||||
```
|
||||
|
||||
### Generating Flame Graphs for the benchmarks
|
||||
|
||||
The benchmarks support profiling and generating [Flame Graphs] while they run in
|
||||
profiling mode using [pprof].
|
||||
|
||||
Profiling usually requires root permissions, to avoid the need for root
|
||||
permissions you can adjust the value of `perf_event_paranoid`, e.g. the most
|
||||
permisive value is `-1`:
|
||||
|
||||
```bash
|
||||
$ echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||
```
|
||||
|
||||
To generate flame graphs feature simply enable the profiling mode using the
|
||||
`--profile-time` command line flag:
|
||||
|
||||
```bash
|
||||
$ cargo bench --bench crypto_bench -- --profile-time=5
|
||||
```
|
||||
|
||||
After the benchmarks are done, a flame graph for each individual benchmark can be
|
||||
found in `target/criterion/<name-of-benchmark>/profile/flamegraph.svg`.
|
||||
|
||||
[pprof]: https://docs.rs/pprof/0.5.0/pprof/index.html#
|
||||
[Criterion]: https://docs.rs/criterion/0.3.5/criterion/
|
||||
[Flame Graphs]: https://www.brendangregg.com/flamegraphs.html
|
||||
@@ -1,60 +1,59 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod perf;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use criterion::*;
|
||||
use matrix_sdk_common::uuid::Uuid;
|
||||
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
|
||||
use matrix_sdk_sled::CryptoStore as SledCryptoStore;
|
||||
use matrix_sdk_test::response_from_file;
|
||||
use ruma::{
|
||||
api::{
|
||||
client::r0::{
|
||||
client::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::Response as ToDeviceResponse,
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
room_id, user_id, DeviceIdBox, UserId,
|
||||
device_id, room_id, user_id, DeviceId, OwnedUserId, TransactionId, UserId,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
fn alice_id() -> UserId {
|
||||
fn alice_id() -> &'static UserId {
|
||||
user_id!("@alice:example.org")
|
||||
}
|
||||
|
||||
fn alice_device_id() -> DeviceIdBox {
|
||||
"JLAFKJWSCS".into()
|
||||
fn alice_device_id() -> &'static DeviceId {
|
||||
device_id!("JLAFKJWSCS")
|
||||
}
|
||||
|
||||
fn keys_query_response() -> get_keys::Response {
|
||||
let data = include_bytes!("./keys_query.json");
|
||||
fn keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::Response::try_from_http_response(data).expect("Can't parse the keys upload response")
|
||||
}
|
||||
|
||||
fn keys_claim_response() -> claim_keys::Response {
|
||||
let data = include_bytes!("./keys_claim.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
claim_keys::Response::try_from_http_response(data)
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
}
|
||||
|
||||
fn huge_keys_query_resopnse() -> get_keys::Response {
|
||||
let data = include_bytes!("./keys_query_2000_members.json");
|
||||
fn keys_claim_response() -> claim_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_claim.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::Response::try_from_http_response(data).expect("Can't parse the keys query response")
|
||||
claim_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys upload response")
|
||||
}
|
||||
|
||||
fn huge_keys_query_response() -> get_keys::v3::Response {
|
||||
let data = include_bytes!("crypto_bench/keys_query_2000_members.json");
|
||||
let data: Value = serde_json::from_slice(data).unwrap();
|
||||
let data = response_from_file(&data);
|
||||
get_keys::v3::Response::try_from_http_response(data)
|
||||
.expect("Can't parse the keys query response")
|
||||
}
|
||||
|
||||
pub fn keys_query(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
let response = keys_query_response();
|
||||
let uuid = Uuid::new_v4();
|
||||
let txn_id = TransactionId::new();
|
||||
|
||||
let count = response.device_keys.values().fold(0, |acc, d| acc + d.len())
|
||||
+ response.master_keys.len()
|
||||
@@ -68,32 +67,27 @@ pub fn keys_query(c: &mut Criterion) {
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&uuid, response).await.unwrap() })
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::new_with_default_store(
|
||||
&alice_id(),
|
||||
&alice_device_id(),
|
||||
dir.path(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
let store = Arc::new(SledCryptoStore::open_with_passphrase(dir.path(), None).unwrap());
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.mark_request_as_sent(&uuid, response).await.unwrap() })
|
||||
.iter(|| async { machine.mark_request_as_sent(&txn_id, response).await.unwrap() })
|
||||
});
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
pub fn keys_claiming(c: &mut Criterion) {
|
||||
let runtime = Arc::new(Builder::new_multi_thread().build().expect("Can't create runtime"));
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
let keys_query_response = keys_query_response();
|
||||
let uuid = Uuid::new_v4();
|
||||
let txn_id = TransactionId::new();
|
||||
|
||||
let response = keys_claim_response();
|
||||
|
||||
@@ -107,14 +101,14 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response))
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, runtime.clone())
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime)| {
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, response)).unwrap()
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(machine.mark_request_as_sent(txn_id, response)).unwrap()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
@@ -124,21 +118,19 @@ pub fn keys_claiming(c: &mut Criterion) {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store =
|
||||
Arc::new(SledCryptoStore::open_with_passphrase(dir.path(), None).unwrap());
|
||||
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::new_with_default_store(
|
||||
&alice_id(),
|
||||
&alice_device_id(),
|
||||
dir.path(),
|
||||
None,
|
||||
))
|
||||
.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store))
|
||||
.unwrap();
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response))
|
||||
.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, runtime.clone())
|
||||
(machine, &runtime, &txn_id)
|
||||
},
|
||||
move |(machine, runtime)| {
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, response)).unwrap()
|
||||
move |(machine, runtime, txn_id)| {
|
||||
runtime.block_on(machine.mark_request_as_sent(txn_id, response)).unwrap()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
@@ -151,18 +143,18 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
let keys_query_response = keys_query_response();
|
||||
let uuid = Uuid::new_v4();
|
||||
let txn_id = TransactionId::new();
|
||||
let response = keys_claim_response();
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let to_device_response = ToDeviceResponse::new();
|
||||
let users: Vec<UserId> = keys_query_response.device_keys.keys().cloned().collect();
|
||||
let users: Vec<OwnedUserId> = keys_query_response.device_keys.keys().cloned().collect();
|
||||
|
||||
let count = response.one_time_keys.values().fold(0, |acc, d| acc + d.len());
|
||||
|
||||
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
let mut group = c.benchmark_group("Room key sharing");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
@@ -171,7 +163,11 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_group_session(&room_id, users.iter(), EncryptionSettings::default())
|
||||
.share_room_key(
|
||||
room_id,
|
||||
users.iter().map(Deref::deref),
|
||||
EncryptionSettings::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -181,26 +177,25 @@ 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.invalidate_group_session(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::new_with_default_store(
|
||||
&alice_id(),
|
||||
&alice_device_id(),
|
||||
dir.path(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
|
||||
let store = Arc::new(SledCryptoStore::open_with_passphrase(dir.path(), None).unwrap());
|
||||
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &keys_query_response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sled store", &name), |b| {
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
let requests = machine
|
||||
.share_group_session(&room_id, users.iter(), EncryptionSettings::default())
|
||||
.share_room_key(
|
||||
room_id,
|
||||
users.iter().map(Deref::deref),
|
||||
EncryptionSettings::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -210,7 +205,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.invalidate_group_session(room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -220,10 +215,10 @@ pub fn room_key_sharing(c: &mut Criterion) {
|
||||
pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
|
||||
|
||||
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
|
||||
let response = huge_keys_query_resopnse();
|
||||
let uuid = Uuid::new_v4();
|
||||
let users: Vec<UserId> = response.device_keys.keys().cloned().collect();
|
||||
let machine = runtime.block_on(OlmMachine::new(alice_id(), alice_device_id()));
|
||||
let response = huge_keys_query_response();
|
||||
let txn_id = TransactionId::new();
|
||||
let users: Vec<OwnedUserId> = response.device_keys.keys().cloned().collect();
|
||||
|
||||
let count = response.device_keys.values().fold(0, |acc, d| acc + d.len());
|
||||
|
||||
@@ -232,29 +227,26 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
let name = format!("{} devices", count);
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
|
||||
b.to_async(&runtime).iter_with_large_drop(|| async {
|
||||
machine.get_missing_sessions(users.iter()).await.unwrap()
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::new_with_default_store(
|
||||
&alice_id(),
|
||||
&alice_device_id(),
|
||||
dir.path(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
let store = Arc::new(SledCryptoStore::open_with_passphrase(dir.path(), None).unwrap());
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
|
||||
let machine =
|
||||
runtime.block_on(OlmMachine::with_store(alice_id(), alice_device_id(), store)).unwrap();
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&txn_id, &response)).unwrap();
|
||||
|
||||
group.bench_function(BenchmarkId::new("sled store", &name), |b| {
|
||||
b.to_async(&runtime)
|
||||
.iter(|| async { machine.get_missing_sessions(users.iter()).await.unwrap() })
|
||||
b.to_async(&runtime).iter(|| async {
|
||||
machine.get_missing_sessions(users.iter().map(Deref::deref)).await.unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
group.finish()
|
||||
@@ -262,7 +254,10 @@ pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(perf::FlamegraphProfiler::new(100));
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 55;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
181AA19B27B52AB40005F102 /* MatrixSDKFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 181AA19A27B52AB40005F102 /* MatrixSDKFFI.xcframework */; };
|
||||
181AA19C27B52AB40005F102 /* MatrixSDKFFI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 181AA19A27B52AB40005F102 /* MatrixSDKFFI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
189A89BA27B40BBF0048B0A5 /* sdk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189A89B927B40BBF0048B0A5 /* sdk.swift */; };
|
||||
18CE89D827B2939900CA89E1 /* MatrixRustSDKApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18CE89D727B2939900CA89E1 /* MatrixRustSDKApp.swift */; };
|
||||
18CE89DA27B2939900CA89E1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18CE89D927B2939900CA89E1 /* ContentView.swift */; };
|
||||
18CE89DC27B2939A00CA89E1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18CE89DB27B2939A00CA89E1 /* Assets.xcassets */; };
|
||||
18CE89E927B2939A00CA89E1 /* MatrixRustSDKTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18CE89E827B2939A00CA89E1 /* MatrixRustSDKTests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
18CE89E527B2939A00CA89E1 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 18CE89CC27B2939900CA89E1 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 18CE89D327B2939900CA89E1;
|
||||
remoteInfo = MatrixRustSDK;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
18CE8A1F27B2941600CA89E1 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
181AA19C27B52AB40005F102 /* MatrixSDKFFI.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
181AA19927B52AA60005F102 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
181AA19A27B52AB40005F102 /* MatrixSDKFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MatrixSDKFFI.xcframework; path = ../../generated/MatrixSDKFFI.xcframework; sourceTree = "<group>"; };
|
||||
189A89B927B40BBF0048B0A5 /* sdk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = sdk.swift; path = ../../../generated/swift/sdk.swift; sourceTree = "<group>"; };
|
||||
189A89C327B417CA0048B0A5 /* MatrixRustSDK-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MatrixRustSDK-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
18CE89D427B2939900CA89E1 /* MatrixRustSDK.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatrixRustSDK.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
18CE89D727B2939900CA89E1 /* MatrixRustSDKApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixRustSDKApp.swift; sourceTree = "<group>"; };
|
||||
18CE89D927B2939900CA89E1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
18CE89DB27B2939A00CA89E1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
18CE89E427B2939A00CA89E1 /* MatrixRustSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatrixRustSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
18CE89E827B2939A00CA89E1 /* MatrixRustSDKTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixRustSDKTests.swift; sourceTree = "<group>"; };
|
||||
18CE8A0127B293A900CA89E1 /* MatrixRustSDK.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MatrixRustSDK.entitlements; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
18CE89D127B2939900CA89E1 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
181AA19B27B52AB40005F102 /* MatrixSDKFFI.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
18CE89E127B2939A00CA89E1 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
189A89AB27B2E16B0048B0A5 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
181AA19A27B52AB40005F102 /* MatrixSDKFFI.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
189A89B827B40BB10048B0A5 /* Generated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
189A89B927B40BBF0048B0A5 /* sdk.swift */,
|
||||
);
|
||||
name = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18CE89CB27B2939900CA89E1 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18CE89D627B2939900CA89E1 /* MatrixRustSDK */,
|
||||
18CE89E727B2939A00CA89E1 /* MatrixRustSDKTests */,
|
||||
18CE89D527B2939900CA89E1 /* Products */,
|
||||
189A89AB27B2E16B0048B0A5 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18CE89D527B2939900CA89E1 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18CE89D427B2939900CA89E1 /* MatrixRustSDK.app */,
|
||||
18CE89E427B2939A00CA89E1 /* MatrixRustSDKTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18CE89D627B2939900CA89E1 /* MatrixRustSDK */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
181AA19927B52AA60005F102 /* Info.plist */,
|
||||
189A89B827B40BB10048B0A5 /* Generated */,
|
||||
18CE89D727B2939900CA89E1 /* MatrixRustSDKApp.swift */,
|
||||
18CE89D927B2939900CA89E1 /* ContentView.swift */,
|
||||
18CE8A0127B293A900CA89E1 /* MatrixRustSDK.entitlements */,
|
||||
18CE89DB27B2939A00CA89E1 /* Assets.xcassets */,
|
||||
189A89C327B417CA0048B0A5 /* MatrixRustSDK-Bridging-Header.h */,
|
||||
);
|
||||
path = MatrixRustSDK;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18CE89E727B2939A00CA89E1 /* MatrixRustSDKTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18CE89E827B2939A00CA89E1 /* MatrixRustSDKTests.swift */,
|
||||
);
|
||||
path = MatrixRustSDKTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
18CE89D327B2939900CA89E1 /* MatrixRustSDK */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 18CE89F827B2939A00CA89E1 /* Build configuration list for PBXNativeTarget "MatrixRustSDK" */;
|
||||
buildPhases = (
|
||||
18CE89D027B2939900CA89E1 /* Sources */,
|
||||
18CE89D127B2939900CA89E1 /* Frameworks */,
|
||||
18CE89D227B2939900CA89E1 /* Resources */,
|
||||
18CE8A1F27B2941600CA89E1 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = MatrixRustSDK;
|
||||
productName = MatrixRustSDK;
|
||||
productReference = 18CE89D427B2939900CA89E1 /* MatrixRustSDK.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
18CE89E327B2939A00CA89E1 /* MatrixRustSDKTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 18CE89FB27B2939A00CA89E1 /* Build configuration list for PBXNativeTarget "MatrixRustSDKTests" */;
|
||||
buildPhases = (
|
||||
18CE89E027B2939A00CA89E1 /* Sources */,
|
||||
18CE89E127B2939A00CA89E1 /* Frameworks */,
|
||||
18CE89E227B2939A00CA89E1 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
18CE89E627B2939A00CA89E1 /* PBXTargetDependency */,
|
||||
);
|
||||
name = MatrixRustSDKTests;
|
||||
productName = MatrixRustSDKTests;
|
||||
productReference = 18CE89E427B2939A00CA89E1 /* MatrixRustSDKTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
18CE89CC27B2939900CA89E1 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1320;
|
||||
LastUpgradeCheck = 1320;
|
||||
TargetAttributes = {
|
||||
18CE89D327B2939900CA89E1 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
LastSwiftMigration = 1320;
|
||||
};
|
||||
18CE89E327B2939A00CA89E1 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
TestTargetID = 18CE89D327B2939900CA89E1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 18CE89CF27B2939900CA89E1 /* Build configuration list for PBXProject "MatrixRustSDK" */;
|
||||
compatibilityVersion = "Xcode 13.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 18CE89CB27B2939900CA89E1;
|
||||
productRefGroup = 18CE89D527B2939900CA89E1 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
18CE89D327B2939900CA89E1 /* MatrixRustSDK */,
|
||||
18CE89E327B2939A00CA89E1 /* MatrixRustSDKTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
18CE89D227B2939900CA89E1 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
18CE89DC27B2939A00CA89E1 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
18CE89E227B2939A00CA89E1 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
18CE89D027B2939900CA89E1 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
18CE89DA27B2939900CA89E1 /* ContentView.swift in Sources */,
|
||||
189A89BA27B40BBF0048B0A5 /* sdk.swift in Sources */,
|
||||
18CE89D827B2939900CA89E1 /* MatrixRustSDKApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
18CE89E027B2939A00CA89E1 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
18CE89E927B2939A00CA89E1 /* MatrixRustSDKTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
18CE89E627B2939A00CA89E1 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 18CE89D327B2939900CA89E1 /* MatrixRustSDK */;
|
||||
targetProxy = 18CE89E527B2939A00CA89E1 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
18CE89F627B2939A00CA89E1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
18CE89F727B2939A00CA89E1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
18CE89F927B2939A00CA89E1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = MatrixRustSDK/MatrixRustSDK.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MatrixRustSDK/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.matrix.MatrixRustSDK;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "MatrixRustSDK/MatrixRustSDK-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
18CE89FA27B2939A00CA89E1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = MatrixRustSDK/MatrixRustSDK.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MatrixRustSDK/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.matrix.MatrixRustSDK;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "MatrixRustSDK/MatrixRustSDK-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
18CE89FC27B2939A00CA89E1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.matrix.MatrixRustSDKTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatrixRustSDK.app/MatrixRustSDK";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
18CE89FD27B2939A00CA89E1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.matrix.MatrixRustSDKTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatrixRustSDK.app/MatrixRustSDK";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
18CE89CF27B2939900CA89E1 /* Build configuration list for PBXProject "MatrixRustSDK" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
18CE89F627B2939A00CA89E1 /* Debug */,
|
||||
18CE89F727B2939A00CA89E1 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
18CE89F827B2939A00CA89E1 /* Build configuration list for PBXNativeTarget "MatrixRustSDK" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
18CE89F927B2939A00CA89E1 /* Debug */,
|
||||
18CE89FA27B2939A00CA89E1 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
18CE89FB27B2939A00CA89E1 /* Build configuration list for PBXNativeTarget "MatrixRustSDKTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
18CE89FC27B2939A00CA89E1 /* Debug */,
|
||||
18CE89FD27B2939A00CA89E1 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 18CE89CC27B2939900CA89E1 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "18CE89D327B2939900CA89E1"
|
||||
BuildableName = "MatrixRustSDK.app"
|
||||
BlueprintName = "MatrixRustSDK"
|
||||
ReferencedContainer = "container:MatrixRustSDK.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "18CE89E327B2939A00CA89E1"
|
||||
BuildableName = "MatrixRustSDKTests.xctest"
|
||||
BlueprintName = "MatrixRustSDKTests"
|
||||
ReferencedContainer = "container:MatrixRustSDK.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "18CE89ED27B2939A00CA89E1"
|
||||
BuildableName = "MatrixRustSDKUITests.xctest"
|
||||
BlueprintName = "MatrixRustSDKUITests"
|
||||
ReferencedContainer = "container:MatrixRustSDK.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "18CE89D327B2939900CA89E1"
|
||||
BuildableName = "MatrixRustSDK.app"
|
||||
BlueprintName = "MatrixRustSDK"
|
||||
ReferencedContainer = "container:MatrixRustSDK.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "18CE89D327B2939900CA89E1"
|
||||
BuildableName = "MatrixRustSDK.app"
|
||||
BlueprintName = "MatrixRustSDK"
|
||||
ReferencedContainer = "container:MatrixRustSDK.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// MatrixRustSDK
|
||||
//
|
||||
// Created by Stefan Ceriu on 08.02.2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello, Rust!")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "sdkFFI.h"
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// MatrixRustSDKApp.swift
|
||||
// MatrixRustSDK
|
||||
//
|
||||
// Created by Stefan Ceriu on 08.02.2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct MatrixRustSDKApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// MatrixRustSDKTests.swift
|
||||
// MatrixRustSDKTests
|
||||
//
|
||||
// Created by Stefan Ceriu on 08.02.2022.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import MatrixRustSDK
|
||||
|
||||
class MatrixRustSDKTests: XCTestCase {
|
||||
|
||||
static var client: Client!
|
||||
|
||||
override class func setUp() {
|
||||
client = try! guestClient(basePath: basePath, homeserver: "https://matrix.org")
|
||||
}
|
||||
|
||||
func testClientProperties() {
|
||||
XCTAssertTrue(Self.client.isGuest())
|
||||
|
||||
XCTAssertNotNil(try? Self.client.restoreToken())
|
||||
XCTAssertNotNil(try? Self.client.deviceId())
|
||||
XCTAssertNotNil(try? Self.client.displayName())
|
||||
}
|
||||
|
||||
func testReadOnlyFileSystemError() {
|
||||
do {
|
||||
let _ = try loginNewClient(basePath: "", username: "test", password: "test")
|
||||
} catch ClientError.Generic(let message) {
|
||||
XCTAssertNotNil(message.range(of: "Read-only file system"))
|
||||
} catch {
|
||||
XCTFail("Not expecting any other kind of exception")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
static private var basePath: String {
|
||||
guard let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
fatalError("Should always be able to retrieve the caches directory")
|
||||
}
|
||||
|
||||
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
return url.path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# Apple platforms support
|
||||
|
||||
This project and build script demonstrate how to create an XCFramework that can be imported into an Xcode project and run on Apple platforms.
|
||||
|
||||
## Building the universal framework
|
||||
|
||||
```
|
||||
sh build_xcframework.sh
|
||||
```
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
* the Rust toolchain
|
||||
* UniFFI - `cargo install uniffi_bindge`
|
||||
* Apple targets (e.g. `rustup target add aarch64-apple-ios`)
|
||||
* `xcodebuild` command line tool from [Apple](https://developer.apple.com/library/archive/technotes/tn2339/_index.html)
|
||||
* `lipo` for creating the fat static libs
|
||||
|
||||
|
||||
The `build_xcframework.sh` script will go through all the steps required to generate a fully usable `.xcframework`:
|
||||
|
||||
1. compile `matrix-sdk-ffi` libraries for iOS, the iOS simulator, MacOS, and Mac Catalyst under `/target`. Some targets are not part of the standard library and they will be built using the nightly toolchain.
|
||||
* `lipo` together the libraries for the same platform under `/generated`
|
||||
* run `uniffi` and generate the C header, module map and swift files
|
||||
* `xcodebuild` an `xcframework` from the fat static libs and the original iOS one, and add the header and module map to it under `generated/MatrixSDKFFI.xcframework`
|
||||
* cleanup and delete the generated files except the .xcframework and the swift sources (that aren't part of the framework)
|
||||
|
||||
## Running the Xcode project
|
||||
|
||||
The Xcode project is meant to provide a simple example on how to integrate everything together but also a place to run unit and integration tests from.
|
||||
|
||||
It's pre-configured to link to the generated .xcframework and .swift files so successfully running the script first is necessary for it to compile.
|
||||
|
||||
It makes the compiled code available to swift by importing the C header through its bridging header.
|
||||
|
||||
Once all the generated components are available running it should be as easy as choosing a platform and clicking run.
|
||||
|
||||
## Distribution
|
||||
The generated framework and Swift code can be distributed and integrated directly but in order to make things simpler we bundle them together as a Swift package available [TBD](here)
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eEu
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Path to the repo root
|
||||
SRC_ROOT=../..
|
||||
|
||||
TARGET_DIR="${SRC_ROOT}/target"
|
||||
|
||||
GENERATED_DIR="${SRC_ROOT}/generated"
|
||||
mkdir -p ${GENERATED_DIR}
|
||||
|
||||
REL_FLAG="--release"
|
||||
REL_TYPE_DIR="release"
|
||||
|
||||
# Build static libs for all the different architectures
|
||||
|
||||
# iOS
|
||||
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios"
|
||||
|
||||
# MacOS
|
||||
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-darwin"
|
||||
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-darwin"
|
||||
|
||||
# iOS Simulator
|
||||
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
cargo build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
|
||||
# Mac Catalyst
|
||||
cargo +nightly build -Z build-std -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios-macabi"
|
||||
cargo +nightly build -Z build-std -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-ios-macabi"
|
||||
|
||||
# Lipo together the libraries for the same platform
|
||||
|
||||
# MacOS
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-darwin/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a"
|
||||
|
||||
# iOS Simulator
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"
|
||||
|
||||
# Mac Catalyst
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios-macabi/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-macabi/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_maccatalyst.a"
|
||||
|
||||
|
||||
# Generate uniffi files
|
||||
uniffi-bindgen generate "${SRC_ROOT}/crates/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR}
|
||||
|
||||
# Move them to the right place
|
||||
HEADERS_DIR=${GENERATED_DIR}/headers
|
||||
mkdir -p ${HEADERS_DIR}
|
||||
|
||||
mv ${GENERATED_DIR}/*.h ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}
|
||||
|
||||
SWIFT_DIR="${GENERATED_DIR}/swift"
|
||||
mkdir -p ${SWIFT_DIR}
|
||||
|
||||
mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
|
||||
|
||||
# Build the xcframework
|
||||
|
||||
if [ -d "${GENERATED_DIR}/MatrixSDKFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKFFI.xcframework"; fi
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_maccatalyst.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-library "${TARGET_DIR}/aarch64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKFFI.xcframework"
|
||||
|
||||
# Cleanup
|
||||
|
||||
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_macos.a"; fi
|
||||
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"; fi
|
||||
if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_maccatalyst.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_maccatalyst.a"; fi
|
||||
if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eEu
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
IS_CI=false
|
||||
|
||||
if [ $# -eq 1 ]; then
|
||||
IS_CI=true
|
||||
echo "Running CI build"
|
||||
else
|
||||
echo "Running debug build"
|
||||
fi
|
||||
|
||||
# Path to the repo root
|
||||
SRC_ROOT=../..
|
||||
|
||||
TARGET_DIR="${SRC_ROOT}/target"
|
||||
|
||||
GENERATED_DIR="${SRC_ROOT}/generated"
|
||||
mkdir -p ${GENERATED_DIR}
|
||||
|
||||
REL_FLAG=""
|
||||
REL_TYPE_DIR="debug"
|
||||
|
||||
# iOS Simulator
|
||||
cargo +nightly build -p matrix-sdk-ffi ${REL_FLAG} --target "aarch64-apple-ios-sim"
|
||||
cargo +nightly build -p matrix-sdk-ffi ${REL_FLAG} --target "x86_64-apple-ios"
|
||||
|
||||
lipo -create \
|
||||
"${TARGET_DIR}/x86_64-apple-ios/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
"${TARGET_DIR}/aarch64-apple-ios-sim/${REL_TYPE_DIR}/libmatrix_sdk_ffi.a" \
|
||||
-output "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"
|
||||
|
||||
# Generate uniffi files
|
||||
uniffi-bindgen generate "${SRC_ROOT}/crates/matrix-sdk-ffi/src/api.udl" --language swift --out-dir ${GENERATED_DIR}
|
||||
|
||||
# Move them to the right place
|
||||
HEADERS_DIR=${GENERATED_DIR}/headers
|
||||
mkdir -p ${HEADERS_DIR}
|
||||
|
||||
mv ${GENERATED_DIR}/*.h ${GENERATED_DIR}/*.modulemap ${HEADERS_DIR}
|
||||
|
||||
SWIFT_DIR="${GENERATED_DIR}/swift"
|
||||
mkdir -p ${SWIFT_DIR}
|
||||
|
||||
mv ${GENERATED_DIR}/*.swift ${SWIFT_DIR}
|
||||
|
||||
# Build the xcframework
|
||||
|
||||
if [ -d "${GENERATED_DIR}/MatrixSDKFFI.xcframework" ]; then rm -rf "${GENERATED_DIR}/MatrixSDKFFI.xcframework"; fi
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" \
|
||||
-headers ${HEADERS_DIR} \
|
||||
-output "${GENERATED_DIR}/MatrixSDKFFI.xcframework"
|
||||
|
||||
# Cleanup
|
||||
|
||||
# if [ -f "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a" ]; then rm -rf "${GENERATED_DIR}/libmatrix_sdk_ffi_iossimulator.a"; fi
|
||||
# if [ -d ${HEADERS_DIR} ]; then rm -rf ${HEADERS_DIR}; fi
|
||||
|
||||
if [ "$IS_CI" = false ] ; then
|
||||
echo "Preparing matrix-rust-components-swift"
|
||||
|
||||
# Debug -> Copy generated files over to ../../../matrix-rust-components-swift
|
||||
echo "$(echo "import MatrixSDKFFIWrapper\n"; cat "${SWIFT_DIR}/sdk.swift")" > "${SWIFT_DIR}/sdk.swift"
|
||||
|
||||
rsync -a --delete "${GENERATED_DIR}/MatrixSDKFFI.xcframework" "${SRC_ROOT}/../matrix-rust-components-swift/"
|
||||
rsync -a --delete "${GENERATED_DIR}/swift/" "${SRC_ROOT}/../matrix-rust-components-swift/Sources/MatrixRustSDK"
|
||||
fi
|
||||
@@ -0,0 +1,44 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# by default, we only care about test coverage of the main
|
||||
# rust crates
|
||||
target: auto
|
||||
threshold: 1%
|
||||
paths:
|
||||
- "crates/matrix-sdk/"
|
||||
- "crates/matrix-sdk-appservice/"
|
||||
- "crates/matrix-sdk-base/"
|
||||
- "crates/matrix-sdk-common/"
|
||||
- "crates/matrix-sdk-crypto/"
|
||||
- "crates/matrix-sdk-qrcode/"
|
||||
- "crates/matrix-sdk-sled/"
|
||||
- "crates/matrix-sdk-store-encryption/"
|
||||
# Coverage of wasm tests isn't supported at the moment,
|
||||
# see rustwasm/wasm-bindgen#2276
|
||||
# - "crates/matrix-sdk-indexeddb"
|
||||
bindings:
|
||||
# Coverage of binding tests is recorded but for informational
|
||||
# purposes only
|
||||
informational: true
|
||||
paths:
|
||||
- "bindings/"
|
||||
- "crates/matrix-sdk-crypto-ffi/"
|
||||
- "crates/matrix-sdk-ffi/"
|
||||
labs:
|
||||
# Coverage of lab tests is recorded but for informational
|
||||
# purposes only
|
||||
informational: true
|
||||
paths:
|
||||
- "labs/"
|
||||
patch: off
|
||||
ignore:
|
||||
- "crates/matrix-sdk-crypto-ffi"
|
||||
- "crates/matrix-sdk-crypto-js"
|
||||
- "crates/matrix-sdk-crypto-nodejs"
|
||||
- "crates/matrix-sdk-ffi"
|
||||
- "crates/matrix-sdk-test"
|
||||
- "crates/matrix-sdk-test-macros"
|
||||
- "labs"
|
||||
- "xtask"
|
||||
@@ -0,0 +1,15 @@
|
||||
.PHONY: default clean png pdf
|
||||
|
||||
default: model.png model.pdf
|
||||
|
||||
clean:
|
||||
rm -f model.png model.pdf
|
||||
|
||||
pdf: model.pdf
|
||||
png: model.png
|
||||
|
||||
model.pdf: model.dot
|
||||
./setdotlabelwidth 45 <model.dot | dot -Tpdf -o model.pdf
|
||||
|
||||
model.png: model.dot
|
||||
./setdotlabelwidth 45 <model.dot | dot -Tpng -o model.png
|
||||
@@ -0,0 +1,11 @@
|
||||
This models the room key sharing algorithm as a decision tree and provides
|
||||
tooling to render it as a PDF or PNG.
|
||||
|
||||
# Usage
|
||||
|
||||
make # Render the decision tree both as a PDF and PNG
|
||||
|
||||
make pdf # Renders the decision tree as a PDF
|
||||
make png # Renders the decision tree as a PNG
|
||||
|
||||
make clean # Remove rendered artifacts
|
||||
@@ -0,0 +1,45 @@
|
||||
digraph {
|
||||
label="Matrix room key sharing algorithm"
|
||||
fontname="Fira Sans"
|
||||
ratio=0.5
|
||||
|
||||
node [shape=box, colorscheme=paired6, style=filled, fillcolor=white, fontname="Fira Sans"]
|
||||
edge [fontname="Fira Sans"]
|
||||
|
||||
/* Non-end states, additional checks needed. */
|
||||
verified_device_check [label="START\n\nIs this our own, verified device?", labelfontname="Fira Sans"]
|
||||
outbound_session_check [label="Outbound session exists?"]
|
||||
outbound_exists [label="Session previously previously_shared with this user ID/device ID pair?"]
|
||||
previously_shared [label="Requesting device sender (Curve25519) key matches the key we originally shared with?"]
|
||||
own_device_check [label="Is this our own device?"]
|
||||
|
||||
/* End states */
|
||||
allow_verified [label="Share the entire session from the earliest known index.\n\nOk(None)", color=4, fillcolor=3]
|
||||
allow_limited [label="Share a limited session starting from index i, which is the index we previously shared at.\n\nOk(Some(i))", color=4, fillcolor=3]
|
||||
refuse_device_key_changed [label="Sender key changed, refuse to share.\n\nErr(KeyForwardDecision::ChangedSenderKey)", color=6, fillcolor=5]
|
||||
refuse_not_shared [label="Session was never shared with this device, refuse to share.\n\nErr(KeyForwardDecision::OutboundSessionNotShared)", color=6, fillcolor=5]
|
||||
refuse_untrusted_own_device [label="Our own device, but it is untrusted and we haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::UntrustedDevice)", color=6, fillcolor=5]
|
||||
refuse_missing_outbound_session [label="Not our device and haven't previously shared with it. Refuse to share.\n\nErr(KeyForwardDecision::MissingOutboundSession)", color=6, fillcolor=5]
|
||||
|
||||
/* Checks */
|
||||
|
||||
/* Is this our own verified device? */
|
||||
verified_device_check -> allow_verified [label="Yes"]
|
||||
verified_device_check -> outbound_session_check [label="No"]
|
||||
|
||||
/* Does the outbound session exist? */
|
||||
outbound_session_check -> outbound_exists [label="Yes"]
|
||||
outbound_session_check -> own_device_check [label="No"]
|
||||
|
||||
/* Previously shared? */
|
||||
outbound_exists -> previously_shared [label="Yes"]
|
||||
outbound_exists -> refuse_not_shared [label="No"]
|
||||
|
||||
/* Requesting device sender key matches key it was shared with? */
|
||||
previously_shared -> allow_limited [label="Yes"]
|
||||
previously_shared -> refuse_device_key_changed [label="No"]
|
||||
|
||||
/* Is this our own device? */
|
||||
own_device_check -> refuse_untrusted_own_device [label="Yes"]
|
||||
own_device_check -> refuse_missing_outbound_session [label="No"]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/perl
|
||||
#
|
||||
# Adapted from https://stackoverflow.com/a/68057031
|
||||
|
||||
use strict;
|
||||
|
||||
my $usage = "setdotlabelwidth [char-width] < [dotfile]";
|
||||
my $width = shift() or die("Usage: $usage $!");
|
||||
|
||||
while(<STDIN>)
|
||||
{
|
||||
if (m/label="(.*?)"/)
|
||||
{
|
||||
my $labeltext = $1;
|
||||
my @words = split(/ +|(?=\\n)/, $labeltext);
|
||||
my @newtext = ();
|
||||
my $newline = "";
|
||||
foreach my $word(@words)
|
||||
{
|
||||
if (length($newline) > 0 and
|
||||
length($newline) + length($word) > $width)
|
||||
{
|
||||
push(@newtext, $newline);
|
||||
$newline = "";
|
||||
}
|
||||
|
||||
$newline .= " " if (length($newline) > 0);
|
||||
$newline .= $word;
|
||||
}
|
||||
|
||||
push(@newtext, $newline) if (length($newline) > 0);
|
||||
my $newlabel = join("\\n", @newtext);
|
||||
s/label=".*?"/label="$newlabel"/;
|
||||
}
|
||||
|
||||
print;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
authors = ["Johannes Becker <j.becker@famedly.com>"]
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
description = "Appservice SDK based on the matrix-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio", "appservice"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-appservice"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.60"
|
||||
|
||||
[features]
|
||||
default = ["native-tls"]
|
||||
|
||||
anyhow = ["matrix-sdk/anyhow"]
|
||||
e2e-encryption = [
|
||||
"matrix-sdk/e2e-encryption"
|
||||
]
|
||||
eyre = ["matrix-sdk/eyre"]
|
||||
sled = ["matrix-sdk/sled"]
|
||||
|
||||
markdown = ["matrix-sdk/markdown"]
|
||||
native-tls = ["matrix-sdk/native-tls"]
|
||||
rustls-tls = ["matrix-sdk/rustls-tls"]
|
||||
socks = ["matrix-sdk/socks"]
|
||||
sso-login = ["matrix-sdk/sso-login"]
|
||||
|
||||
docs = []
|
||||
|
||||
[dependencies]
|
||||
dashmap = "5.2.0"
|
||||
http = "0.2.6"
|
||||
matrix-sdk = { version = "0.5.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] }
|
||||
percent-encoding = "2.1.0"
|
||||
regex = "1.5.5"
|
||||
ruma = { version = "0.6.1", features = ["client-api-c", "appservice-api-s"] }
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
serde_yaml = "0.8.23"
|
||||
tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread"] }
|
||||
thiserror = "1.0.30"
|
||||
tracing = "0.1.34"
|
||||
url = "2.2.2"
|
||||
warp = { version = "0.3.2", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
matrix-sdk-test = { version = "0.5.0", path = "../matrix-sdk-test", features = ["appservice"] }
|
||||
mockito = "0.31.0"
|
||||
tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = "0.3.11"
|
||||
@@ -0,0 +1,78 @@
|
||||
use std::env;
|
||||
|
||||
use matrix_sdk_appservice::{
|
||||
matrix_sdk::{
|
||||
event_handler::Ctx,
|
||||
room::Room,
|
||||
ruma::{
|
||||
events::room::member::{MembershipState, OriginalSyncRoomMemberEvent},
|
||||
UserId,
|
||||
},
|
||||
HttpError,
|
||||
},
|
||||
AppService, AppServiceRegistration, Result,
|
||||
};
|
||||
use ruma::api::{
|
||||
client::{error::ErrorKind, uiaa::UiaaResponse},
|
||||
error::{FromHttpResponseError, ServerError},
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
pub async fn handle_room_member(
|
||||
appservice: AppService,
|
||||
room: Room,
|
||||
event: OriginalSyncRoomMemberEvent,
|
||||
) -> Result<()> {
|
||||
if !appservice.user_id_is_in_namespace(&event.state_key) {
|
||||
trace!("not an appservice user: {}", event.state_key);
|
||||
} else if let MembershipState::Invite = event.content.membership {
|
||||
let user_id = UserId::parse(event.state_key.as_str())?;
|
||||
if let Err(error) = appservice.register_virtual_user(user_id.localpart()).await {
|
||||
error_if_user_not_in_use(error)?;
|
||||
}
|
||||
|
||||
let client = appservice.virtual_user_client(user_id.localpart()).await?;
|
||||
client.join_room_by_id(room.room_id()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn error_if_user_not_in_use(error: matrix_sdk_appservice::Error) -> Result<()> {
|
||||
match error {
|
||||
// If user is already in use that's OK.
|
||||
matrix_sdk_appservice::Error::Matrix(matrix_sdk::Error::Http(HttpError::UiaaError(
|
||||
FromHttpResponseError::Server(ServerError::Known(UiaaResponse::MatrixError(error))),
|
||||
))) if matches!(error.kind, ErrorKind::UserInUse) => Ok(()),
|
||||
// In all other cases return with an error.
|
||||
error => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env::set_var("RUST_LOG", "matrix_sdk=debug,matrix_sdk_appservice=debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let homeserver_url = "http://localhost:8008";
|
||||
let server_name = "localhost";
|
||||
let registration = AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?;
|
||||
|
||||
let appservice = AppService::new(homeserver_url, server_name, registration).await?;
|
||||
appservice.register_user_query(Box::new(|_, _| Box::pin(async { true }))).await;
|
||||
appservice
|
||||
.register_event_handler_context(appservice.clone())?
|
||||
.register_event_handler(
|
||||
move |event: OriginalSyncRoomMemberEvent,
|
||||
room: Room,
|
||||
Ctx(appservice): Ctx<AppService>| {
|
||||
handle_room_member(appservice, room, event)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (host, port) = appservice.registration().get_host_and_port()?;
|
||||
appservice.run(host, port).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::api::client::uiaa::UiaaInfo;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -34,11 +35,17 @@ pub enum Error {
|
||||
#[error("could not convert host:port to socket addr")]
|
||||
HostPortToSocketAddrs,
|
||||
|
||||
#[error("uri has empty path")]
|
||||
UriEmptyPath,
|
||||
|
||||
#[error("uri path is unknown")]
|
||||
UriPathUnknown,
|
||||
|
||||
#[error(transparent)]
|
||||
HttpRequest(#[from] ruma::api::error::FromHttpRequestError),
|
||||
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] ruma::identifiers::Error),
|
||||
Identifier(#[from] ruma::IdParseError),
|
||||
|
||||
#[error(transparent)]
|
||||
Http(#[from] http::Error),
|
||||
@@ -67,28 +74,49 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
#[error(transparent)]
|
||||
Utf8Error(#[from] std::str::Utf8Error),
|
||||
|
||||
#[error("warp rejection: {0}")]
|
||||
WarpRejection(String),
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
#[error(transparent)]
|
||||
Actix(#[from] actix_web::Error),
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
#[error(transparent)]
|
||||
ActixPayload(#[from] actix_web::error::PayloadError),
|
||||
}
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
impl actix_web::error::ResponseError for Error {}
|
||||
impl Error {
|
||||
/// Try to destructure the error into an universal interactive auth info.
|
||||
///
|
||||
/// Some requests require universal interactive auth, doing such a request
|
||||
/// will always fail the first time with a 401 status code, the response
|
||||
/// body will contain info how the client can authenticate.
|
||||
///
|
||||
/// The request will need to be retried, this time containing additional
|
||||
/// authentication data.
|
||||
///
|
||||
/// This method is an convenience method to get to the info the server
|
||||
/// returned on the first, failed request.
|
||||
pub fn uiaa_response(&self) -> Option<&UiaaInfo> {
|
||||
match self {
|
||||
Error::Matrix(matrix) => matrix.uiaa_response(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
impl warp::reject::Reject for Error {}
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
impl From<warp::Rejection> for Error {
|
||||
fn from(rejection: warp::Rejection) -> Self {
|
||||
Self::WarpRejection(format!("{:?}", rejection))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::HttpError> for Error {
|
||||
fn from(e: matrix_sdk::HttpError) -> Self {
|
||||
matrix_sdk::Error::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::StoreError> for Error {
|
||||
fn from(e: matrix_sdk::StoreError) -> Self {
|
||||
matrix_sdk::Error::from(e).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
use matrix_sdk::locks::Mutex;
|
||||
|
||||
use crate::{
|
||||
ruma::api::appservice::query::{
|
||||
query_room_alias::v1 as query_room, query_user_id::v1 as query_user,
|
||||
},
|
||||
AppService,
|
||||
};
|
||||
|
||||
pub(crate) type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
pub(crate) type AppserviceFn<A, R> =
|
||||
Box<dyn FnMut(AppService, A) -> BoxFuture<'static, R> + Send + Sync + 'static>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct EventHandler {
|
||||
pub users: Arc<Mutex<Option<AppserviceFn<query_user::IncomingRequest, bool>>>>,
|
||||
pub rooms: Arc<Mutex<Option<AppserviceFn<query_room::IncomingRequest, bool>>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EventHandler {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut debug = f.debug_struct("EventHandler");
|
||||
match self.users.try_lock() {
|
||||
Ok(lock) => debug.field("users", &lock.is_some()),
|
||||
Err(_) => debug.field("users", &format_args!("<locked>")),
|
||||
};
|
||||
match self.rooms.try_lock() {
|
||||
Ok(lock) => debug.field("rooms", &lock.is_some()),
|
||||
Err(_) => debug.field("rooms", &format_args!("<locked>")),
|
||||
};
|
||||
debug.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,868 @@
|
||||
// Copyright 2021 Famedly GmbH
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! Matrix [Application Service] library
|
||||
//!
|
||||
//! The appservice crate aims to provide a batteries-included experience by
|
||||
//! being a thin wrapper around the [`matrix_sdk`]. That means that we
|
||||
//!
|
||||
//! * ship with functionality to configure your webserver crate or simply run
|
||||
//! the webserver for you
|
||||
//! * receive and validate requests from the homeserver correctly
|
||||
//! * allow calling the homeserver with proper virtual user identity assertion
|
||||
//! * have consistent room state by leveraging matrix-sdk's state store
|
||||
//! * provide E2EE support by leveraging matrix-sdk's crypto store
|
||||
//!
|
||||
//! # Status
|
||||
//!
|
||||
//! The crate is in an experimental state. Follow
|
||||
//! [matrix-org/matrix-rust-sdk#228] for progress.
|
||||
//!
|
||||
//! # Registration
|
||||
//!
|
||||
//! The crate relies on the registration being always in sync with the actual
|
||||
//! registration used by the homeserver. That's because it's required for the
|
||||
//! access tokens and because membership states for virtual users are determined
|
||||
//! based on the registered namespace.
|
||||
//!
|
||||
//! **Note:** Non-exclusive registration namespaces are not yet supported and
|
||||
//! hence might lead to undefined behavior.
|
||||
//!
|
||||
//! # Quickstart
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # async {
|
||||
//! #
|
||||
//! use matrix_sdk_appservice::{
|
||||
//! ruma::events::room::member::SyncRoomMemberEvent,
|
||||
//! AppService, AppServiceRegistration
|
||||
//! };
|
||||
//!
|
||||
//! let homeserver_url = "http://127.0.0.1:8008";
|
||||
//! let server_name = "localhost";
|
||||
//! let registration = AppServiceRegistration::try_from_yaml_str(
|
||||
//! r"
|
||||
//! id: appservice
|
||||
//! url: http://127.0.0.1:9009
|
||||
//! as_token: as_token
|
||||
//! hs_token: hs_token
|
||||
//! sender_localpart: _appservice
|
||||
//! namespaces:
|
||||
//! users:
|
||||
//! - exclusive: true
|
||||
//! regex: '@_appservice_.*'
|
||||
//! ")?;
|
||||
//!
|
||||
//! let mut appservice = AppService::new(homeserver_url, server_name, registration).await?;
|
||||
//! appservice.register_event_handler(|_ev: SyncRoomMemberEvent| async {
|
||||
//! // do stuff
|
||||
//! });
|
||||
//!
|
||||
//! let (host, port) = appservice.registration().get_host_and_port()?;
|
||||
//! appservice.run(host, port).await?;
|
||||
//! #
|
||||
//! # Ok::<(), Box<dyn std::error::Error + 'static>>(())
|
||||
//! # };
|
||||
//! ```
|
||||
//!
|
||||
//! Check the [examples directory] for fully working examples.
|
||||
//!
|
||||
//! [Application Service]: https://matrix.org/docs/spec/application_service/r0.1.2
|
||||
//! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228
|
||||
//! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/main/crates/matrix-sdk-appservice/examples
|
||||
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fs::File,
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
pub use error::Error;
|
||||
use event_handler::AppserviceFn;
|
||||
use http::Uri;
|
||||
pub use matrix_sdk;
|
||||
#[doc(no_inline)]
|
||||
pub use matrix_sdk::ruma;
|
||||
use matrix_sdk::{
|
||||
bytes::Bytes,
|
||||
config::RequestConfig,
|
||||
event_handler::{EventHandler, EventHandlerResult, SyncEvent},
|
||||
reqwest::Url,
|
||||
Client, ClientBuildError, ClientBuilder, Session,
|
||||
};
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
api::{
|
||||
appservice::{
|
||||
event::push_events,
|
||||
query::{query_room_alias::v1 as query_room, query_user_id::v1 as query_user},
|
||||
Registration,
|
||||
},
|
||||
client::{account::register, session::login, sync::sync_events, uiaa::UserIdentifier},
|
||||
},
|
||||
assign,
|
||||
events::{room::member::MembershipState, AnyRoomEvent, AnyStateEvent},
|
||||
DeviceId, IdParseError, OwnedDeviceId, OwnedRoomId, OwnedServerName, UserId,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
mod error;
|
||||
pub mod event_handler;
|
||||
mod webserver;
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
pub type Host = String;
|
||||
pub type Port = u16;
|
||||
|
||||
const USER_KEY: &[u8] = b"appservice.users.";
|
||||
pub const USER_MEMBER: &[u8] = b"appservice.users.membership.";
|
||||
|
||||
/// Builder for a virtual user
|
||||
#[derive(Debug)]
|
||||
pub struct VirtualUserBuilder<'a> {
|
||||
appservice: &'a AppService,
|
||||
localpart: &'a str,
|
||||
device_id: Option<OwnedDeviceId>,
|
||||
client_builder: ClientBuilder,
|
||||
log_in: bool,
|
||||
restored_session: Option<Session>,
|
||||
}
|
||||
|
||||
impl<'a> VirtualUserBuilder<'a> {
|
||||
/// Create a new virtual user builder
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `localpart` - The localpart of the virtual user
|
||||
pub fn new(appservice: &'a AppService, localpart: &'a str) -> Self {
|
||||
Self {
|
||||
appservice,
|
||||
localpart,
|
||||
device_id: None,
|
||||
client_builder: Client::builder(),
|
||||
log_in: false,
|
||||
restored_session: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the device id of the virtual user
|
||||
pub fn device_id(mut self, device_id: Option<OwnedDeviceId>) -> Self {
|
||||
self.device_id = device_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the client builder to use for the virtual user
|
||||
pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self {
|
||||
self.client_builder = client_builder;
|
||||
self
|
||||
}
|
||||
|
||||
/// Log in as the virtual user
|
||||
///
|
||||
/// In some cases it is necessary to log in as the virtual user, such as to
|
||||
/// upload device keys
|
||||
pub fn login(mut self) -> Self {
|
||||
self.log_in = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Restore a persisted session
|
||||
///
|
||||
/// This is primarily useful if you enable
|
||||
/// [`VirtualUserBuilder::login()`] and want to restore a session
|
||||
/// from a previous run.
|
||||
pub fn restored_session(mut self, session: Session) -> Self {
|
||||
self.restored_session = Some(session);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the virtual user
|
||||
///
|
||||
/// # Errors
|
||||
/// This function returns an error if an invalid localpart is provided.
|
||||
pub async fn build(self) -> Result<Client> {
|
||||
if let Some(client) = self.appservice.clients.get(self.localpart) {
|
||||
return Ok(client.clone());
|
||||
}
|
||||
|
||||
let user_id = UserId::parse_with_server_name(self.localpart, &self.appservice.server_name)?;
|
||||
|
||||
let mut builder = self.client_builder;
|
||||
|
||||
if !self.log_in && self.localpart != self.appservice.registration.sender_localpart {
|
||||
builder = builder.assert_identity();
|
||||
}
|
||||
|
||||
let client = builder
|
||||
.homeserver_url(self.appservice.homeserver_url.clone())
|
||||
.appservice_mode()
|
||||
.build()
|
||||
.await
|
||||
.map_err(ClientBuildError::assert_valid_builder_args)?;
|
||||
|
||||
let session = if let Some(session) = self.restored_session {
|
||||
session
|
||||
} else if self.log_in && self.localpart != self.appservice.registration.sender_localpart {
|
||||
self.appservice
|
||||
.create_session(self.localpart, self.device_id.as_ref().map(|v| v.as_ref()), None)
|
||||
.await?
|
||||
} else {
|
||||
// Don’t log in
|
||||
Session {
|
||||
access_token: self.appservice.registration.as_token.clone(),
|
||||
user_id: user_id.clone(),
|
||||
device_id: self.device_id.unwrap_or_else(DeviceId::new),
|
||||
}
|
||||
};
|
||||
|
||||
client.restore_login(session).await?;
|
||||
|
||||
self.appservice.clients.insert(self.localpart.to_owned(), client.clone());
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
/// AppService Registration
|
||||
///
|
||||
/// Wrapper around [`Registration`]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppServiceRegistration {
|
||||
inner: Registration,
|
||||
}
|
||||
|
||||
impl AppServiceRegistration {
|
||||
/// Try to load registration from yaml string
|
||||
///
|
||||
/// See the fields of [`Registration`] for the required format
|
||||
pub fn try_from_yaml_str(value: impl AsRef<str>) -> Result<Self> {
|
||||
Ok(Self { inner: serde_yaml::from_str(value.as_ref())? })
|
||||
}
|
||||
|
||||
/// Try to load registration from yaml file
|
||||
///
|
||||
/// See the fields of [`Registration`] for the required format
|
||||
pub fn try_from_yaml_file(path: impl Into<PathBuf>) -> Result<Self> {
|
||||
let file = File::open(path.into())?;
|
||||
|
||||
Ok(Self { inner: serde_yaml::from_reader(file)? })
|
||||
}
|
||||
|
||||
/// Get the host and port from the registration URL
|
||||
///
|
||||
/// If no port is found it falls back to scheme defaults: 80 for http and
|
||||
/// 443 for https
|
||||
pub fn get_host_and_port(&self) -> Result<(Host, Port)> {
|
||||
let uri = Uri::try_from(&self.inner.url)?;
|
||||
|
||||
let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned();
|
||||
let port = match uri.port() {
|
||||
Some(port) => Ok(port.as_u16()),
|
||||
None => match uri.scheme_str() {
|
||||
Some("http") => Ok(80),
|
||||
Some("https") => Ok(443),
|
||||
_ => Err(Error::MissingRegistrationPort),
|
||||
},
|
||||
}?;
|
||||
|
||||
Ok((host, port))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Registration> for AppServiceRegistration {
|
||||
fn from(value: Registration) -> Self {
|
||||
Self { inner: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AppServiceRegistration {
|
||||
type Target = Registration;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache data for the registration namespaces.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NamespaceCache {
|
||||
/// List of user regexes in our namespace
|
||||
users: Vec<Regex>,
|
||||
/// List of alias regexes in our namespace
|
||||
#[allow(dead_code)]
|
||||
aliases: Vec<Regex>,
|
||||
/// List of room id regexes in our namespace
|
||||
#[allow(dead_code)]
|
||||
rooms: Vec<Regex>,
|
||||
}
|
||||
|
||||
impl NamespaceCache {
|
||||
/// Creates a new registration cache from a [`Registration`] value
|
||||
pub fn from_registration(registration: &Registration) -> Result<Self> {
|
||||
let users = registration
|
||||
.namespaces
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| Regex::new(&user.regex))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let aliases = registration
|
||||
.namespaces
|
||||
.aliases
|
||||
.iter()
|
||||
.map(|user| Regex::new(&user.regex))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let rooms = registration
|
||||
.namespaces
|
||||
.rooms
|
||||
.iter()
|
||||
.map(|user| Regex::new(&user.regex))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(NamespaceCache { users, aliases, rooms })
|
||||
}
|
||||
}
|
||||
|
||||
type Localpart = String;
|
||||
|
||||
/// The `localpart` of the user associated with the application service via
|
||||
/// `sender_localpart` in [`AppServiceRegistration`].
|
||||
///
|
||||
/// Dummy type for shared documentation
|
||||
#[allow(dead_code)]
|
||||
pub type MainUser = ();
|
||||
|
||||
/// The application service may specify the virtual user to act as through use
|
||||
/// of a user_id query string parameter on the request. The user specified in
|
||||
/// the query string must be covered by one of the [`AppServiceRegistration`]'s
|
||||
/// `users` namespaces.
|
||||
///
|
||||
/// Dummy type for shared documentation
|
||||
pub type VirtualUser = ();
|
||||
|
||||
/// AppService
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppService {
|
||||
homeserver_url: Url,
|
||||
server_name: OwnedServerName,
|
||||
registration: Arc<AppServiceRegistration>,
|
||||
namespaces: Arc<NamespaceCache>,
|
||||
clients: Arc<DashMap<Localpart, Client>>,
|
||||
event_handler: event_handler::EventHandler,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
/// Create new AppService
|
||||
///
|
||||
/// Also creates and caches a [`Client`] for the [`MainUser`].
|
||||
/// A default [`ClientBuilder`] is used, if you want to customize it
|
||||
/// use [`with_client_builder()`][Self::with_client_builder] instead.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `homeserver_url` - The homeserver that the client should connect to.
|
||||
/// * `server_name` - The server name to use when constructing user ids from
|
||||
/// the localpart.
|
||||
/// * `registration` - The [AppService Registration] to use when interacting
|
||||
/// with the homeserver.
|
||||
///
|
||||
/// [AppService Registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
|
||||
pub async fn new(
|
||||
homeserver_url: impl TryInto<Url, Error = url::ParseError>,
|
||||
server_name: impl TryInto<OwnedServerName, Error = IdParseError>,
|
||||
registration: AppServiceRegistration,
|
||||
) -> Result<Self> {
|
||||
let appservice =
|
||||
Self::with_client_builder(homeserver_url, server_name, registration, Client::builder())
|
||||
.await?;
|
||||
|
||||
Ok(appservice)
|
||||
}
|
||||
|
||||
/// Same as [`new()`][Self::new] but lets you provide a [`ClientBuilder`]
|
||||
/// for the [`Client`]
|
||||
pub async fn with_client_builder(
|
||||
homeserver_url: impl TryInto<Url, Error = url::ParseError>,
|
||||
server_name: impl TryInto<OwnedServerName, Error = IdParseError>,
|
||||
registration: AppServiceRegistration,
|
||||
builder: ClientBuilder,
|
||||
) -> Result<Self> {
|
||||
let homeserver_url = homeserver_url.try_into()?;
|
||||
let server_name = server_name.try_into()?;
|
||||
let registration = Arc::new(registration);
|
||||
let namespaces = Arc::new(NamespaceCache::from_registration(®istration)?);
|
||||
let clients = Arc::new(DashMap::new());
|
||||
let sender_localpart = registration.sender_localpart.clone();
|
||||
let event_handler = event_handler::EventHandler::default();
|
||||
|
||||
let appservice = AppService {
|
||||
homeserver_url,
|
||||
server_name,
|
||||
registration,
|
||||
namespaces,
|
||||
clients,
|
||||
event_handler,
|
||||
};
|
||||
|
||||
// we create and cache the [`MainUser`] by default
|
||||
appservice.virtual_user_builder(&sender_localpart).client_builder(builder).build().await?;
|
||||
|
||||
Ok(appservice)
|
||||
}
|
||||
|
||||
/// Create a [`Client`] for the given [`VirtualUser`]'s `localpart`
|
||||
///
|
||||
/// Will create and return a [`Client`] that's configured to [assert the
|
||||
/// identity] on all outgoing homeserver requests if `localpart` is
|
||||
/// given.
|
||||
///
|
||||
/// This method is a singleton that saves the client internally for re-use
|
||||
/// based on the `localpart`. The cached [`Client`] can be retrieved either
|
||||
/// by calling this method again or by calling
|
||||
/// [`get_cached_client()`][Self::get_cached_client] which is non-async
|
||||
/// convenience wrapper.
|
||||
///
|
||||
/// Note that if you want to do actions like joining rooms with a virtual
|
||||
/// user it needs to be registered first. `Self::register_virtual_user()`
|
||||
/// can be used for that purpose.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `localpart` - The localpart of the user we want assert our identity to
|
||||
///
|
||||
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
|
||||
/// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion
|
||||
pub async fn virtual_user_client(&self, localpart: impl AsRef<str>) -> Result<Client> {
|
||||
self.virtual_user_builder(localpart.as_ref()).build().await
|
||||
}
|
||||
|
||||
/// Same as [`virtual_user_client()`][Self::virtual_user_client] but with
|
||||
/// the ability to pass in a [`ClientBuilder`]
|
||||
///
|
||||
/// Since this method is a singleton follow-up calls with different
|
||||
/// [`ClientBuilder`]s will be ignored.
|
||||
pub async fn virtual_user_client_with_client_builder(
|
||||
&self,
|
||||
localpart: impl AsRef<str>,
|
||||
builder: ClientBuilder,
|
||||
) -> Result<Client> {
|
||||
self.virtual_user_builder(localpart.as_ref()).client_builder(builder).build().await
|
||||
}
|
||||
|
||||
pub fn virtual_user_builder<'a>(&'a self, localpart: &'a str) -> VirtualUserBuilder<'a> {
|
||||
VirtualUserBuilder::new(self, localpart)
|
||||
}
|
||||
|
||||
/// Create a session using appservice login for a virtual user.
|
||||
async fn create_session(
|
||||
&self,
|
||||
user: impl AsRef<str>,
|
||||
device_id: Option<&str>,
|
||||
initial_device_display_name: Option<&str>,
|
||||
) -> Result<Session> {
|
||||
let homeserver = self.homeserver_url.clone();
|
||||
info!(homeserver = homeserver.as_str(), user = user.as_ref(), "Logging in as virtual user");
|
||||
|
||||
let login_info = login::v3::LoginInfo::ApplicationService(
|
||||
login::v3::ApplicationService::new(UserIdentifier::UserIdOrLocalpart(user.as_ref())),
|
||||
);
|
||||
|
||||
let request = assign!(login::v3::Request::new(login_info), {
|
||||
device_id: device_id.map(|d| d.into()),
|
||||
initial_device_display_name
|
||||
});
|
||||
|
||||
let response = self
|
||||
.get_cached_client(None)?
|
||||
.send(request, Some(RequestConfig::short_retry().force_auth()))
|
||||
.await?;
|
||||
|
||||
Ok(Session {
|
||||
access_token: response.access_token,
|
||||
user_id: response.user_id,
|
||||
device_id: response.device_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get cached [`Client`]
|
||||
///
|
||||
/// Will return the client for the given `localpart` if previously
|
||||
/// constructed with [`virtual_user_client()`][Self::virtual_user_client] or
|
||||
/// [`virtual_user_client_with_config()`][Self::
|
||||
/// virtual_user_client_with_client_builder].
|
||||
///
|
||||
/// If no `localpart` is given it assumes the [`MainUser`]'s `localpart`. If
|
||||
/// no client for `localpart` is found it will return an Error.
|
||||
pub fn get_cached_client(&self, localpart: Option<&str>) -> Result<Client> {
|
||||
let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref());
|
||||
|
||||
let entry = self.clients.get(localpart).ok_or(Error::NoClientForLocalpart)?;
|
||||
|
||||
Ok(entry.value().clone())
|
||||
}
|
||||
|
||||
/// Convenience wrapper around [`Client::register_event_handler()`] that
|
||||
/// attaches the event handler to the [`MainUser`]'s [`Client`]
|
||||
///
|
||||
/// Note that the event handler in the [`AppService`] context only triggers
|
||||
/// [`join` room `timeline` events], so no state events or events from the
|
||||
/// `invite`, `knock` or `leave` scope. The rationale behind that is
|
||||
/// that incoming AppService transactions from the homeserver are not
|
||||
/// necessarily bound to a specific user but can cover a multitude of
|
||||
/// namespaces, and as such the AppService basically only "observes
|
||||
/// joined rooms". Also currently homeservers only push PDUs to appservices,
|
||||
/// no EDUs. There's the open [MSC2409] regarding supporting EDUs in the
|
||||
/// future, though it seems to be planned to put EDUs into a different
|
||||
/// JSON key than `events` to stay backwards compatible.
|
||||
///
|
||||
/// [`join` room `timeline` events]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientr0sync
|
||||
/// [MSC2409]: https://github.com/matrix-org/matrix-doc/pull/2409
|
||||
pub async fn register_event_handler<Ev, Ctx, H>(&self, handler: H) -> Result<&Self>
|
||||
where
|
||||
Ev: SyncEvent + DeserializeOwned + Send + 'static,
|
||||
H: EventHandler<Ev, Ctx>,
|
||||
<H::Future as Future>::Output: EventHandlerResult,
|
||||
{
|
||||
let client = self.get_cached_client(None)?;
|
||||
client.register_event_handler(handler).await;
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Convenience wrapper around [`Client::register_event_handler_context`]
|
||||
/// attaches the event handler context to the [`MainUser`]'s [`Client`].
|
||||
pub fn register_event_handler_context<T>(&self, ctx: T) -> Result<&Self>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let client = self.get_cached_client(None)?;
|
||||
client.register_event_handler_context(ctx);
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Register a responder for queries about the existence of a user with a
|
||||
/// given mxid.
|
||||
///
|
||||
/// See [GET /_matrix/app/v1/users/{userId}](https://matrix.org/docs/spec/application_service/r0.1.2#get-matrix-app-v1-users-userid).
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_appservice::AppService;
|
||||
/// # fn run(appservice: AppService) {
|
||||
/// appservice.register_user_query(Box::new(|appservice, req| Box::pin(async move {
|
||||
/// println!("Got request for {}", req.user_id);
|
||||
/// true
|
||||
/// })));
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn register_user_query(
|
||||
&self,
|
||||
handler: AppserviceFn<query_user::IncomingRequest, bool>,
|
||||
) {
|
||||
*self.event_handler.users.lock().await = Some(handler);
|
||||
}
|
||||
|
||||
/// Register a responder for queries about the existence of a room with the
|
||||
/// given alias.
|
||||
///
|
||||
/// See [GET /_matrix/app/v1/rooms/{roomAlias}](https://matrix.org/docs/spec/application_service/r0.1.2#get-matrix-app-v1-rooms-roomalias).
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_appservice::AppService;
|
||||
/// # fn run(appservice: AppService) {
|
||||
/// appservice.register_room_query(Box::new(|appservice, req| Box::pin(async move {
|
||||
/// println!("Got request for {}", req.room_alias);
|
||||
/// true
|
||||
/// })));
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn register_room_query(
|
||||
&self,
|
||||
handler: AppserviceFn<query_room::IncomingRequest, bool>,
|
||||
) {
|
||||
*self.event_handler.rooms.lock().await = Some(handler);
|
||||
}
|
||||
|
||||
/// Register a virtual user by sending a [`register::v3::Request`] to the
|
||||
/// homeserver
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `localpart` - The localpart of the user to register. Must be covered
|
||||
/// by the namespaces in the [`Registration`] in order to succeed.
|
||||
///
|
||||
/// # Returns
|
||||
/// This function may return a UIAA response, which should be checked for
|
||||
/// with [`Error::uiaa_response()`].
|
||||
pub async fn register_virtual_user(&self, localpart: impl AsRef<str>) -> Result<()> {
|
||||
if self.is_user_registered(localpart.as_ref()).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let request = assign!(register::v3::Request::new(), {
|
||||
username: Some(localpart.as_ref()),
|
||||
login_type: Some(®ister::LoginType::ApplicationService),
|
||||
});
|
||||
|
||||
let client = self.get_cached_client(None)?;
|
||||
client.register(request).await?;
|
||||
self.set_user_registered(localpart.as_ref()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add the given localpart to the database of registered localparts.
|
||||
async fn set_user_registered(&self, localpart: impl AsRef<str>) -> Result<()> {
|
||||
let client = self.get_cached_client(None)?;
|
||||
client
|
||||
.store()
|
||||
.set_custom_value(
|
||||
&[USER_KEY, localpart.as_ref().as_bytes()].concat(),
|
||||
vec![u8::from(true)],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get whether a localpart is listed in the database as registered.
|
||||
async fn is_user_registered(&self, localpart: impl AsRef<str>) -> Result<bool> {
|
||||
let client = self.get_cached_client(None)?;
|
||||
let key = [USER_KEY, localpart.as_ref().as_bytes()].concat();
|
||||
let store = client.store().get_custom_value(&key).await?;
|
||||
let registered =
|
||||
store.and_then(|vec| vec.first().copied()).map_or(false, |b| b == u8::from(true));
|
||||
Ok(registered)
|
||||
}
|
||||
|
||||
/// Get the AppService [registration]
|
||||
///
|
||||
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
|
||||
pub fn registration(&self) -> &AppServiceRegistration {
|
||||
&self.registration
|
||||
}
|
||||
|
||||
/// Compare the given `hs_token` against `registration.hs_token`
|
||||
///
|
||||
/// Returns `true` if the tokens match, `false` otherwise.
|
||||
pub fn compare_hs_token(&self, hs_token: impl AsRef<str>) -> bool {
|
||||
self.registration.hs_token == hs_token.as_ref()
|
||||
}
|
||||
|
||||
/// Check if given `user_id` is in any of the [`AppServiceRegistration`]'s
|
||||
/// `users` namespaces
|
||||
pub fn user_id_is_in_namespace(&self, user_id: impl AsRef<str>) -> bool {
|
||||
for regex in &self.namespaces.users {
|
||||
if regex.is_match(user_id.as_ref()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns a [`warp::Filter`] to be used as [`warp::serve()`] route
|
||||
///
|
||||
/// Note that if you handle any of the [application-service-specific
|
||||
/// routes], including the legacy routes, you will break the appservice
|
||||
/// functionality.
|
||||
///
|
||||
/// [application-service-specific routes]: https://spec.matrix.org/unstable/application-service-api/#legacy-routes
|
||||
pub fn warp_filter(&self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
|
||||
webserver::warp_filter(self.clone())
|
||||
}
|
||||
|
||||
/// Receive an incoming [transaction], pushing the contained events to
|
||||
/// active virtual clients.
|
||||
///
|
||||
/// [transaction]: https://spec.matrix.org/v1.2/application-service-api/#put_matrixappv1transactionstxnid
|
||||
pub async fn receive_transaction(
|
||||
&self,
|
||||
transaction: push_events::v1::IncomingRequest,
|
||||
) -> Result<()> {
|
||||
let client = self.get_cached_client(None)?;
|
||||
|
||||
// Find membership events affecting members in our namespace, and update
|
||||
// membership accordingly
|
||||
for event in transaction.events.iter() {
|
||||
let event = match event.deserialize() {
|
||||
Ok(AnyRoomEvent::State(AnyStateEvent::RoomMember(event))) => event,
|
||||
_ => continue,
|
||||
};
|
||||
if !self.user_id_is_in_namespace(event.state_key()) {
|
||||
continue;
|
||||
}
|
||||
let localpart = event.state_key().localpart();
|
||||
client
|
||||
.store()
|
||||
.set_custom_value(
|
||||
&[USER_MEMBER, event.room_id().as_bytes(), b".", localpart.as_bytes()].concat(),
|
||||
event.membership().to_string().into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
/// Helper type for extracting the room id for an event
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EventRoomId {
|
||||
room_id: Option<OwnedRoomId>,
|
||||
}
|
||||
|
||||
// Spawn a task for each client that constructs and pushes a sync event
|
||||
let mut tasks: Vec<JoinHandle<_>> = Vec::new();
|
||||
let transaction = Arc::new(transaction);
|
||||
for virt_client in self.clients.iter() {
|
||||
let client = client.clone();
|
||||
let virt_client = virt_client.clone();
|
||||
let transaction = transaction.clone();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let user_id = match virt_client.user_id() {
|
||||
Some(user_id) => user_id.localpart(),
|
||||
// Unauthenticated client. (should that be possible?)
|
||||
None => return Ok(()),
|
||||
};
|
||||
let mut response = sync_events::v3::Response::new(transaction.txn_id.to_string());
|
||||
|
||||
// Clients expect events to be grouped per room, where the group also denotes
|
||||
// what the client's membership of the given room is. We take the
|
||||
// all the events in the transaction and sort them into appropriate
|
||||
// groups, falling back to a membership of "join" if it's unknown.
|
||||
for raw_event in &transaction.events {
|
||||
let room_id = match raw_event.deserialize_as::<EventRoomId>()?.room_id {
|
||||
Some(room_id) => room_id,
|
||||
None => {
|
||||
warn!("Transaction contained event with no ID");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let key = &[USER_MEMBER, room_id.as_bytes(), b".", user_id.as_bytes()].concat();
|
||||
let membership = match client.store().get_custom_value(key).await? {
|
||||
Some(value) => String::from_utf8(value).ok().map(MembershipState::from),
|
||||
None => None,
|
||||
};
|
||||
|
||||
match membership.unwrap_or(MembershipState::Join) {
|
||||
MembershipState::Join => {
|
||||
let room = response.rooms.join.entry(room_id).or_default();
|
||||
room.timeline.events.push(raw_event.clone().cast())
|
||||
}
|
||||
MembershipState::Leave | MembershipState::Ban => {
|
||||
let room = response.rooms.leave.entry(room_id).or_default();
|
||||
room.timeline.events.push(raw_event.clone().cast())
|
||||
}
|
||||
MembershipState::Knock => {
|
||||
response.rooms.knock.entry(room_id).or_default();
|
||||
}
|
||||
MembershipState::Invite => {
|
||||
response.rooms.invite.entry(room_id).or_default();
|
||||
}
|
||||
_ => debug!("Membership type we don't know how to handle"),
|
||||
}
|
||||
}
|
||||
virt_client.receive_transaction(&transaction.txn_id, response).await?;
|
||||
Ok::<_, Error>(())
|
||||
});
|
||||
|
||||
tasks.push(task);
|
||||
}
|
||||
for task in tasks {
|
||||
if let Err(e) = task.await {
|
||||
warn!("Joining sync task failed: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convenience method that runs an http server depending on the selected
|
||||
/// server feature
|
||||
///
|
||||
/// This is a blocking call that tries to listen on the provided host and
|
||||
/// port
|
||||
pub async fn run(&self, host: impl Into<String>, port: impl Into<u16>) -> Result<()> {
|
||||
let host = host.into();
|
||||
let port = port.into();
|
||||
info!("Starting AppService on {}:{}", &host, &port);
|
||||
|
||||
webserver::run_server(self.clone(), host, port).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Ruma always expects the path to start with `/_matrix`, so we transform
|
||||
/// accordingly. Handles [legacy routes] and appservice being located on a sub
|
||||
/// path.
|
||||
///
|
||||
/// [legacy routes]: https://matrix.org/docs/spec/application_service/r0.1.2#legacy-routes
|
||||
// TODO: consider ruma PR
|
||||
pub(crate) fn transform_request_path(
|
||||
mut request: http::Request<Bytes>,
|
||||
) -> Result<http::Request<Bytes>> {
|
||||
let uri = request.uri();
|
||||
// remove trailing slash from path
|
||||
let path = uri.path().trim_end_matches('/').to_owned();
|
||||
|
||||
if !path.starts_with("/_matrix/app/v1/") {
|
||||
let path = match path {
|
||||
// special-case paths without value at the end
|
||||
_ if path.ends_with("/_matrix/app/unstable/thirdparty/user") => {
|
||||
"/_matrix/app/v1/thirdparty/user".to_owned()
|
||||
}
|
||||
_ if path.ends_with("/_matrix/app/unstable/thirdparty/location") => {
|
||||
"/_matrix/app/v1/thirdparty/location".to_owned()
|
||||
}
|
||||
// regular paths with values at the end
|
||||
_ => {
|
||||
let mut path = path.split('/').into_iter().rev();
|
||||
let value = match path.next() {
|
||||
Some(value) => value,
|
||||
None => return Err(Error::UriEmptyPath),
|
||||
};
|
||||
|
||||
let mut path = match path.next() {
|
||||
Some(path_segment)
|
||||
if ["transactions", "users", "rooms"].contains(&path_segment) =>
|
||||
{
|
||||
format!("/_matrix/app/v1/{}/{}", path_segment, value)
|
||||
}
|
||||
Some(path_segment) => match path.next() {
|
||||
Some(path_segment2) if path_segment2 == "thirdparty" => {
|
||||
format!("/_matrix/app/v1/thirdparty/{}/{}", path_segment, value)
|
||||
}
|
||||
_ => return Err(Error::UriPathUnknown),
|
||||
},
|
||||
None => return Err(Error::UriEmptyPath),
|
||||
};
|
||||
|
||||
if let Some(query) = uri.query() {
|
||||
path.push('?');
|
||||
path.push_str(query);
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
};
|
||||
|
||||
let mut parts = uri.clone().into_parts();
|
||||
parts.path_and_query = Some(path.parse()?);
|
||||
|
||||
let uri = parts.try_into().map_err(http::Error::from)?;
|
||||
*request.uri_mut() = uri;
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
+53
-22
@@ -12,10 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{net::ToSocketAddrs, result::Result as StdResult};
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use futures::TryFutureExt;
|
||||
use matrix_sdk::Bytes;
|
||||
use matrix_sdk::{
|
||||
bytes::Bytes,
|
||||
ruma::{
|
||||
self,
|
||||
api::{
|
||||
appservice::query::{
|
||||
query_room_alias::v1 as query_room, query_user_id::v1 as query_user,
|
||||
},
|
||||
IncomingRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use warp::{filters::BoxedFilter, path::FullPath, Filter, Rejection, Reply};
|
||||
|
||||
@@ -95,10 +105,10 @@ mod filters {
|
||||
|
||||
fn common(appservice: AppService) -> BoxedFilter<(AppService, http::Request<Bytes>)> {
|
||||
warp::any()
|
||||
.and(filters::valid_access_token(appservice.registration().hs_token.clone()))
|
||||
.and(valid_access_token(appservice.registration().hs_token.clone()))
|
||||
.map(move || appservice.clone())
|
||||
.and(http_request().and_then(|request| async move {
|
||||
let request = crate::transform_legacy_route(request).map_err(Error::from)?;
|
||||
let request = crate::transform_request_path(request).map_err(Error::from)?;
|
||||
Ok::<http::Request<Bytes>, Rejection>(request)
|
||||
}))
|
||||
.boxed()
|
||||
@@ -110,7 +120,7 @@ mod filters {
|
||||
.and(warp::query::raw())
|
||||
.and_then(|token: String, query: String| async move {
|
||||
let query: Vec<(String, String)> =
|
||||
matrix_sdk::urlencoded::from_str(&query).map_err(Error::from)?;
|
||||
ruma::serde::urlencoded::from_str(&query).map_err(Error::from)?;
|
||||
|
||||
if query.into_iter().any(|(key, value)| key == "access_token" && value == token) {
|
||||
Ok::<(), Rejection>(())
|
||||
@@ -152,35 +162,56 @@ mod filters {
|
||||
}
|
||||
|
||||
mod handlers {
|
||||
use percent_encoding::percent_decode_str;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn user(
|
||||
_user_id: String,
|
||||
_appservice: AppService,
|
||||
_request: http::Request<Bytes>,
|
||||
) -> StdResult<impl warp::Reply, Rejection> {
|
||||
user_id: String,
|
||||
appservice: AppService,
|
||||
request: http::Request<Bytes>,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
if let Some(user_exists) = appservice.event_handler.users.lock().await.as_mut() {
|
||||
let user_id = percent_decode_str(&user_id).decode_utf8().map_err(Error::from)?;
|
||||
let request = query_user::IncomingRequest::try_from_http_request(request, &[user_id])
|
||||
.map_err(Error::from)?;
|
||||
return if user_exists(appservice.clone(), request).await {
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
} else {
|
||||
Err(warp::reject::not_found())
|
||||
};
|
||||
}
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
}
|
||||
|
||||
pub async fn room(
|
||||
_room_id: String,
|
||||
_appservice: AppService,
|
||||
_request: http::Request<Bytes>,
|
||||
) -> StdResult<impl warp::Reply, Rejection> {
|
||||
room_id: String,
|
||||
appservice: AppService,
|
||||
request: http::Request<Bytes>,
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
if let Some(room_exists) = appservice.event_handler.rooms.lock().await.as_mut() {
|
||||
let room_id = percent_decode_str(&room_id).decode_utf8().map_err(Error::from)?;
|
||||
let request = query_room::IncomingRequest::try_from_http_request(request, &[room_id])
|
||||
.map_err(Error::from)?;
|
||||
return if room_exists(appservice.clone(), request).await {
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
} else {
|
||||
Err(warp::reject::not_found())
|
||||
};
|
||||
}
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
}
|
||||
|
||||
pub async fn transaction(
|
||||
_txn_id: String,
|
||||
txn_id: String,
|
||||
appservice: AppService,
|
||||
request: http::Request<Bytes>,
|
||||
) -> StdResult<impl warp::Reply, Rejection> {
|
||||
let incoming_transaction: matrix_sdk::api_appservice::event::push_events::v1::IncomingRequest =
|
||||
matrix_sdk::IncomingRequest::try_from_http_request(request).map_err(Error::from)?;
|
||||
|
||||
let client = appservice.get_cached_client(None)?;
|
||||
client.receive_transaction(incoming_transaction).map_err(Error::from).await?;
|
||||
) -> Result<impl Reply, Rejection> {
|
||||
let incoming_transaction: ruma::api::appservice::event::push_events::v1::IncomingRequest =
|
||||
ruma::api::IncomingRequest::try_from_http_request(request, &[txn_id])
|
||||
.map_err(Error::from)?;
|
||||
|
||||
appservice.receive_transaction(incoming_transaction).await?;
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
}
|
||||
}
|
||||
@@ -196,7 +227,7 @@ struct ErrorMessage {
|
||||
message: String,
|
||||
}
|
||||
|
||||
pub async fn handle_rejection(err: Rejection) -> std::result::Result<impl Reply, Rejection> {
|
||||
pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Rejection> {
|
||||
if err.find::<Unauthorized>().is_some() || err.find::<warp::reject::InvalidQuery>().is_some() {
|
||||
let code = http::StatusCode::UNAUTHORIZED;
|
||||
let message = "UNAUTHORIZED";
|
||||
+71
-121
@@ -1,18 +1,17 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
future,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
use actix_web::{test as actix_test, App as ActixApp, HttpResponse};
|
||||
use matrix_sdk::{
|
||||
api_appservice::Registration,
|
||||
async_trait,
|
||||
events::{room::member::MemberEventContent, SyncStateEvent},
|
||||
room::Room,
|
||||
ClientConfig, EventHandler, RequestConfig,
|
||||
config::RequestConfig,
|
||||
ruma::{api::appservice::Registration, events::room::member::OriginalSyncRoomMemberEvent},
|
||||
Client,
|
||||
};
|
||||
use matrix_sdk_appservice::*;
|
||||
use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson};
|
||||
use ruma::{api::MatrixVersion, room_id};
|
||||
use serde_json::json;
|
||||
#[cfg(feature = "warp")]
|
||||
use warp::{Filter, Reply};
|
||||
|
||||
fn registration_string() -> String {
|
||||
@@ -22,7 +21,7 @@ fn registration_string() -> String {
|
||||
async fn appservice(registration: Option<Registration>) -> Result<AppService> {
|
||||
// env::set_var(
|
||||
// "RUST_LOG",
|
||||
// "mockito=debug,matrix_sdk=debug,ruma=debug,actix_web=debug,warp=debug",
|
||||
// "mockito=debug,matrix_sdk=debug,ruma=debug,warp=debug",
|
||||
// );
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
@@ -34,16 +33,17 @@ async fn appservice(registration: Option<Registration>) -> Result<AppService> {
|
||||
let homeserver_url = mockito::server_url();
|
||||
let server_name = "localhost";
|
||||
|
||||
let client_config =
|
||||
ClientConfig::default().request_config(RequestConfig::default().disable_retry());
|
||||
let client_builder = Client::builder()
|
||||
.request_config(RequestConfig::default().disable_retry())
|
||||
.server_versions([MatrixVersion::V1_0]);
|
||||
|
||||
Ok(AppService::new_with_config(
|
||||
AppService::with_client_builder(
|
||||
homeserver_url.as_ref(),
|
||||
server_name,
|
||||
registration,
|
||||
client_config,
|
||||
client_builder,
|
||||
)
|
||||
.await?)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
@@ -86,7 +86,6 @@ async fn test_put_transaction() -> Result<()> {
|
||||
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
@@ -97,16 +96,6 @@ async fn test_put_transaction() -> Result<()> {
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
let status = {
|
||||
let app =
|
||||
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
|
||||
|
||||
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
|
||||
|
||||
actix_test::call_service(&app, req).await.status()
|
||||
};
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
@@ -115,10 +104,10 @@ async fn test_put_transaction() -> Result<()> {
|
||||
#[async_test]
|
||||
async fn test_get_user() -> Result<()> {
|
||||
let appservice = appservice(None).await?;
|
||||
appservice.register_user_query(Box::new(|_, _| Box::pin(async move { true }))).await;
|
||||
|
||||
let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token";
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
let status = warp::test::request()
|
||||
.method("GET")
|
||||
.path(uri)
|
||||
@@ -128,16 +117,6 @@ async fn test_get_user() -> Result<()> {
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
let status = {
|
||||
let app =
|
||||
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
|
||||
|
||||
let req = actix_test::TestRequest::get().uri(uri).to_request();
|
||||
|
||||
actix_test::call_service(&app, req).await.status()
|
||||
};
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
@@ -146,10 +125,10 @@ async fn test_get_user() -> Result<()> {
|
||||
#[async_test]
|
||||
async fn test_get_room() -> Result<()> {
|
||||
let appservice = appservice(None).await?;
|
||||
appservice.register_room_query(Box::new(|_, _| Box::pin(async move { true }))).await;
|
||||
|
||||
let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token";
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
let status = warp::test::request()
|
||||
.method("GET")
|
||||
.path(uri)
|
||||
@@ -159,16 +138,6 @@ async fn test_get_room() -> Result<()> {
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
let status = {
|
||||
let app =
|
||||
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
|
||||
|
||||
let req = actix_test::TestRequest::get().uri(uri).to_request();
|
||||
|
||||
actix_test::call_service(&app, req).await.status()
|
||||
};
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
@@ -184,7 +153,6 @@ async fn test_invalid_access_token() -> Result<()> {
|
||||
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
@@ -195,16 +163,6 @@ async fn test_invalid_access_token() -> Result<()> {
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
let status = {
|
||||
let app =
|
||||
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
|
||||
|
||||
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
|
||||
|
||||
actix_test::call_service(&app, req).await.status()
|
||||
};
|
||||
|
||||
assert_eq!(status, 401);
|
||||
|
||||
Ok(())
|
||||
@@ -220,7 +178,6 @@ async fn test_no_access_token() -> Result<()> {
|
||||
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
{
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
@@ -235,49 +192,24 @@ async fn test_no_access_token() -> Result<()> {
|
||||
assert_eq!(status, 401);
|
||||
}
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
{
|
||||
let app =
|
||||
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
|
||||
|
||||
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
|
||||
|
||||
let resp = actix_test::call_service(&app, req).await;
|
||||
|
||||
// TODO: this should actually return a 401 but is 500 because something in the
|
||||
// extractor fails
|
||||
assert_eq!(resp.status(), 500);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_event_handler() -> Result<()> {
|
||||
let mut appservice = appservice(None).await?;
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Example {
|
||||
pub on_state_member: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
pub fn new() -> Self {
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
Self { on_state_member: Arc::new(Mutex::new(false)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Example {
|
||||
async fn on_room_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {
|
||||
let on_state_member = self.on_state_member.clone();
|
||||
*on_state_member.lock().unwrap() = true;
|
||||
}
|
||||
}
|
||||
|
||||
let example = Example::new();
|
||||
appservice.set_event_handler(Box::new(example.clone())).await?;
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
let on_state_member = Arc::new(Mutex::new(false));
|
||||
appservice
|
||||
.register_event_handler({
|
||||
let on_state_member = on_state_member.clone();
|
||||
move |_ev: OriginalSyncRoomMemberEvent| {
|
||||
*on_state_member.lock().unwrap() = true;
|
||||
future::ready(())
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
@@ -285,7 +217,6 @@ async fn test_event_handler() -> Result<()> {
|
||||
transaction_builder.add_room_event(EventsJson::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
@@ -294,17 +225,7 @@ async fn test_event_handler() -> Result<()> {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
{
|
||||
let app =
|
||||
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
|
||||
|
||||
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
|
||||
|
||||
actix_test::call_service(&app, req).await;
|
||||
};
|
||||
|
||||
let on_room_member_called = *example.on_state_member.lock().unwrap();
|
||||
let on_room_member_called = *on_state_member.lock().unwrap();
|
||||
assert!(on_room_member_called);
|
||||
|
||||
Ok(())
|
||||
@@ -314,7 +235,6 @@ async fn test_event_handler() -> Result<()> {
|
||||
async fn test_unrelated_path() -> Result<()> {
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
let status = {
|
||||
let consumer_filter = warp::any()
|
||||
.and(appservice.warp_filter())
|
||||
@@ -330,21 +250,51 @@ async fn test_unrelated_path() -> Result<()> {
|
||||
response.status()
|
||||
};
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
let status = {
|
||||
let app = actix_test::init_service(
|
||||
ActixApp::new()
|
||||
.configure(appservice.actix_configure())
|
||||
.route("/unrelated", actix_web::web::get().to(HttpResponse::Ok)),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, 200);
|
||||
|
||||
let req = actix_test::TestRequest::get().uri("/unrelated").to_request();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
actix_test::call_service(&app, req).await.status()
|
||||
#[async_test]
|
||||
async fn test_appservice_on_sub_path() -> Result<()> {
|
||||
let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
|
||||
let uri_1 = "/sub_path/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
let uri_2 = "/sub_path/_matrix/app/v1/transactions/2?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(EventsJson::Member);
|
||||
let transaction_1 = transaction_builder.build_json_transaction();
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(EventsJson::MemberNameChange);
|
||||
let transaction_2 = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
{
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri_1)
|
||||
.json(&transaction_1)
|
||||
.filter(&warp::path("sub_path").and(appservice.warp_filter()))
|
||||
.await?;
|
||||
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri_2)
|
||||
.json(&transaction_2)
|
||||
.filter(&warp::path("sub_path").and(appservice.warp_filter()))
|
||||
.await?;
|
||||
};
|
||||
|
||||
assert_eq!(status, 200);
|
||||
let members = appservice
|
||||
.get_cached_client(None)?
|
||||
.get_room(room_id)
|
||||
.expect("Expected room to be available")
|
||||
.members_no_sync()
|
||||
.await?;
|
||||
|
||||
assert_eq!(members[0].display_name().unwrap(), "changed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "The base component to build a Matrix client library."
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = "1.60"
|
||||
version = "0.5.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
e2e-encryption = ["matrix-sdk-crypto"]
|
||||
qrcode = ["matrix-sdk-crypto/qrcode"]
|
||||
experimental-timeline = []
|
||||
|
||||
# helpers for testing features build upon this
|
||||
testing = ["http"]
|
||||
|
||||
[dependencies]
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.53"
|
||||
chacha20poly1305 = { version = "0.9.0", optional = true }
|
||||
dashmap = "5.2.0"
|
||||
futures-channel = "0.3.21"
|
||||
futures-core = "0.3.21"
|
||||
futures-util = { version = "0.3.21", default-features = false }
|
||||
hmac = { version = "0.12.1", optional = true }
|
||||
http = { version = "0.2.6", optional = true }
|
||||
lru = "0.7.5"
|
||||
matrix-sdk-common = { version = "0.5.0", path = "../matrix-sdk-common" }
|
||||
matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto", optional = true }
|
||||
once_cell = "1.10.0"
|
||||
pbkdf2 = { version = "0.11.0", default-features = false, optional = true }
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
serde = { version = "1.0.136", features = ["rc"] }
|
||||
serde_json = "1.0.79"
|
||||
sha2 = { version = "0.10.2", optional = true }
|
||||
thiserror = "1.0.30"
|
||||
tracing = "0.1.34"
|
||||
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
ruma = { version = "0.6.1", features = ["client-api-c", "js", "signatures"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
ruma = { version = "0.6.2", features = ["client-api-c", "signatures"] }
|
||||
|
||||
[dev-dependencies]
|
||||
futures = { version = "0.3.21", default-features = false, features = ["executor"] }
|
||||
tracing = { version = "0.1.26", features = ["log"] }
|
||||
http = "0.2.6"
|
||||
assign = "1.1.1"
|
||||
env_logger = "0.9.0"
|
||||
matrix-sdk-test = { version = "0.5.0", path = "../matrix-sdk-test" }
|
||||
tokio = { version = "1.17.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.30"
|
||||
@@ -0,0 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this crate will be documented in this file.
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Bug Fixes
|
||||
- #664: Fix regression with push rules being applied to the own user_id only instead of all but the own user_id
|
||||
|
||||
## 0.5.0
|
||||
@@ -0,0 +1,10 @@
|
||||
This crate implements the base to build a [Matrix](https://matrix.org/) client
|
||||
library.
|
||||
|
||||
## Crate Feature Flags
|
||||
|
||||
The following crate feature flags are available:
|
||||
|
||||
* `encryption`: Enables end-to-end encryption support in the library.
|
||||
* `qrcode`: Enables 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.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
|
||||
use std::io::Error as IoError;
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
|
||||
use serde_json::Error as JsonError;
|
||||
use thiserror::Error;
|
||||
@@ -26,6 +26,7 @@ use thiserror::Error;
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// Internal representation of errors.
|
||||
#[non_exhaustive]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// Queried endpoint requires authentication but was called on an anonymous
|
||||
@@ -33,6 +34,12 @@ pub enum Error {
|
||||
#[error("the queried endpoint requires authentication but was called before logging in")]
|
||||
AuthenticationRequired,
|
||||
|
||||
/// Attempting to restore a session after the olm-machine has already been
|
||||
/// set up fails
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error("The olm machine has already been initialized")]
|
||||
BadCryptoStoreState,
|
||||
|
||||
/// A generic error returned when the state store fails not due to
|
||||
/// IO or (de)serialization.
|
||||
#[error(transparent)]
|
||||
@@ -47,20 +54,17 @@ pub enum Error {
|
||||
IoError(#[from] IoError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
CryptoStore(#[from] CryptoStoreError),
|
||||
|
||||
/// An error occurred during a E2EE operation.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
OlmError(#[from] OlmError),
|
||||
|
||||
/// An error occurred during a E2EE group operation.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
#[error(transparent)]
|
||||
MegolmError(#[from] MegolmError),
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2020 Damir Jelić
|
||||
// Copyright 2020 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.
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
pub use matrix_sdk_common::*;
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
pub use crate::timeline_stream::TimelineStreamError;
|
||||
pub use crate::{
|
||||
error::{Error, Result},
|
||||
session::Session,
|
||||
};
|
||||
|
||||
mod client;
|
||||
mod error;
|
||||
pub mod media;
|
||||
mod rooms;
|
||||
mod session;
|
||||
pub mod store;
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
mod timeline_stream;
|
||||
mod utils;
|
||||
|
||||
pub use client::BaseClient;
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
pub use http;
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use once_cell;
|
||||
pub use rooms::{DisplayName, Room, RoomInfo, RoomMember, RoomType};
|
||||
pub use store::{StateChanges, StateStore, Store, StoreError};
|
||||
pub use utils::{
|
||||
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
|
||||
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
events::{
|
||||
room::{
|
||||
message::{
|
||||
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
|
||||
LocationMessageEventContent, VideoMessageEventContent,
|
||||
},
|
||||
MediaSource,
|
||||
},
|
||||
sticker::StickerEventContent,
|
||||
},
|
||||
UInt,
|
||||
};
|
||||
|
||||
const UNIQUE_SEPARATOR: &str = "_";
|
||||
|
||||
/// A trait to uniquely identify values of the same type.
|
||||
pub trait UniqueKey {
|
||||
/// A string that uniquely identifies `Self` compared to other values of
|
||||
/// the same type.
|
||||
fn unique_key(&self) -> String;
|
||||
}
|
||||
|
||||
/// The requested format of a media file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MediaFormat {
|
||||
/// The file that was uploaded.
|
||||
File,
|
||||
|
||||
/// A thumbnail of the file that was uploaded.
|
||||
Thumbnail(MediaThumbnailSize),
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaFormat {
|
||||
fn unique_key(&self) -> String {
|
||||
match self {
|
||||
Self::File => "file".into(),
|
||||
Self::Thumbnail(size) => size.unique_key(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The requested size of a media thumbnail.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaThumbnailSize {
|
||||
/// The desired resizing method.
|
||||
pub method: Method,
|
||||
|
||||
/// The desired width of the thumbnail. The actual thumbnail may not match
|
||||
/// the size specified.
|
||||
pub width: UInt,
|
||||
|
||||
/// The desired height of the thumbnail. The actual thumbnail may not match
|
||||
/// the size specified.
|
||||
pub height: UInt,
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaThumbnailSize {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{}{}x{}", self.method, UNIQUE_SEPARATOR, self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaSource {
|
||||
fn unique_key(&self) -> String {
|
||||
match self {
|
||||
Self::Plain(uri) => uri.to_string(),
|
||||
Self::Encrypted(file) => file.url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for media data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaRequest {
|
||||
/// The source of the media file.
|
||||
pub source: MediaSource,
|
||||
|
||||
/// The requested format of the media data.
|
||||
pub format: MediaFormat,
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaRequest {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{}{}", self.source.unique_key(), UNIQUE_SEPARATOR, self.format.unique_key())
|
||||
}
|
||||
}
|
||||
/// Trait for media event content.
|
||||
pub trait MediaEventContent {
|
||||
/// Get the source of the file for `Self`.
|
||||
///
|
||||
/// Returns `None` if `Self` has no file.
|
||||
fn source(&self) -> Option<MediaSource>;
|
||||
|
||||
/// Get the source of the thumbnail for `Self`.
|
||||
///
|
||||
/// Returns `None` if `Self` has no thumbnail.
|
||||
fn thumbnail_source(&self) -> Option<MediaSource>;
|
||||
}
|
||||
|
||||
impl MediaEventContent for StickerEventContent {
|
||||
fn source(&self) -> Option<MediaSource> {
|
||||
Some(MediaSource::Plain(self.url.clone()))
|
||||
}
|
||||
|
||||
fn thumbnail_source(&self) -> Option<MediaSource> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for AudioMessageEventContent {
|
||||
fn source(&self) -> Option<MediaSource> {
|
||||
Some(self.source.clone())
|
||||
}
|
||||
|
||||
fn thumbnail_source(&self) -> Option<MediaSource> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for FileMessageEventContent {
|
||||
fn source(&self) -> Option<MediaSource> {
|
||||
Some(self.source.clone())
|
||||
}
|
||||
|
||||
fn thumbnail_source(&self) -> Option<MediaSource> {
|
||||
self.info.as_ref()?.thumbnail_source.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for ImageMessageEventContent {
|
||||
fn source(&self) -> Option<MediaSource> {
|
||||
Some(self.source.clone())
|
||||
}
|
||||
|
||||
fn thumbnail_source(&self) -> Option<MediaSource> {
|
||||
self.info
|
||||
.as_ref()
|
||||
.and_then(|info| info.thumbnail_source.clone())
|
||||
.or_else(|| Some(self.source.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for VideoMessageEventContent {
|
||||
fn source(&self) -> Option<MediaSource> {
|
||||
Some(self.source.clone())
|
||||
}
|
||||
|
||||
fn thumbnail_source(&self) -> Option<MediaSource> {
|
||||
self.info
|
||||
.as_ref()
|
||||
.and_then(|info| info.thumbnail_source.clone())
|
||||
.or_else(|| Some(self.source.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for LocationMessageEventContent {
|
||||
fn source(&self) -> Option<MediaSource> {
|
||||
None
|
||||
}
|
||||
|
||||
fn thumbnail_source(&self) -> Option<MediaSource> {
|
||||
self.info.as_ref()?.thumbnail_source.clone()
|
||||
}
|
||||
}
|
||||
+26
-20
@@ -17,21 +17,24 @@ use std::sync::Arc;
|
||||
use ruma::{
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
room::{member::MemberEventContent, power_levels::PowerLevelsEventContent},
|
||||
SyncStateEvent,
|
||||
room::{member::MembershipState, power_levels::SyncRoomPowerLevelsEvent},
|
||||
},
|
||||
MxcUri, UserId,
|
||||
};
|
||||
|
||||
use crate::deserialized_responses::MemberEvent;
|
||||
use crate::{deserialized_responses::MemberEvent, MinimalRoomMemberEvent};
|
||||
|
||||
/// A member of a room.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoomMember {
|
||||
pub(crate) event: Arc<MemberEvent>,
|
||||
pub(crate) profile: Arc<Option<MemberEventContent>>,
|
||||
// The latest member event sent by the member themselves.
|
||||
// Stored in addition to the latest member event overall to get displayname
|
||||
// and avatar from, which should be ignored on events sent by others.
|
||||
pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) presence: Arc<Option<PresenceEvent>>,
|
||||
pub(crate) power_levels: Arc<Option<SyncStateEvent<PowerLevelsEventContent>>>,
|
||||
pub(crate) power_levels: Arc<Option<SyncRoomPowerLevelsEvent>>,
|
||||
pub(crate) max_power_level: i64,
|
||||
pub(crate) is_room_creator: bool,
|
||||
pub(crate) display_name_ambiguous: bool,
|
||||
@@ -40,15 +43,15 @@ pub struct RoomMember {
|
||||
impl RoomMember {
|
||||
/// Get the unique user id of this member.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.event.state_key
|
||||
self.event.user_id()
|
||||
}
|
||||
|
||||
/// Get the display name of the member if there is one.
|
||||
pub fn display_name(&self) -> Option<&str> {
|
||||
if let Some(p) = self.profile.as_ref() {
|
||||
p.displayname.as_deref()
|
||||
p.as_original().and_then(|e| e.content.displayname.as_deref())
|
||||
} else {
|
||||
self.event.content.displayname.as_deref()
|
||||
self.event.original_content()?.displayname.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,9 +69,10 @@ impl RoomMember {
|
||||
|
||||
/// Get the avatar url of the member, if there is one.
|
||||
pub fn avatar_url(&self) -> Option<&MxcUri> {
|
||||
match self.profile.as_ref() {
|
||||
Some(p) => p.avatar_url.as_ref(),
|
||||
None => self.event.content.avatar_url.as_ref(),
|
||||
if let Some(p) = self.profile.as_ref() {
|
||||
p.as_original().and_then(|e| e.content.avatar_url.as_deref())
|
||||
} else {
|
||||
self.event.original_content()?.avatar_url.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,16 +90,9 @@ impl RoomMember {
|
||||
|
||||
/// Get the power level of this member.
|
||||
pub fn power_level(&self) -> i64 {
|
||||
self.power_levels
|
||||
(*self.power_levels)
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.map(|e| {
|
||||
e.content
|
||||
.users
|
||||
.get(self.user_id())
|
||||
.map(|p| (*p).into())
|
||||
.unwrap_or_else(|| e.content.users_default.into())
|
||||
})
|
||||
.map(|e| e.power_levels().for_user(self.user_id()).into())
|
||||
.unwrap_or_else(|| if self.is_room_creator { 100 } else { 0 })
|
||||
}
|
||||
|
||||
@@ -106,4 +103,13 @@ impl RoomMember {
|
||||
pub fn name_ambiguous(&self) -> bool {
|
||||
self.display_name_ambiguous
|
||||
}
|
||||
|
||||
/// Get the membership state of this member.
|
||||
pub fn membership(&self) -> &MembershipState {
|
||||
if let Some(p) = self.profile.as_ref() {
|
||||
p.membership()
|
||||
} else {
|
||||
self.event.membership()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
mod members;
|
||||
mod normal;
|
||||
|
||||
use std::{collections::HashSet, fmt};
|
||||
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{Room, RoomInfo, RoomType};
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent, canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent, encryption::RoomEncryptionEventContent,
|
||||
guest_access::RoomGuestAccessEventContent,
|
||||
history_visibility::RoomHistoryVisibilityEventContent,
|
||||
join_rules::RoomJoinRulesEventContent, name::RoomNameEventContent,
|
||||
redaction::OriginalSyncRoomRedactionEvent, tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
AnyStrippedStateEvent, AnySyncStateEvent, RedactContent, RedactedEventContent,
|
||||
StateEventContent, SyncStateEvent,
|
||||
},
|
||||
EventId, OwnedUserId, RoomVersionId,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
use crate::MinimalStateEvent;
|
||||
|
||||
/// The name of the room, either from the metadata or calculated
|
||||
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum DisplayName {
|
||||
/// The room has been named explicitly as
|
||||
Named(String),
|
||||
/// The room has a canonical alias that should be used
|
||||
Aliased(String),
|
||||
/// The room has not given an explicit name but a name could be
|
||||
/// calculated
|
||||
Calculated(String),
|
||||
/// The room doesn't have a name right now, but used to have one
|
||||
/// e.g. because it was a DM and everyone has left the room
|
||||
EmptyWas(String),
|
||||
/// No useful name could be calculated or ever found
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {})", s),
|
||||
DisplayName::Empty => write!(f, "Empty Room"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A base room info struct that is the backbone of normal as well as stripped
|
||||
/// rooms. Holds all the state events that are important to present a room to
|
||||
/// users.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BaseRoomInfo {
|
||||
/// The avatar URL of this room.
|
||||
avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
|
||||
/// The canonical alias of this room.
|
||||
canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
|
||||
/// The `m.room.create` event content of this room.
|
||||
create: Option<MinimalStateEvent<RoomCreateEventContent>>,
|
||||
/// A list of user ids this room is considered as direct message, if this
|
||||
/// room is a DM.
|
||||
pub(crate) dm_targets: HashSet<OwnedUserId>,
|
||||
/// The `m.room.encryption` event content that enabled E2EE in this room.
|
||||
pub(crate) encryption: Option<RoomEncryptionEventContent>,
|
||||
/// The guest access policy of this room.
|
||||
guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
|
||||
/// The history visibility policy of this room.
|
||||
history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
|
||||
/// The join rule policy of this room.
|
||||
join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
|
||||
/// The maximal power level that can be found in this room.
|
||||
pub(crate) max_power_level: i64,
|
||||
/// The `m.room.name` of this room.
|
||||
name: Option<MinimalStateEvent<RoomNameEventContent>>,
|
||||
/// The `m.room.tombstone` event content of this room.
|
||||
tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
|
||||
/// The topic of this room.
|
||||
topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
|
||||
}
|
||||
|
||||
impl BaseRoomInfo {
|
||||
/// Create a new, empty base room info.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_room_name(
|
||||
&self,
|
||||
joined_member_count: u64,
|
||||
invited_member_count: u64,
|
||||
heroes: Vec<RoomMember>,
|
||||
) -> DisplayName {
|
||||
calculate_room_name(
|
||||
joined_member_count,
|
||||
invited_member_count,
|
||||
heroes.iter().take(3).map(|mem| mem.name()).collect::<Vec<&str>>(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle a state event for this room and update our info accordingly.
|
||||
///
|
||||
/// Returns true if the event modified the info, false otherwise.
|
||||
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
|
||||
match ev {
|
||||
// No redacted branch - enabling encryption cannot be undone.
|
||||
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
|
||||
self.encryption = Some(encryption.content.clone());
|
||||
}
|
||||
AnySyncStateEvent::RoomAvatar(a) => {
|
||||
self.avatar = Some(a.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomName(n) => {
|
||||
self.name = Some(n.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
|
||||
self.create = Some(c.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomHistoryVisibility(h) => {
|
||||
self.history_visibility = Some(h.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomGuestAccess(g) => {
|
||||
self.guest_access = Some(g.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomJoinRules(c) => {
|
||||
self.join_rules = Some(c.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomCanonicalAlias(a) => {
|
||||
self.canonical_alias = Some(a.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomTopic(t) => {
|
||||
self.topic = Some(t.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomTombstone(t) => {
|
||||
self.tombstone = Some(t.into());
|
||||
}
|
||||
AnySyncStateEvent::RoomPowerLevels(p) => {
|
||||
self.max_power_level = p.power_levels().max().into();
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Handle a stripped state event for this room and update our info
|
||||
/// accordingly.
|
||||
///
|
||||
/// Returns true if the event modified the info, false otherwise.
|
||||
pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
|
||||
match ev {
|
||||
AnyStrippedStateEvent::RoomEncryption(encryption) => {
|
||||
self.encryption = Some(encryption.content.clone());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomAvatar(a) => {
|
||||
self.avatar = Some(a.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomName(n) => {
|
||||
self.name = Some(n.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
|
||||
self.create = Some(c.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
|
||||
self.history_visibility = Some(h.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomGuestAccess(g) => {
|
||||
self.guest_access = Some(g.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomJoinRules(c) => {
|
||||
self.join_rules = Some(c.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
|
||||
self.canonical_alias = Some(a.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomTopic(t) => {
|
||||
self.topic = Some(t.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomTombstone(t) => {
|
||||
self.tombstone = Some(t.into());
|
||||
}
|
||||
AnyStrippedStateEvent::RoomPowerLevels(p) => {
|
||||
self.max_power_level = p.power_levels().max().into();
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn handle_redaction(&mut self, event: &OriginalSyncRoomRedactionEvent) {
|
||||
let room_version = self
|
||||
.create
|
||||
.as_ref()
|
||||
.and_then(|ev| Some(ev.as_original()?.content.room_version.to_owned()))
|
||||
.unwrap_or(RoomVersionId::V1);
|
||||
|
||||
// FIXME: Use let chains once available to get rid of unwrap()s
|
||||
if self.avatar.has_event_id(&event.redacts) {
|
||||
self.avatar.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.canonical_alias.has_event_id(&event.redacts) {
|
||||
self.canonical_alias.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.create.has_event_id(&event.redacts) {
|
||||
self.create.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.guest_access.has_event_id(&event.redacts) {
|
||||
self.guest_access.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.history_visibility.has_event_id(&event.redacts) {
|
||||
self.history_visibility.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.join_rules.has_event_id(&event.redacts) {
|
||||
self.join_rules.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.name.has_event_id(&event.redacts) {
|
||||
self.name.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.tombstone.has_event_id(&event.redacts) {
|
||||
self.tombstone.as_mut().unwrap().redact(&room_version);
|
||||
} else if self.topic.has_event_id(&event.redacts) {
|
||||
self.topic.as_mut().unwrap().redact(&room_version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait OptionExt {
|
||||
fn has_event_id(&self, ev_id: &EventId) -> bool;
|
||||
}
|
||||
|
||||
impl<C> OptionExt for Option<MinimalStateEvent<C>>
|
||||
where
|
||||
C: StateEventContent + RedactContent,
|
||||
C::Redacted: StateEventContent + RedactedEventContent + DeserializeOwned,
|
||||
{
|
||||
fn has_event_id(&self, ev_id: &EventId) -> bool {
|
||||
self.as_ref().and_then(|ev| ev.event_id()).map_or(false, |id| id == ev_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BaseRoomInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
avatar: None,
|
||||
canonical_alias: None,
|
||||
create: None,
|
||||
dm_targets: Default::default(),
|
||||
encryption: None,
|
||||
guest_access: None,
|
||||
history_visibility: None,
|
||||
join_rules: None,
|
||||
max_power_level: 100,
|
||||
name: None,
|
||||
tombstone: None,
|
||||
topic: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate room name according to step 3 of the [naming algorithm.]
|
||||
fn calculate_room_name(
|
||||
joined_member_count: u64,
|
||||
invited_member_count: u64,
|
||||
heroes: Vec<&str>,
|
||||
) -> DisplayName {
|
||||
let heroes_count = heroes.len() as u64;
|
||||
let invited_joined = invited_member_count + joined_member_count;
|
||||
let invited_joined_minus_one = invited_joined.saturating_sub(1);
|
||||
|
||||
let names = if heroes_count >= invited_joined_minus_one {
|
||||
let mut names = heroes;
|
||||
// stabilize ordering
|
||||
names.sort_unstable();
|
||||
names.join(", ")
|
||||
} else if heroes_count < invited_joined_minus_one && invited_joined > 1 {
|
||||
let mut names = heroes;
|
||||
names.sort_unstable();
|
||||
|
||||
// TODO: What length does the spec want us to use here and in
|
||||
// the `else`?
|
||||
format!("{}, and {} others", names.join(", "), (invited_joined - heroes_count))
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
|
||||
// User is alone.
|
||||
if invited_joined <= 1 {
|
||||
if names.is_empty() {
|
||||
DisplayName::Empty
|
||||
} else {
|
||||
DisplayName::EmptyWas(names)
|
||||
}
|
||||
} else {
|
||||
DisplayName::Calculated(names)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
|
||||
fn test_calculate_room_name() {
|
||||
let mut actual = calculate_room_name(2, 0, vec!["a"]);
|
||||
assert_eq!(DisplayName::Calculated("a".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(3, 0, vec!["a", "b"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(4, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(5, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(0, 0, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
|
||||
actual = calculate_room_name(0, 1, vec![]);
|
||||
assert_eq!(DisplayName::Empty, actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a", "b"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,17 +15,42 @@
|
||||
|
||||
//! User sessions.
|
||||
|
||||
use ruma::{DeviceId, UserId};
|
||||
use ruma::{OwnedDeviceId, OwnedUserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A user session, containing an access token and information about the
|
||||
/// associated user account.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use matrix_sdk_base::Session;
|
||||
/// use ruma::{device_id, user_id};
|
||||
///
|
||||
/// let session = Session {
|
||||
/// access_token: "My-Token".to_owned(),
|
||||
/// user_id: user_id!("@example:localhost").to_owned(),
|
||||
/// device_id: device_id!("MYDEVICEID").to_owned(),
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(session.device_id.as_str(), "MYDEVICEID");
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
/// The access token used for this session.
|
||||
pub access_token: String,
|
||||
/// The user the access token was issued for.
|
||||
pub user_id: UserId,
|
||||
pub user_id: OwnedUserId,
|
||||
/// The ID of the client device
|
||||
pub device_id: Box<DeviceId>,
|
||||
pub device_id: OwnedDeviceId,
|
||||
}
|
||||
|
||||
impl From<ruma::api::client::session::login::v3::Response> for Session {
|
||||
fn from(response: ruma::api::client::session::login::v3::Response) -> Self {
|
||||
Self {
|
||||
access_token: response.access_token,
|
||||
user_id: response.user_id,
|
||||
device_id: response.device_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
-50
@@ -15,27 +15,30 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::{AmbiguityChange, MemberEvent};
|
||||
use ruma::{events::room::member::MembershipState, EventId, RoomId, UserId};
|
||||
use ruma::{
|
||||
events::room::member::{MembershipState, SyncRoomMemberEvent},
|
||||
OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
use super::{Result, StateChanges};
|
||||
use crate::Store;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AmbiguityCache {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AmbiguityCache {
|
||||
pub store: Store,
|
||||
pub cache: BTreeMap<RoomId, BTreeMap<String, BTreeSet<UserId>>>,
|
||||
pub changes: BTreeMap<RoomId, BTreeMap<EventId, AmbiguityChange>>,
|
||||
pub cache: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
struct AmbiguityMap {
|
||||
display_name: String,
|
||||
users: BTreeSet<UserId>,
|
||||
users: BTreeSet<OwnedUserId>,
|
||||
}
|
||||
|
||||
impl AmbiguityMap {
|
||||
fn remove(&mut self, user_id: &UserId) -> Option<UserId> {
|
||||
fn remove(&mut self, user_id: &UserId) -> Option<OwnedUserId> {
|
||||
self.users.remove(user_id);
|
||||
|
||||
if self.user_count() == 1 {
|
||||
@@ -45,7 +48,7 @@ impl AmbiguityMap {
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, user_id: UserId) -> Option<UserId> {
|
||||
fn add(&mut self, user_id: OwnedUserId) -> Option<OwnedUserId> {
|
||||
let ambiguous_user =
|
||||
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
|
||||
|
||||
@@ -72,7 +75,7 @@ impl AmbiguityCache {
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &MemberEvent,
|
||||
member_event: &SyncRoomMemberEvent,
|
||||
) -> Result<()> {
|
||||
// Synapse seems to have a bug where it puts the same event into the
|
||||
// state and the timeline sometimes.
|
||||
@@ -86,7 +89,7 @@ impl AmbiguityCache {
|
||||
if self
|
||||
.changes
|
||||
.get(room_id)
|
||||
.map(|c| c.contains_key(&member_event.event_id))
|
||||
.map(|c| c.contains_key(member_event.event_id()))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(());
|
||||
@@ -103,9 +106,10 @@ impl AmbiguityCache {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let disambiguated_member = old_map.as_mut().and_then(|o| o.remove(&member_event.state_key));
|
||||
let disambiguated_member =
|
||||
old_map.as_mut().and_then(|o| o.remove(member_event.state_key()));
|
||||
let ambiguated_member =
|
||||
new_map.as_mut().and_then(|n| n.add(member_event.state_key.clone()));
|
||||
new_map.as_mut().and_then(|n| n.add(member_event.state_key().clone()));
|
||||
let ambiguous = new_map.as_ref().map(|n| n.is_ambiguous()).unwrap_or(false);
|
||||
|
||||
self.update(room_id, old_map, new_map);
|
||||
@@ -116,9 +120,9 @@ impl AmbiguityCache {
|
||||
member_ambiguous: ambiguous,
|
||||
};
|
||||
|
||||
trace!("Handling display name ambiguity for {}: {:#?}", member_event.state_key, change);
|
||||
trace!("Handling display name ambiguity for {}: {:#?}", member_event.state_key(), change);
|
||||
|
||||
self.add_change(room_id, member_event.event_id.clone(), change);
|
||||
self.add_change(room_id, member_event.event_id().to_owned(), change);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -129,7 +133,7 @@ impl AmbiguityCache {
|
||||
old_map: Option<AmbiguityMap>,
|
||||
new_map: Option<AmbiguityMap>,
|
||||
) {
|
||||
let entry = self.cache.entry(room_id.clone()).or_insert_with(BTreeMap::new);
|
||||
let entry = self.cache.entry(room_id.to_owned()).or_default();
|
||||
|
||||
if let Some(old) = old_map {
|
||||
entry.insert(old.display_name, old.users);
|
||||
@@ -140,47 +144,49 @@ impl AmbiguityCache {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_change(&mut self, room_id: &RoomId, event_id: EventId, change: AmbiguityChange) {
|
||||
self.changes.entry(room_id.clone()).or_insert_with(BTreeMap::new).insert(event_id, change);
|
||||
fn add_change(&mut self, room_id: &RoomId, event_id: OwnedEventId, change: AmbiguityChange) {
|
||||
self.changes.entry(room_id.to_owned()).or_default().insert(event_id, change);
|
||||
}
|
||||
|
||||
async fn get(
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &MemberEvent,
|
||||
member_event: &SyncRoomMemberEvent,
|
||||
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
|
||||
use MembershipState::*;
|
||||
|
||||
let old_event = if let Some(m) =
|
||||
changes.members.get(room_id).and_then(|m| m.get(&member_event.state_key))
|
||||
changes.members.get(room_id).and_then(|m| m.get(member_event.state_key()))
|
||||
{
|
||||
Some(m.clone())
|
||||
Some(MemberEvent::Sync(m.clone()))
|
||||
} else {
|
||||
self.store.get_member_event(room_id, &member_event.state_key).await?
|
||||
self.store.get_member_event(room_id, member_event.state_key()).await?
|
||||
};
|
||||
|
||||
let old_display_name = if let Some(event) = old_event {
|
||||
if matches!(event.content.membership, Join | Invite) {
|
||||
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.displayname.as_deref())
|
||||
.and_then(|p| p.get(member_event.state_key()))
|
||||
.and_then(|p| p.as_original())
|
||||
.and_then(|p| p.content.displayname.as_deref())
|
||||
{
|
||||
Some(d.to_string())
|
||||
Some(d.to_owned())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, &member_event.state_key)
|
||||
.get_profile(room_id, member_event.state_key())
|
||||
.await?
|
||||
.and_then(|c| c.displayname)
|
||||
.and_then(|p| p.into_original())
|
||||
.and_then(|p| p.content.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
event.content.displayname.clone()
|
||||
event.original_content().and_then(|c| c.displayname.clone())
|
||||
};
|
||||
|
||||
Some(display_name.unwrap_or_else(|| event.state_key.localpart().to_string()))
|
||||
Some(display_name.unwrap_or_else(|| event.user_id().localpart().to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -189,30 +195,28 @@ impl AmbiguityCache {
|
||||
};
|
||||
|
||||
let old_map = if let Some(old_name) = old_display_name.as_deref() {
|
||||
let old_display_name_map = if let Some(u) =
|
||||
self.cache.entry(room_id.clone()).or_insert_with(BTreeMap::new).get(old_name)
|
||||
{
|
||||
u.clone()
|
||||
} else {
|
||||
self.store.get_users_with_display_name(room_id, old_name).await?
|
||||
};
|
||||
let old_display_name_map =
|
||||
if let Some(u) = self.cache.entry(room_id.to_owned()).or_default().get(old_name) {
|
||||
u.clone()
|
||||
} else {
|
||||
self.store.get_users_with_display_name(room_id, old_name).await?
|
||||
};
|
||||
|
||||
Some(AmbiguityMap { display_name: old_name.to_string(), users: old_display_name_map })
|
||||
Some(AmbiguityMap { display_name: old_name.to_owned(), users: old_display_name_map })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let new_map = if matches!(member_event.content.membership, Join | Invite) {
|
||||
let new_map = if matches!(member_event.membership(), Join | Invite) {
|
||||
let new = member_event
|
||||
.content
|
||||
.displayname
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| member_event.state_key.localpart());
|
||||
.as_original()
|
||||
.and_then(|ev| ev.content.displayname.as_deref())
|
||||
.unwrap_or_else(|| member_event.state_key().localpart());
|
||||
|
||||
// We don't allow other users to set the display name, so if we
|
||||
// have a more trusted version of the display
|
||||
// name use that.
|
||||
let new_display_name = if member_event.sender.as_str() == member_event.state_key {
|
||||
let new_display_name = if member_event.sender().as_str() == member_event.state_key() {
|
||||
new
|
||||
} else if let Some(old) = old_display_name.as_deref() {
|
||||
old
|
||||
@@ -220,11 +224,8 @@ impl AmbiguityCache {
|
||||
new
|
||||
};
|
||||
|
||||
let new_display_name_map = if let Some(u) = self
|
||||
.cache
|
||||
.entry(room_id.clone())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.get(new_display_name)
|
||||
let new_display_name_map = if let Some(u) =
|
||||
self.cache.entry(room_id.to_owned()).or_default().get(new_display_name)
|
||||
{
|
||||
u.clone()
|
||||
} else {
|
||||
@@ -232,7 +233,7 @@ impl AmbiguityCache {
|
||||
};
|
||||
|
||||
Some(AmbiguityMap {
|
||||
display_name: new_display_name.to_string(),
|
||||
display_name: new_display_name.to_owned(),
|
||||
users: new_display_name_map,
|
||||
})
|
||||
} else {
|
||||
@@ -0,0 +1,797 @@
|
||||
//! Macro of integration tests for StateStore implementions.
|
||||
|
||||
/// Macro building to allow your StateStore implementation to run the entire
|
||||
/// tests suite locally.
|
||||
///
|
||||
/// You need to provide a `async fn get_store() -> StoreResult<impl StateStore>`
|
||||
/// providing a fresh store on the same level you invoke the macro.
|
||||
///
|
||||
/// ## Usage Example:
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_base::store::{
|
||||
/// # StateStore,
|
||||
/// # MemoryStore as MyStore,
|
||||
/// # Result as StoreResult,
|
||||
/// # };
|
||||
///
|
||||
/// #[cfg(test)]
|
||||
/// mod tests {
|
||||
///
|
||||
/// use super::{MyStore, StoreResult, StateStore};
|
||||
///
|
||||
/// async fn get_store() -> StoreResult<impl StateStore> {
|
||||
/// Ok(MyStore::new())
|
||||
/// }
|
||||
///
|
||||
/// statestore_integration_tests! { integration }
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(unused_macros, unused_extern_crates)]
|
||||
#[macro_export]
|
||||
macro_rules! statestore_integration_tests {
|
||||
($($name:ident)*) => {
|
||||
$(
|
||||
mod $name {
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use futures_util::StreamExt;
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
message::get_message_events::v3::Response as MessageResponse,
|
||||
sync::sync_events::v3::Response as SyncResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
}
|
||||
};
|
||||
use matrix_sdk_test::{async_test, test_json};
|
||||
use ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
device_id, event_id,
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
room::{
|
||||
member::{
|
||||
MembershipState, OriginalSyncRoomMemberEvent, SyncRoomMemberEvent,
|
||||
RoomMemberEventContent, StrippedRoomMemberEvent,
|
||||
},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
MediaSource,
|
||||
},
|
||||
AnyEphemeralRoomEventContent, AnySyncEphemeralRoomEvent,
|
||||
AnyStrippedStateEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
|
||||
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType,
|
||||
StateEventType, StateUnsigned,
|
||||
},
|
||||
mxc_uri,
|
||||
receipt::ReceiptType,
|
||||
room_id,
|
||||
serde::Raw,
|
||||
uint, user_id, MilliSecondsSinceUnixEpoch, UserId, EventId, OwnedEventId,
|
||||
RoomId,
|
||||
};
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use $crate::{
|
||||
http::Response,
|
||||
deserialized_responses::{ RoomEvent, SyncRoomEvent, TimelineSlice},
|
||||
};
|
||||
use $crate::{
|
||||
RoomType, Session,
|
||||
media::{MediaFormat, MediaRequest, MediaThumbnailSize},
|
||||
store::{
|
||||
Store,
|
||||
StateStore,
|
||||
Result as StoreResult,
|
||||
StateChanges
|
||||
}
|
||||
};
|
||||
|
||||
use super::get_store;
|
||||
|
||||
|
||||
fn user_id() -> &'static UserId {
|
||||
user_id!("@example:localhost")
|
||||
}
|
||||
pub(crate) fn invited_user_id() -> &'static UserId {
|
||||
user_id!("@invited:localhost")
|
||||
}
|
||||
|
||||
pub(crate) fn room_id() -> &'static RoomId {
|
||||
room_id!("!test:localhost")
|
||||
}
|
||||
|
||||
pub(crate) fn stripped_room_id() -> &'static RoomId {
|
||||
room_id!("!stripped:localhost")
|
||||
}
|
||||
|
||||
pub(crate) fn first_receipt_event_id() -> &'static EventId {
|
||||
event_id!("$example")
|
||||
}
|
||||
|
||||
/// Populate the given `StateStore`.
|
||||
pub(crate) async fn populated_store(inner: Box<dyn StateStore>) -> StoreResult<Store> {
|
||||
let mut changes = StateChanges::default();
|
||||
let store = Store::new(inner);
|
||||
|
||||
let user_id = user_id();
|
||||
let invited_user_id = invited_user_id();
|
||||
let room_id = room_id();
|
||||
let stripped_room_id = stripped_room_id();
|
||||
let device_id = device_id!("device");
|
||||
|
||||
let session = Session {
|
||||
access_token: "token".to_owned(),
|
||||
user_id: user_id.to_owned(),
|
||||
device_id: device_id.to_owned(),
|
||||
};
|
||||
store.restore_session(session).await.unwrap();
|
||||
|
||||
changes.sync_token = Some("t392-516_47314_0_7_1_1_1_11444_1".to_owned());
|
||||
|
||||
let presence_json: &JsonValue = &test_json::PRESENCE;
|
||||
let presence_raw =
|
||||
serde_json::from_value::<Raw<PresenceEvent>>(presence_json.clone()).unwrap();
|
||||
let presence_event = presence_raw.deserialize().unwrap();
|
||||
changes.add_presence_event(presence_event, presence_raw);
|
||||
|
||||
let pushrules_json: &JsonValue = &test_json::PUSH_RULES;
|
||||
let pushrules_raw =
|
||||
serde_json::from_value::<Raw<AnyGlobalAccountDataEvent>>(pushrules_json.clone())
|
||||
.unwrap();
|
||||
let pushrules_event = pushrules_raw.deserialize().unwrap();
|
||||
changes.add_account_data(pushrules_event, pushrules_raw);
|
||||
|
||||
let mut room = store.get_or_create_room(room_id, RoomType::Joined).await.clone_info();
|
||||
room.mark_as_left();
|
||||
|
||||
let tag_json: &JsonValue = &test_json::TAG;
|
||||
let tag_raw =
|
||||
serde_json::from_value::<Raw<AnyRoomAccountDataEvent>>(tag_json.clone()).unwrap();
|
||||
let tag_event = tag_raw.deserialize().unwrap();
|
||||
changes.add_room_account_data(room_id, tag_event, tag_raw);
|
||||
|
||||
let name_json: &JsonValue = &test_json::NAME;
|
||||
let name_raw = serde_json::from_value::<Raw<AnySyncStateEvent>>(name_json.clone()).unwrap();
|
||||
let name_event = name_raw.deserialize().unwrap();
|
||||
room.handle_state_event(&name_event);
|
||||
changes.add_state_event(room_id, name_event, name_raw);
|
||||
|
||||
let topic_json: &JsonValue = &test_json::TOPIC;
|
||||
let topic_raw =
|
||||
serde_json::from_value::<Raw<AnySyncStateEvent>>(topic_json.clone()).unwrap();
|
||||
let topic_event = topic_raw.deserialize().unwrap();
|
||||
room.handle_state_event(&topic_event);
|
||||
changes.add_state_event(room_id, topic_event, topic_raw);
|
||||
|
||||
let mut room_ambiguity_map = BTreeMap::new();
|
||||
let mut room_profiles = BTreeMap::new();
|
||||
let mut room_members = BTreeMap::new();
|
||||
|
||||
let member_json: &JsonValue = &test_json::MEMBER;
|
||||
let member_event: SyncRoomMemberEvent =
|
||||
serde_json::from_value(member_json.clone()).unwrap();
|
||||
let displayname =
|
||||
member_event.as_original().unwrap().content.displayname.clone().unwrap();
|
||||
room_ambiguity_map.insert(
|
||||
displayname.clone(),
|
||||
BTreeSet::from([user_id.to_owned()]),
|
||||
);
|
||||
room_profiles.insert(user_id.to_owned(), (&member_event).into());
|
||||
room_members.insert(user_id.to_owned(), member_event);
|
||||
|
||||
let member_state_raw =
|
||||
serde_json::from_value::<Raw<AnySyncStateEvent>>(member_json.clone()).unwrap();
|
||||
let member_state_event = member_state_raw.deserialize().unwrap();
|
||||
changes.add_state_event(room_id, member_state_event, member_state_raw);
|
||||
|
||||
let invited_member_json: &JsonValue = &test_json::MEMBER_INVITE;
|
||||
// FIXME: Should be stripped room member event
|
||||
let invited_member_event: SyncRoomMemberEvent =
|
||||
serde_json::from_value(invited_member_json.clone()).unwrap();
|
||||
room_ambiguity_map
|
||||
.entry(displayname)
|
||||
.or_default()
|
||||
.insert(invited_user_id.to_owned());
|
||||
room_profiles.insert(invited_user_id.to_owned(), (&invited_member_event).into());
|
||||
room_members.insert(invited_user_id.to_owned(), invited_member_event);
|
||||
|
||||
let invited_member_state_raw =
|
||||
serde_json::from_value::<Raw<AnySyncStateEvent>>(invited_member_json.clone()).unwrap();
|
||||
let invited_member_state_event = invited_member_state_raw.deserialize().unwrap();
|
||||
changes.add_state_event(room_id, invited_member_state_event, invited_member_state_raw);
|
||||
|
||||
let receipt_json: &JsonValue = &test_json::READ_RECEIPT;
|
||||
let receipt_event =
|
||||
serde_json::from_value::<AnySyncEphemeralRoomEvent>(receipt_json.clone()).unwrap();
|
||||
let receipt_content = match receipt_event.content() {
|
||||
AnyEphemeralRoomEventContent::Receipt(content) => content,
|
||||
_ => panic!(),
|
||||
};
|
||||
changes.add_receipts(room_id, receipt_content);
|
||||
|
||||
changes.ambiguity_maps.insert(room_id.to_owned(), room_ambiguity_map);
|
||||
changes.profiles.insert(room_id.to_owned(), room_profiles);
|
||||
changes.members.insert(room_id.to_owned(), room_members);
|
||||
changes.add_room(room);
|
||||
|
||||
let mut stripped_room =
|
||||
store.get_or_create_stripped_room(stripped_room_id).await.clone_info();
|
||||
|
||||
let stripped_name_json: &JsonValue = &test_json::NAME_STRIPPED;
|
||||
let stripped_name_raw =
|
||||
serde_json::from_value::<Raw<AnyStrippedStateEvent>>(stripped_name_json.clone())
|
||||
.unwrap();
|
||||
let stripped_name_event = stripped_name_raw.deserialize().unwrap();
|
||||
stripped_room.handle_stripped_state_event(&stripped_name_event);
|
||||
changes.stripped_state.insert(
|
||||
stripped_room_id.to_owned(),
|
||||
BTreeMap::from([(
|
||||
stripped_name_event.event_type(),
|
||||
BTreeMap::from([(
|
||||
stripped_name_event.state_key().to_owned(),
|
||||
stripped_name_raw.clone(),
|
||||
)]),
|
||||
)]),
|
||||
);
|
||||
|
||||
changes.add_stripped_room(stripped_room);
|
||||
|
||||
let stripped_member_json: &JsonValue = &test_json::MEMBER_STRIPPED;
|
||||
let stripped_member_event = serde_json::from_value::<StrippedRoomMemberEvent>(
|
||||
stripped_member_json.clone(),
|
||||
).unwrap();
|
||||
changes.add_stripped_member(stripped_room_id, stripped_member_event);
|
||||
|
||||
store.save_changes(&changes).await?;
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
fn power_level_event() -> Raw<AnySyncStateEvent> {
|
||||
let content = RoomPowerLevelsEventContent::default();
|
||||
|
||||
let event = json!({
|
||||
"event_id": "$h29iv0s8:example.com",
|
||||
"content": content,
|
||||
"sender": user_id(),
|
||||
"type": "m.room.power_levels",
|
||||
"origin_server_ts": 0u64,
|
||||
"state_key": "",
|
||||
});
|
||||
|
||||
serde_json::from_value(event).unwrap()
|
||||
}
|
||||
|
||||
fn stripped_membership_event() -> StrippedRoomMemberEvent {
|
||||
custom_stripped_membership_event(user_id())
|
||||
}
|
||||
|
||||
fn custom_stripped_membership_event(
|
||||
user_id: &UserId,
|
||||
) -> StrippedRoomMemberEvent {
|
||||
StrippedRoomMemberEvent {
|
||||
content: RoomMemberEventContent::new(MembershipState::Join),
|
||||
sender: user_id.to_owned(),
|
||||
state_key: user_id.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn membership_event() -> SyncRoomMemberEvent {
|
||||
custom_membership_event(user_id(), event_id!("$h29iv0s8:example.com").to_owned())
|
||||
}
|
||||
|
||||
fn custom_membership_event(
|
||||
user_id: &UserId,
|
||||
event_id: OwnedEventId,
|
||||
) -> SyncRoomMemberEvent {
|
||||
SyncRoomMemberEvent::Original(OriginalSyncRoomMemberEvent {
|
||||
event_id,
|
||||
content: RoomMemberEventContent::new(MembershipState::Join),
|
||||
sender: user_id.to_owned(),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch(198u32.into()),
|
||||
state_key: user_id.to_owned(),
|
||||
unsigned: StateUnsigned::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_populate_store() -> StoreResult<()> {
|
||||
let room_id = room_id();
|
||||
let user_id = user_id();
|
||||
let inner_store = get_store().await?;
|
||||
|
||||
let store = populated_store(Box::new(inner_store)).await?;
|
||||
|
||||
assert!(store.get_sync_token().await?.is_some());
|
||||
assert!(store.get_presence_event(user_id).await?.is_some());
|
||||
assert_eq!(store.get_room_infos().await?.len(), 1, "Expected to find 1 room info ");
|
||||
assert_eq!(store.get_stripped_room_infos().await?.len(), 1, "Expected to find 1 stripped room info");
|
||||
assert!(store.get_account_data_event(GlobalAccountDataEventType::PushRules).await?.is_some());
|
||||
|
||||
assert!(store.get_state_event(room_id, StateEventType::RoomName, "").await?.is_some());
|
||||
assert_eq!(store.get_state_events(room_id, StateEventType::RoomTopic).await?.len(), 1, "Expected to find 1 room topic");
|
||||
assert!(store.get_profile(room_id, user_id).await?.is_some());
|
||||
assert!(store.get_member_event(room_id, user_id).await?.is_some());
|
||||
assert_eq!(store.get_user_ids(room_id).await?.len(), 2, "Expected to find 2 members for room");
|
||||
assert_eq!(store.get_invited_user_ids(room_id).await?.len(), 1, "Expected to find 1 invited user ids");
|
||||
assert_eq!(store.get_joined_user_ids(room_id).await?.len(), 1, "Expected to find 1 joined user ids");
|
||||
assert_eq!(store.get_users_with_display_name(room_id, "example").await?.len(), 2, "Expected to find 2 display names for room");
|
||||
assert!(store
|
||||
.get_room_account_data_event(room_id, RoomAccountDataEventType::Tag)
|
||||
.await?
|
||||
.is_some());
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(room_id, ReceiptType::Read, user_id)
|
||||
.await?
|
||||
.is_some());
|
||||
assert_eq!(
|
||||
store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, first_receipt_event_id())
|
||||
.await?
|
||||
.len(),
|
||||
1, "Expected to find 1 read receipt");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_member_saving() {
|
||||
let store = get_store().await.unwrap();
|
||||
let room_id = room_id!("!test_member_saving:localhost");
|
||||
let user_id = user_id();
|
||||
|
||||
assert!(store.get_member_event(room_id, user_id).await.unwrap().is_none());
|
||||
let mut changes = StateChanges::default();
|
||||
changes
|
||||
.members
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(user_id.to_owned(), membership_event());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
assert!(store.get_member_event(room_id, user_id).await.unwrap().is_some());
|
||||
|
||||
let members = store.get_user_ids(room_id).await.unwrap();
|
||||
assert!(!members.is_empty(), "We expected to find members for the room")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_filter_saving() {
|
||||
let store = get_store().await.unwrap();
|
||||
let test_name = "filter_name";
|
||||
let filter_id = "filter_id_1234";
|
||||
assert_eq!(store.get_filter(test_name).await.unwrap(), None);
|
||||
store.save_filter(test_name, filter_id).await.unwrap();
|
||||
assert_eq!(store.get_filter(test_name).await.unwrap(), Some(filter_id.to_owned()));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_sync_token_saving() {
|
||||
let mut changes = StateChanges::default();
|
||||
let store = get_store().await.unwrap();
|
||||
let sync_token = "t392-516_47314_0_7_1".to_owned();
|
||||
|
||||
changes.sync_token = Some(sync_token.clone());
|
||||
assert_eq!(store.get_sync_token().await.unwrap(), None);
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
assert_eq!(store.get_sync_token().await.unwrap(), Some(sync_token));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_stripped_member_saving() {
|
||||
let store = get_store().await.unwrap();
|
||||
let room_id = room_id!("!test_stripped_member_saving:localhost");
|
||||
let user_id = user_id();
|
||||
|
||||
assert!(store.get_member_event(room_id, user_id).await.unwrap().is_none());
|
||||
let mut changes = StateChanges::default();
|
||||
changes
|
||||
.stripped_members
|
||||
.entry(room_id.to_owned())
|
||||
.or_default()
|
||||
.insert(user_id.to_owned(), stripped_membership_event());
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
assert!(store.get_member_event(room_id, user_id).await.unwrap().is_some());
|
||||
|
||||
let members = store.get_user_ids(room_id).await.unwrap();
|
||||
assert!(!members.is_empty(), "We expected to find members for the room")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_power_level_saving() {
|
||||
let store = get_store().await.unwrap();
|
||||
let room_id = room_id!("!test_power_level_saving:localhost");
|
||||
|
||||
let raw_event = power_level_event();
|
||||
let event = raw_event.deserialize().unwrap();
|
||||
|
||||
assert!(store
|
||||
.get_state_event(room_id, StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_state_event(room_id, event, raw_event);
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
assert!(store
|
||||
.get_state_event(room_id, StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receipts_saving() {
|
||||
let store = get_store().await.expect("creating store failed");
|
||||
|
||||
let room_id = room_id!("!test_receipts_saving:localhost");
|
||||
|
||||
let first_event_id = event_id!("$1435641916114394fHBLK:matrix.org");
|
||||
let second_event_id = event_id!("$fHBLK1435641916114394:matrix.org");
|
||||
|
||||
let first_receipt_event = serde_json::from_value(json!({
|
||||
first_event_id: {
|
||||
"m.read": {
|
||||
user_id(): {
|
||||
"ts": 1436451550453u64
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.expect("json creation failed");
|
||||
|
||||
let second_receipt_event = serde_json::from_value(json!({
|
||||
second_event_id: {
|
||||
"m.read": {
|
||||
user_id(): {
|
||||
"ts": 1436451551453u64
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.expect("json creation failed");
|
||||
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(room_id, ReceiptType::Read, user_id())
|
||||
.await
|
||||
.expect("failed to read user room receipt")
|
||||
.is_none());
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, &first_event_id)
|
||||
.await
|
||||
.expect("failed to read user room receipt for 1")
|
||||
.is_empty());
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, &second_event_id)
|
||||
.await
|
||||
.expect("failed to read user room receipt for 2")
|
||||
.is_empty());
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_receipts(room_id, first_receipt_event);
|
||||
|
||||
store.save_changes(&changes).await.expect("writing changes fauked");
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(room_id, ReceiptType::Read, user_id())
|
||||
.await
|
||||
.expect("failed to read user room receipt after save")
|
||||
.is_some());
|
||||
assert_eq!(
|
||||
store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, &first_event_id)
|
||||
.await
|
||||
.expect("failed to read user room receipt for 1 after save")
|
||||
.len(),
|
||||
1,
|
||||
"Found a wrong number of receipts for 1 after save"
|
||||
);
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, &second_event_id)
|
||||
.await
|
||||
.expect("failed to read user room receipt for 2 after save")
|
||||
.is_empty());
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_receipts(room_id, second_receipt_event);
|
||||
|
||||
store.save_changes(&changes).await.expect("Saving works");
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(room_id, ReceiptType::Read, user_id())
|
||||
.await
|
||||
.expect("Getting user room receipts failed")
|
||||
.is_some());
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, &first_event_id)
|
||||
.await
|
||||
.expect("Getting event room receipt events for first event failed")
|
||||
.is_empty());
|
||||
assert_eq!(
|
||||
store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, &second_event_id)
|
||||
.await
|
||||
.expect("Getting event room receipt events for second event failed")
|
||||
.len(),
|
||||
1,
|
||||
"Found a wrong number of receipts for second event after save"
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_content() {
|
||||
let store = get_store().await.unwrap();
|
||||
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let content: Vec<u8> = "somebinarydata".into();
|
||||
|
||||
let request_file = MediaRequest {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::File,
|
||||
};
|
||||
|
||||
let request_thumbnail = MediaRequest {
|
||||
source: MediaSource::Plain(uri.to_owned()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
method: Method::Crop,
|
||||
width: uint!(100),
|
||||
height: uint!(100),
|
||||
}),
|
||||
};
|
||||
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_none(), "unexpected media found");
|
||||
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_none(), "media not found");
|
||||
|
||||
store.add_media_content(&request_file, content.clone()).await.expect("adding media failed");
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_some(), "media not found though added");
|
||||
|
||||
store.remove_media_content(&request_file).await.expect("removing media failed");
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_none(), "media still there after removing");
|
||||
|
||||
store.add_media_content(&request_file, content.clone()).await.expect("adding media again failed");
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_some(), "media not found after adding again");
|
||||
|
||||
store.add_media_content(&request_thumbnail, content.clone()).await.expect("adding thumbnail failed");
|
||||
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_some(), "thumbnail not found");
|
||||
|
||||
store.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_none(), "media wasn't removed");
|
||||
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_none(), "thumbnail wasn't removed");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_custom_storage() -> StoreResult<()> {
|
||||
let key = "my_key";
|
||||
let value = &[0, 1, 2, 3];
|
||||
let store = get_store().await?;
|
||||
|
||||
store.set_custom_value(key.as_bytes(), value.to_vec()).await?;
|
||||
|
||||
let read = store.get_custom_value(key.as_bytes()).await?;
|
||||
|
||||
assert_eq!(Some(value.as_ref()), read.as_deref());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_persist_invited_room() -> StoreResult<()> {
|
||||
let stripped_room_id = stripped_room_id();
|
||||
let inner_store = get_store().await?;
|
||||
let store = populated_store(Box::new(inner_store)).await?;
|
||||
|
||||
assert_eq!(store.get_stripped_room_infos().await?.len(), 1);
|
||||
assert!(store.get_stripped_room(stripped_room_id).is_some());
|
||||
|
||||
// populate rooom
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_room_removal() -> StoreResult<()> {
|
||||
let room_id = room_id();
|
||||
let user_id = user_id();
|
||||
let inner_store = get_store().await?;
|
||||
let stripped_room_id = stripped_room_id();
|
||||
|
||||
let store = populated_store(Box::new(inner_store)).await?;
|
||||
|
||||
store.remove_room(room_id).await?;
|
||||
|
||||
assert!(store.get_room_infos().await?.is_empty(), "room is still there");
|
||||
assert_eq!(store.get_stripped_room_infos().await?.len(), 1);
|
||||
|
||||
assert!(store.get_state_event(room_id, StateEventType::RoomName, "").await?.is_none());
|
||||
assert!(store.get_state_events(room_id, StateEventType::RoomTopic).await?.is_empty(), "still state events found");
|
||||
assert!(store.get_profile(room_id, user_id).await?.is_none());
|
||||
assert!(store.get_member_event(room_id, user_id).await?.is_none());
|
||||
assert!(store.get_user_ids(room_id).await?.is_empty(), "still user ids found");
|
||||
assert!(store.get_invited_user_ids(room_id).await?.is_empty(), "still invited user ids found");
|
||||
assert!(store.get_joined_user_ids(room_id).await?.is_empty(), "still joined users found");
|
||||
assert!(store.get_users_with_display_name(room_id, "example").await?.is_empty(), "still display names found");
|
||||
assert!(store
|
||||
.get_room_account_data_event(room_id, RoomAccountDataEventType::Tag)
|
||||
.await?
|
||||
.is_none());
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(room_id, ReceiptType::Read, user_id)
|
||||
.await?
|
||||
.is_none());
|
||||
assert!(
|
||||
store
|
||||
.get_event_room_receipt_events(room_id, ReceiptType::Read, first_receipt_event_id())
|
||||
.await?
|
||||
.is_empty(),
|
||||
"still event recepts in the store"
|
||||
);
|
||||
|
||||
store.remove_room(stripped_room_id).await?;
|
||||
|
||||
assert!(store.get_room_infos().await?.is_empty(), "still room info found");
|
||||
assert!(store.get_stripped_room_infos().await?.is_empty(), "still stripped room info found");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
async fn test_room_timeline() {
|
||||
let store = get_store().await.unwrap();
|
||||
let mut stored_events = Vec::new();
|
||||
let room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
|
||||
|
||||
// Before the first sync the timeline should be empty
|
||||
assert!(store.room_timeline(room_id).await.expect("failed to read timeline").is_none(), "TL wasn't empty");
|
||||
|
||||
// Add sync response
|
||||
let sync = SyncResponse::try_from_http_response(
|
||||
Response::builder().body(serde_json::to_vec(&*test_json::MORE_SYNC).expect("Parsing MORE_SYNC failed")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let timeline = &sync.rooms.join[room_id].timeline;
|
||||
let events: Vec<SyncRoomEvent> = timeline.events.iter().cloned().map(Into::into).collect();
|
||||
|
||||
stored_events.extend(events.iter().rev().cloned());
|
||||
|
||||
let timeline_slice = TimelineSlice::new(
|
||||
events,
|
||||
sync.next_batch.clone(),
|
||||
timeline.prev_batch.clone(),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
let mut changes = StateChanges::new(sync.next_batch.clone());
|
||||
changes.add_timeline(room_id, timeline_slice);
|
||||
store.save_changes(&changes).await.expect("Saving room timeline failed");
|
||||
|
||||
check_timeline_events(room_id, &store, &stored_events, timeline.prev_batch.as_deref())
|
||||
.await;
|
||||
|
||||
// Add message response
|
||||
let messages = MessageResponse::try_from_http_response(
|
||||
Response::builder()
|
||||
.body(serde_json::to_vec(&*test_json::SYNC_ROOM_MESSAGES_BATCH_1).expect("Parsing SYNC_ROOM_MESSAGES_BATCH_1 failed"))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let events: Vec<SyncRoomEvent> = messages
|
||||
.chunk
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|event| RoomEvent { event, encryption_info: None }.into())
|
||||
.collect();
|
||||
|
||||
stored_events.append(&mut events.clone());
|
||||
|
||||
let timeline_slice =
|
||||
TimelineSlice::new(events, messages.start.clone(), messages.end.clone(), false, false);
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_timeline(room_id, timeline_slice);
|
||||
store.save_changes(&changes).await.expect("Saving room update timeline failed");
|
||||
|
||||
check_timeline_events(room_id, &store, &stored_events, messages.end.as_deref()).await;
|
||||
|
||||
// Add second message response
|
||||
let messages = MessageResponse::try_from_http_response(
|
||||
Response::builder()
|
||||
.body(serde_json::to_vec(&*test_json::SYNC_ROOM_MESSAGES_BATCH_2).expect("Parsing SYNC_ROOM_MESSAGES_BATCH_2 failed"))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let events: Vec<SyncRoomEvent> = messages
|
||||
.chunk
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|event| RoomEvent { event, encryption_info: None }.into())
|
||||
.collect();
|
||||
|
||||
stored_events.append(&mut events.clone());
|
||||
|
||||
let timeline_slice =
|
||||
TimelineSlice::new(events, messages.start.clone(), messages.end.clone(), false, false);
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_timeline(room_id, timeline_slice);
|
||||
store.save_changes(&changes).await.expect("Saving room update timeline 2 failed");
|
||||
|
||||
check_timeline_events(room_id, &store, &stored_events, messages.end.as_deref()).await;
|
||||
|
||||
// Add second sync response
|
||||
let sync = SyncResponse::try_from_http_response(
|
||||
Response::builder()
|
||||
.body(serde_json::to_vec(&*test_json::MORE_SYNC_2).unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let timeline = &sync.rooms.join[room_id].timeline;
|
||||
let events: Vec<SyncRoomEvent> = timeline.events.iter().cloned().map(Into::into).collect();
|
||||
|
||||
let prev_stored_events = stored_events;
|
||||
stored_events = events.iter().rev().cloned().collect();
|
||||
stored_events.extend(prev_stored_events);
|
||||
|
||||
let timeline_slice = TimelineSlice::new(
|
||||
events,
|
||||
sync.next_batch.clone(),
|
||||
timeline.prev_batch.clone(),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
let mut changes = StateChanges::new(sync.next_batch.clone());
|
||||
changes.add_timeline(room_id, timeline_slice);
|
||||
store.save_changes(&changes).await.expect("Saving room update timeline 3 failed");
|
||||
|
||||
check_timeline_events(room_id, &store, &stored_events, messages.end.as_deref()).await;
|
||||
|
||||
// Check if limited sync removes the stored timeline
|
||||
let end_token = Some("end token".to_owned());
|
||||
let timeline_slice = TimelineSlice::new(
|
||||
Vec::new(),
|
||||
"start token".to_owned(),
|
||||
end_token.clone(),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_timeline(room_id, timeline_slice);
|
||||
store.save_changes(&changes).await.expect("Saving room update timeline 4 failed");
|
||||
|
||||
check_timeline_events(room_id, &store, &Vec::new(), end_token.as_deref()).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
async fn check_timeline_events(
|
||||
room_id: &RoomId,
|
||||
store: &dyn StateStore,
|
||||
stored_events: &[SyncRoomEvent],
|
||||
expected_end_token: Option<&str>,
|
||||
) {
|
||||
let (timeline_iter, end_token) = store.room_timeline(room_id).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(end_token.as_deref(), expected_end_token);
|
||||
|
||||
let timeline = timeline_iter.collect::<Vec<StoreResult<SyncRoomEvent>>>().await;
|
||||
|
||||
let expected: Vec<OwnedEventId> = stored_events.iter().map(|a| a.event_id().expect("event id doesn't exist")).collect();
|
||||
let found: Vec<OwnedEventId> = timeline.iter().map(|a| a.as_ref().expect("object missing").event_id().clone().expect("event id missing")).collect();
|
||||
|
||||
for (idx, (a, b)) in timeline
|
||||
.into_iter()
|
||||
.zip(stored_events.iter())
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!(a.expect("not a value").event_id(), b.event_id(), "pos {} not equal - expected: {:#?}, but found {:#?}", idx, expected, found);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
)*
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,887 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use async_stream::stream;
|
||||
use async_trait::async_trait;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use lru::LruCache;
|
||||
#[allow(unused_imports)]
|
||||
use matrix_sdk_common::{instant::Instant, locks::Mutex};
|
||||
use ruma::{
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
receipt::Receipt,
|
||||
room::member::{MembershipState, StrippedRoomMemberEvent, SyncRoomMemberEvent},
|
||||
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
|
||||
},
|
||||
receipt::ReceiptType,
|
||||
serde::Raw,
|
||||
EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use ruma::{
|
||||
events::{room::redaction::SyncRoomRedactionEvent, AnySyncMessageLikeEvent, AnySyncRoomEvent},
|
||||
signatures::{redact_in_place, CanonicalJsonObject},
|
||||
RoomVersionId,
|
||||
};
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use super::BoxStream;
|
||||
use super::{Result, RoomInfo, StateChanges, StateStore};
|
||||
use crate::{
|
||||
deserialized_responses::MemberEvent,
|
||||
media::{MediaRequest, UniqueKey},
|
||||
MinimalRoomMemberEvent,
|
||||
};
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use crate::{deserialized_responses::SyncRoomEvent, StoreError};
|
||||
|
||||
/// In-Memory, non-persistent implementation of the `StateStore`
|
||||
///
|
||||
/// Default if no other is configured at startup.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStore {
|
||||
sync_token: Arc<RwLock<Option<String>>>,
|
||||
filters: Arc<DashMap<String, String>>,
|
||||
account_data: Arc<DashMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>>,
|
||||
members: Arc<DashMap<OwnedRoomId, DashMap<OwnedUserId, SyncRoomMemberEvent>>>,
|
||||
profiles: Arc<DashMap<OwnedRoomId, DashMap<OwnedUserId, MinimalRoomMemberEvent>>>,
|
||||
display_names: Arc<DashMap<OwnedRoomId, DashMap<String, BTreeSet<OwnedUserId>>>>,
|
||||
joined_user_ids: Arc<DashMap<OwnedRoomId, DashSet<OwnedUserId>>>,
|
||||
invited_user_ids: Arc<DashMap<OwnedRoomId, DashSet<OwnedUserId>>>,
|
||||
room_info: Arc<DashMap<OwnedRoomId, RoomInfo>>,
|
||||
room_state:
|
||||
Arc<DashMap<OwnedRoomId, DashMap<StateEventType, DashMap<String, Raw<AnySyncStateEvent>>>>>,
|
||||
room_account_data:
|
||||
Arc<DashMap<OwnedRoomId, DashMap<RoomAccountDataEventType, Raw<AnyRoomAccountDataEvent>>>>,
|
||||
stripped_room_infos: Arc<DashMap<OwnedRoomId, RoomInfo>>,
|
||||
stripped_room_state: Arc<
|
||||
DashMap<OwnedRoomId, DashMap<StateEventType, DashMap<String, Raw<AnyStrippedStateEvent>>>>,
|
||||
>,
|
||||
stripped_members: Arc<DashMap<OwnedRoomId, DashMap<OwnedUserId, StrippedRoomMemberEvent>>>,
|
||||
stripped_joined_user_ids: Arc<DashMap<OwnedRoomId, DashSet<OwnedUserId>>>,
|
||||
stripped_invited_user_ids: Arc<DashMap<OwnedRoomId, DashSet<OwnedUserId>>>,
|
||||
presence: Arc<DashMap<OwnedUserId, Raw<PresenceEvent>>>,
|
||||
room_user_receipts:
|
||||
Arc<DashMap<OwnedRoomId, DashMap<String, DashMap<OwnedUserId, (OwnedEventId, Receipt)>>>>,
|
||||
room_event_receipts: Arc<
|
||||
DashMap<OwnedRoomId, DashMap<String, DashMap<OwnedEventId, DashMap<OwnedUserId, Receipt>>>>,
|
||||
>,
|
||||
media: Arc<Mutex<LruCache<String, Vec<u8>>>>,
|
||||
custom: Arc<DashMap<Vec<u8>, Vec<u8>>>,
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
room_timeline: Arc<DashMap<OwnedRoomId, TimelineData>>,
|
||||
}
|
||||
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
#[allow(dead_code)]
|
||||
/// Create a new empty MemoryStore
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sync_token: Default::default(),
|
||||
filters: Default::default(),
|
||||
account_data: Default::default(),
|
||||
members: Default::default(),
|
||||
profiles: Default::default(),
|
||||
display_names: Default::default(),
|
||||
joined_user_ids: Default::default(),
|
||||
invited_user_ids: Default::default(),
|
||||
room_info: Default::default(),
|
||||
room_state: Default::default(),
|
||||
room_account_data: Default::default(),
|
||||
stripped_room_infos: Default::default(),
|
||||
stripped_room_state: Default::default(),
|
||||
stripped_members: Default::default(),
|
||||
stripped_joined_user_ids: Default::default(),
|
||||
stripped_invited_user_ids: Default::default(),
|
||||
presence: Default::default(),
|
||||
room_user_receipts: Default::default(),
|
||||
room_event_receipts: Default::default(),
|
||||
media: Arc::new(Mutex::new(LruCache::new(100))),
|
||||
custom: DashMap::new().into(),
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
room_timeline: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> {
|
||||
self.filters.insert(filter_name.to_owned(), filter_id.to_owned());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_filter(&self, filter_name: &str) -> Result<Option<String>> {
|
||||
Ok(self.filters.get(filter_name).map(|f| f.to_string()))
|
||||
}
|
||||
|
||||
async fn get_sync_token(&self) -> Result<Option<String>> {
|
||||
Ok(self.sync_token.read().unwrap().clone())
|
||||
}
|
||||
|
||||
async fn save_changes(&self, changes: &StateChanges) -> Result<()> {
|
||||
let now = Instant::now();
|
||||
|
||||
if let Some(s) = &changes.sync_token {
|
||||
*self.sync_token.write().unwrap() = Some(s.to_owned());
|
||||
}
|
||||
|
||||
for (room, events) in &changes.members {
|
||||
for event in events.values() {
|
||||
match event.membership() {
|
||||
MembershipState::Join => {
|
||||
self.joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(event.state_key().to_owned());
|
||||
self.invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.remove(event.state_key());
|
||||
}
|
||||
MembershipState::Invite => {
|
||||
self.invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(event.state_key().to_owned());
|
||||
self.joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.remove(event.state_key());
|
||||
}
|
||||
_ => {
|
||||
self.joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.remove(event.state_key());
|
||||
self.invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.remove(event.state_key());
|
||||
}
|
||||
}
|
||||
|
||||
self.members
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(event.state_key().to_owned(), event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (room, users) in &changes.profiles {
|
||||
for (user_id, profile) in users {
|
||||
self.profiles
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(user_id.clone(), profile.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (room, map) in &changes.ambiguity_maps {
|
||||
for (display_name, display_names) in map {
|
||||
self.display_names
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(display_name.clone(), display_names.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (event_type, event) in &changes.account_data {
|
||||
self.account_data.insert(event_type.clone(), event.clone());
|
||||
}
|
||||
|
||||
for (room, events) in &changes.room_account_data {
|
||||
for (event_type, event) in events {
|
||||
self.room_account_data
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(event_type.clone(), event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (room, event_types) in &changes.state {
|
||||
for (event_type, events) in event_types {
|
||||
for (state_key, event) in events {
|
||||
self.room_state
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.entry(event_type.clone())
|
||||
.or_default()
|
||||
.insert(state_key.to_owned(), event.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &changes.room_infos {
|
||||
self.room_info.insert(room_id.clone(), room_info.clone());
|
||||
}
|
||||
|
||||
for (sender, event) in &changes.presence {
|
||||
self.presence.insert(sender.clone(), event.clone());
|
||||
}
|
||||
|
||||
for (room_id, info) in &changes.stripped_room_infos {
|
||||
self.stripped_room_infos.insert(room_id.clone(), info.clone());
|
||||
}
|
||||
|
||||
for (room, events) in &changes.stripped_members {
|
||||
for event in events.values() {
|
||||
match event.content.membership {
|
||||
MembershipState::Join => {
|
||||
self.stripped_joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(event.state_key.clone());
|
||||
self.stripped_invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
}
|
||||
MembershipState::Invite => {
|
||||
self.stripped_invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(event.state_key.clone());
|
||||
self.stripped_joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
}
|
||||
_ => {
|
||||
self.stripped_joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
self.stripped_invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
}
|
||||
}
|
||||
|
||||
self.stripped_members
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.insert(event.state_key.clone(), event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (room, event_types) in &changes.stripped_state {
|
||||
for (event_type, events) in event_types {
|
||||
for (state_key, event) in events {
|
||||
self.stripped_room_state
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.entry(event_type.clone())
|
||||
.or_default()
|
||||
.insert(state_key.to_owned(), event.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room, content) in &changes.receipts {
|
||||
for (event_id, receipts) in &content.0 {
|
||||
for (receipt_type, receipts) in receipts {
|
||||
for (user_id, receipt) in receipts {
|
||||
// Add the receipt to the room user receipts
|
||||
if let Some((old_event, _)) = self
|
||||
.room_user_receipts
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.entry(receipt_type.to_string())
|
||||
.or_default()
|
||||
.insert(user_id.clone(), (event_id.clone(), receipt.clone()))
|
||||
{
|
||||
// Remove the old receipt from the room event receipts
|
||||
if let Some(receipt_map) = self.room_event_receipts.get(room) {
|
||||
if let Some(event_map) = receipt_map.get(receipt_type.as_ref()) {
|
||||
if let Some(user_map) = event_map.get_mut(&old_event) {
|
||||
user_map.remove(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the receipt to the room event receipts
|
||||
self.room_event_receipts
|
||||
.entry(room.clone())
|
||||
.or_default()
|
||||
.entry(receipt_type.to_string())
|
||||
.or_default()
|
||||
.entry(event_id.clone())
|
||||
.or_default()
|
||||
.insert(user_id.clone(), receipt.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
for (room, timeline) in &changes.timeline {
|
||||
if timeline.sync {
|
||||
tracing::info!("Save new timeline batch from sync response for {}", room);
|
||||
} else {
|
||||
tracing::info!("Save new timeline batch from messages response for {}", room);
|
||||
}
|
||||
|
||||
let mut delete_timeline = false;
|
||||
if timeline.limited {
|
||||
tracing::info!(
|
||||
"Delete stored timeline for {} because the sync response was limited",
|
||||
room
|
||||
);
|
||||
delete_timeline = true;
|
||||
} else if let Some(mut data) = self.room_timeline.get_mut(room) {
|
||||
if !timeline.sync && Some(&timeline.start) != data.end.as_ref() {
|
||||
// This should only happen when a developer adds a wrong timeline
|
||||
// batch to the `StateChanges` or the server returns a wrong response
|
||||
// to our request.
|
||||
tracing::warn!("Drop unexpected timeline batch for {}", room);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if the event already exists in the store
|
||||
for event in &timeline.events {
|
||||
if let Some(event_id) = event.event_id() {
|
||||
if data.event_id_to_position.contains_key(&event_id) {
|
||||
delete_timeline = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !delete_timeline {
|
||||
if timeline.sync {
|
||||
data.start = timeline.start.clone();
|
||||
} else {
|
||||
data.end = timeline.end.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if delete_timeline {
|
||||
tracing::info!("Delete stored timeline for {} because of duplicated events", room);
|
||||
self.room_timeline.remove(room);
|
||||
}
|
||||
|
||||
let mut data =
|
||||
self.room_timeline.entry(room.to_owned()).or_insert_with(|| TimelineData {
|
||||
start: timeline.start.clone(),
|
||||
end: timeline.end.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let make_room_version = || {
|
||||
self.room_info
|
||||
.get(room)
|
||||
.and_then(|info| info.room_version().cloned())
|
||||
.unwrap_or_else(|| {
|
||||
tracing::warn!(
|
||||
"Unable to find the room version for {}, assume version 9",
|
||||
room
|
||||
);
|
||||
RoomVersionId::V9
|
||||
})
|
||||
};
|
||||
|
||||
if timeline.sync {
|
||||
let mut room_version = None;
|
||||
for event in &timeline.events {
|
||||
// Redact events already in store only on sync response
|
||||
if let Ok(AnySyncRoomEvent::MessageLike(
|
||||
AnySyncMessageLikeEvent::RoomRedaction(SyncRoomRedactionEvent::Original(
|
||||
redaction,
|
||||
)),
|
||||
)) = event.event.deserialize()
|
||||
{
|
||||
let pos = data.event_id_to_position.get(&redaction.redacts).copied();
|
||||
|
||||
if let Some(position) = pos {
|
||||
if let Some(mut full_event) = data.events.get_mut(&position.clone()) {
|
||||
let mut event_json: CanonicalJsonObject =
|
||||
full_event.event.deserialize_as()?;
|
||||
let v = room_version.get_or_insert_with(make_room_version);
|
||||
|
||||
redact_in_place(&mut event_json, v)
|
||||
.map_err(StoreError::Redaction)?;
|
||||
full_event.event = Raw::new(&event_json)?.cast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.start_position -= 1;
|
||||
let start_position = data.start_position;
|
||||
// Only add event with id to the position map
|
||||
if let Some(event_id) = event.event_id() {
|
||||
data.event_id_to_position.insert(event_id, start_position);
|
||||
}
|
||||
data.events.insert(start_position, event.clone());
|
||||
}
|
||||
} else {
|
||||
for event in &timeline.events {
|
||||
data.end_position += 1;
|
||||
let end_position = data.end_position;
|
||||
// Only add event with id to the position map
|
||||
if let Some(event_id) = event.event_id() {
|
||||
data.event_id_to_position.insert(event_id, end_position);
|
||||
}
|
||||
data.events.insert(end_position, event.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Saved changes in {:?}", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>> {
|
||||
Ok(self.presence.get(user_id).map(|p| p.clone()))
|
||||
}
|
||||
|
||||
async fn get_state_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: StateEventType,
|
||||
state_key: &str,
|
||||
) -> Result<Option<Raw<AnySyncStateEvent>>> {
|
||||
Ok(self
|
||||
.room_state
|
||||
.get(room_id)
|
||||
.and_then(|e| e.get(&event_type).and_then(|s| s.get(state_key).map(|e| e.clone()))))
|
||||
}
|
||||
|
||||
async fn get_state_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: StateEventType,
|
||||
) -> Result<Vec<Raw<AnySyncStateEvent>>> {
|
||||
Ok(self
|
||||
.room_state
|
||||
.get(room_id)
|
||||
.and_then(|e| {
|
||||
e.get(&event_type).map(|s| s.iter().map(|e| e.clone()).collect::<Vec<_>>())
|
||||
})
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn get_profile(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<MinimalRoomMemberEvent>> {
|
||||
Ok(self.profiles.get(room_id).and_then(|p| p.get(user_id).map(|p| p.clone())))
|
||||
}
|
||||
|
||||
async fn get_member_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
state_key: &UserId,
|
||||
) -> Result<Option<MemberEvent>> {
|
||||
if let Some(e) = self.members.get(room_id).and_then(|m| m.get(state_key).map(|m| m.clone()))
|
||||
{
|
||||
Ok(Some(MemberEvent::Sync(e)))
|
||||
} else if let Some(e) =
|
||||
self.stripped_members.get(room_id).and_then(|m| m.get(state_key).map(|m| m.clone()))
|
||||
{
|
||||
Ok(Some(MemberEvent::Stripped(e)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_user_ids(&self, room_id: &RoomId) -> Vec<OwnedUserId> {
|
||||
if let Some(u) = self.members.get(room_id) {
|
||||
u.iter().map(|u| u.key().clone()).collect()
|
||||
} else {
|
||||
self.stripped_members
|
||||
.get(room_id)
|
||||
.map(|u| u.iter().map(|u| u.key().clone()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_invited_user_ids(&self, room_id: &RoomId) -> Vec<OwnedUserId> {
|
||||
self.invited_user_ids
|
||||
.get(room_id)
|
||||
.map(|u| u.iter().map(|u| u.clone()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_joined_user_ids(&self, room_id: &RoomId) -> Vec<OwnedUserId> {
|
||||
self.joined_user_ids
|
||||
.get(room_id)
|
||||
.map(|u| u.iter().map(|u| u.clone()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_stripped_invited_user_ids(&self, room_id: &RoomId) -> Vec<OwnedUserId> {
|
||||
self.stripped_invited_user_ids
|
||||
.get(room_id)
|
||||
.map(|u| u.iter().map(|u| u.clone()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_stripped_joined_user_ids(&self, room_id: &RoomId) -> Vec<OwnedUserId> {
|
||||
self.stripped_joined_user_ids
|
||||
.get(room_id)
|
||||
.map(|u| u.iter().map(|u| u.clone()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_room_infos(&self) -> Vec<RoomInfo> {
|
||||
self.room_info.iter().map(|r| r.clone()).collect()
|
||||
}
|
||||
|
||||
fn get_stripped_room_infos(&self) -> Vec<RoomInfo> {
|
||||
self.stripped_room_infos.iter().map(|r| r.clone()).collect()
|
||||
}
|
||||
|
||||
async fn get_account_data_event(
|
||||
&self,
|
||||
event_type: GlobalAccountDataEventType,
|
||||
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
|
||||
Ok(self.account_data.get(&event_type).map(|e| e.clone()))
|
||||
}
|
||||
|
||||
async fn get_room_account_data_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: RoomAccountDataEventType,
|
||||
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
|
||||
Ok(self.room_account_data.get(room_id).and_then(|m| m.get(&event_type).map(|e| e.clone())))
|
||||
}
|
||||
|
||||
async fn get_user_room_receipt_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<(OwnedEventId, Receipt)>> {
|
||||
Ok(self.room_user_receipts.get(room_id).and_then(|m| {
|
||||
m.get(receipt_type.as_ref()).and_then(|m| m.get(user_id).map(|r| r.clone()))
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_event_room_receipt_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
event_id: &EventId,
|
||||
) -> Result<Vec<(OwnedUserId, Receipt)>> {
|
||||
Ok(self
|
||||
.room_event_receipts
|
||||
.get(room_id)
|
||||
.and_then(|m| {
|
||||
m.get(receipt_type.as_ref()).and_then(|m| {
|
||||
m.get(event_id)
|
||||
.map(|m| m.iter().map(|r| (r.key().clone(), r.value().clone())).collect())
|
||||
})
|
||||
})
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
|
||||
Ok(self.custom.get(key).map(|e| e.value().clone()))
|
||||
}
|
||||
|
||||
async fn set_custom_value(&self, key: &[u8], value: Vec<u8>) -> Result<Option<Vec<u8>>> {
|
||||
Ok(self.custom.insert(key.to_vec(), value))
|
||||
}
|
||||
|
||||
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
|
||||
self.media.lock().await.put(request.unique_key(), data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
|
||||
Ok(self.media.lock().await.get(&request.unique_key()).cloned())
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
|
||||
self.media.lock().await.pop(&request.unique_key());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
let mut media_store = self.media.lock().await;
|
||||
|
||||
let keys: Vec<String> = media_store
|
||||
.iter()
|
||||
.filter_map(
|
||||
|(key, _)| if key.starts_with(&uri.to_string()) { Some(key.clone()) } else { None },
|
||||
)
|
||||
.collect();
|
||||
|
||||
for key in keys {
|
||||
media_store.pop(&key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<()> {
|
||||
self.members.remove(room_id);
|
||||
self.profiles.remove(room_id);
|
||||
self.display_names.remove(room_id);
|
||||
self.joined_user_ids.remove(room_id);
|
||||
self.invited_user_ids.remove(room_id);
|
||||
self.room_info.remove(room_id);
|
||||
self.room_state.remove(room_id);
|
||||
self.room_account_data.remove(room_id);
|
||||
self.stripped_room_infos.remove(room_id);
|
||||
self.stripped_room_state.remove(room_id);
|
||||
self.stripped_members.remove(room_id);
|
||||
self.room_user_receipts.remove(room_id);
|
||||
self.room_event_receipts.remove(room_id);
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
self.room_timeline.remove(room_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
async fn room_timeline(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<(BoxStream<Result<SyncRoomEvent>>, Option<String>)>> {
|
||||
let (events, end_token) = if let Some(data) = self.room_timeline.get(room_id) {
|
||||
(data.events.clone(), data.end.clone())
|
||||
} else {
|
||||
tracing::info!("No timeline for {} was previously stored", room_id);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let stream = stream! {
|
||||
for (_, item) in events {
|
||||
yield Ok(item);
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"Found previously stored timeline for {}, with end token {:?}",
|
||||
room_id,
|
||||
end_token
|
||||
);
|
||||
|
||||
Ok(Some((Box::pin(stream), end_token)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl StateStore for MemoryStore {
|
||||
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> {
|
||||
self.save_filter(filter_name, filter_id).await
|
||||
}
|
||||
|
||||
async fn save_changes(&self, changes: &StateChanges) -> Result<()> {
|
||||
self.save_changes(changes).await
|
||||
}
|
||||
|
||||
async fn get_filter(&self, filter_id: &str) -> Result<Option<String>> {
|
||||
self.get_filter(filter_id).await
|
||||
}
|
||||
|
||||
async fn get_sync_token(&self) -> Result<Option<String>> {
|
||||
self.get_sync_token().await
|
||||
}
|
||||
|
||||
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>> {
|
||||
self.get_presence_event(user_id).await
|
||||
}
|
||||
|
||||
async fn get_state_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: StateEventType,
|
||||
state_key: &str,
|
||||
) -> Result<Option<Raw<AnySyncStateEvent>>> {
|
||||
self.get_state_event(room_id, event_type, state_key).await
|
||||
}
|
||||
|
||||
async fn get_state_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: StateEventType,
|
||||
) -> Result<Vec<Raw<AnySyncStateEvent>>> {
|
||||
self.get_state_events(room_id, event_type).await
|
||||
}
|
||||
|
||||
async fn get_profile(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<MinimalRoomMemberEvent>> {
|
||||
self.get_profile(room_id, user_id).await
|
||||
}
|
||||
|
||||
async fn get_member_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
state_key: &UserId,
|
||||
) -> Result<Option<MemberEvent>> {
|
||||
self.get_member_event(room_id, state_key).await
|
||||
}
|
||||
|
||||
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>> {
|
||||
Ok(self.get_user_ids(room_id))
|
||||
}
|
||||
|
||||
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>> {
|
||||
let v = self.get_invited_user_ids(room_id);
|
||||
if !v.is_empty() {
|
||||
return Ok(v);
|
||||
}
|
||||
Ok(self.get_stripped_invited_user_ids(room_id))
|
||||
}
|
||||
|
||||
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>> {
|
||||
let v = self.get_joined_user_ids(room_id);
|
||||
if !v.is_empty() {
|
||||
return Ok(v);
|
||||
}
|
||||
Ok(self.get_stripped_joined_user_ids(room_id))
|
||||
}
|
||||
|
||||
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>> {
|
||||
Ok(self.get_room_infos())
|
||||
}
|
||||
|
||||
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>> {
|
||||
Ok(self.get_stripped_room_infos())
|
||||
}
|
||||
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
) -> Result<BTreeSet<OwnedUserId>> {
|
||||
Ok(self
|
||||
.display_names
|
||||
.get(room_id)
|
||||
.and_then(|d| d.get(display_name).map(|d| d.clone()))
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn get_account_data_event(
|
||||
&self,
|
||||
event_type: GlobalAccountDataEventType,
|
||||
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
|
||||
self.get_account_data_event(event_type).await
|
||||
}
|
||||
|
||||
async fn get_room_account_data_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: RoomAccountDataEventType,
|
||||
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
|
||||
self.get_room_account_data_event(room_id, event_type).await
|
||||
}
|
||||
|
||||
async fn get_user_room_receipt_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<(OwnedEventId, Receipt)>> {
|
||||
self.get_user_room_receipt_event(room_id, receipt_type, user_id).await
|
||||
}
|
||||
|
||||
async fn get_event_room_receipt_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
event_id: &EventId,
|
||||
) -> Result<Vec<(OwnedUserId, Receipt)>> {
|
||||
self.get_event_room_receipt_events(room_id, receipt_type, event_id).await
|
||||
}
|
||||
|
||||
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
|
||||
self.get_custom_value(key).await
|
||||
}
|
||||
|
||||
async fn set_custom_value(&self, key: &[u8], value: Vec<u8>) -> Result<Option<Vec<u8>>> {
|
||||
self.set_custom_value(key, value).await
|
||||
}
|
||||
|
||||
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
|
||||
self.add_media_content(request, data).await
|
||||
}
|
||||
|
||||
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
|
||||
self.get_media_content(request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
|
||||
self.remove_media_content(request).await
|
||||
}
|
||||
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
|
||||
self.remove_media_content_for_uri(uri).await
|
||||
}
|
||||
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<()> {
|
||||
self.remove_room(room_id).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
async fn room_timeline(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<(BoxStream<Result<SyncRoomEvent>>, Option<String>)>> {
|
||||
self.room_timeline(room_id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
struct TimelineData {
|
||||
pub start: String,
|
||||
pub start_position: isize,
|
||||
pub end: Option<String>,
|
||||
pub end_position: isize,
|
||||
pub events: BTreeMap<isize, SyncRoomEvent>,
|
||||
pub event_id_to_position: HashMap<OwnedEventId, isize>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::{MemoryStore, Result, StateStore};
|
||||
|
||||
async fn get_store() -> Result<impl StateStore> {
|
||||
Ok(MemoryStore::new())
|
||||
}
|
||||
|
||||
statestore_integration_tests! { integration }
|
||||
}
|
||||
@@ -12,63 +12,77 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
use std::path::Path;
|
||||
//! The state store holds the overall state for rooms, users and their
|
||||
//! profiles and their timelines. It is an overall cache for faster access
|
||||
//! and convenience- accessible through `Store`.
|
||||
//!
|
||||
//! Implementing the `StateStore` trait, you can plug any storage backend
|
||||
//! into the store for the actual storage. By default this brings an in-memory
|
||||
//! store.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
ops::Deref,
|
||||
pin::Pin,
|
||||
result::Result as StdResult,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[macro_use]
|
||||
pub mod integration_tests;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use dashmap::DashMap;
|
||||
use matrix_sdk_common::{async_trait, locks::RwLock, AsyncTraitDeps};
|
||||
use matrix_sdk_common::{locks::RwLock, AsyncTraitDeps};
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
use matrix_sdk_crypto::store::CryptoStore;
|
||||
use ruma::{
|
||||
api::client::r0::push::get_notifications::Notification,
|
||||
api::client::push::get_notifications::v3::Notification,
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
receipt::{Receipt, ReceiptEventContent},
|
||||
room::member::MemberEventContent,
|
||||
room::member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
|
||||
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
AnySyncStateEvent, EventContent, EventType,
|
||||
AnySyncStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
|
||||
},
|
||||
receipt::ReceiptType,
|
||||
serde::Raw,
|
||||
EventId, MxcUri, RoomId, UserId,
|
||||
EventId, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
use sled::Db;
|
||||
|
||||
/// BoxStream of owned Types
|
||||
pub type BoxStream<T> = Pin<Box<dyn futures_util::Stream<Item = T> + Send>>;
|
||||
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
use crate::deserialized_responses::{SyncRoomEvent, TimelineSlice};
|
||||
use crate::{
|
||||
deserialized_responses::{MemberEvent, StrippedMemberEvent},
|
||||
deserialized_responses::MemberEvent,
|
||||
media::MediaRequest,
|
||||
rooms::{RoomInfo, RoomType},
|
||||
Room, Session,
|
||||
MinimalRoomMemberEvent, Room, Session,
|
||||
};
|
||||
|
||||
pub(crate) mod ambiguity_map;
|
||||
mod memory_store;
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
mod sled_store;
|
||||
|
||||
#[cfg(not(feature = "sled_state_store"))]
|
||||
use self::memory_store::MemoryStore;
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
use self::sled_store::SledStore;
|
||||
pub use self::memory_store::MemoryStore;
|
||||
|
||||
/// State store specific error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StoreError {
|
||||
/// An error happened in the underlying sled database.
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
/// An error happened in the underlying database backend.
|
||||
#[error(transparent)]
|
||||
Sled(#[from] sled::Error),
|
||||
Backend(Box<dyn std::error::Error + Send + Sync>),
|
||||
/// An error happened while serializing or deserializing some data.
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
/// An error happened while deserializing a Matrix identifier, e.g. an user
|
||||
/// id.
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] ruma::identifiers::Error),
|
||||
Identifier(#[from] ruma::IdParseError),
|
||||
/// The store is locked with a passphrase and an incorrect passphrase was
|
||||
/// given.
|
||||
#[error("The store failed to be unlocked")]
|
||||
@@ -79,6 +93,34 @@ pub enum StoreError {
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the store: {0}")]
|
||||
Encryption(String),
|
||||
/// The store failed to encode or decode some data.
|
||||
#[error("Error encoding or decoding data from the store: {0}")]
|
||||
Codec(String),
|
||||
|
||||
/// The database format has changed in a backwards incompatible way.
|
||||
#[error(
|
||||
"The database format changed in an incompatible way, current \
|
||||
version: {0}, latest version: {1}"
|
||||
)]
|
||||
UnsupportedDatabaseVersion(usize, usize),
|
||||
/// Redacting an event in the store has failed.
|
||||
///
|
||||
/// This should never happen.
|
||||
#[error("Redaction failed: {0}")]
|
||||
Redaction(#[source] ruma::signatures::Error),
|
||||
}
|
||||
|
||||
impl StoreError {
|
||||
/// Create a new [`Backend`][Self::Backend] error.
|
||||
///
|
||||
/// Shorthand for `StoreError::Backend(Box::new(error))`.
|
||||
#[inline]
|
||||
pub fn backend<E>(error: E) -> Self
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
Self::Backend(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// A `StateStore` specific result type.
|
||||
@@ -129,10 +171,23 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
async fn get_state_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: EventType,
|
||||
event_type: StateEventType,
|
||||
state_key: &str,
|
||||
) -> Result<Option<Raw<AnySyncStateEvent>>>;
|
||||
|
||||
/// Get a list of state events for a given room and `StateEventType`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room to find events for.
|
||||
///
|
||||
/// * `event_type` - The event type.
|
||||
async fn get_state_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: StateEventType,
|
||||
) -> Result<Vec<Raw<AnySyncStateEvent>>>;
|
||||
|
||||
/// Get the current profile for the given user in the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -144,9 +199,9 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<MemberEventContent>>;
|
||||
) -> Result<Option<MinimalRoomMemberEvent>>;
|
||||
|
||||
/// Get a raw `MemberEvent` for the given state key in the given room id.
|
||||
/// Get the `MemberEvent` for the given state key in the given room id.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -159,16 +214,17 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
state_key: &UserId,
|
||||
) -> Result<Option<MemberEvent>>;
|
||||
|
||||
/// Get all the user ids of members for a given room.
|
||||
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>>;
|
||||
/// Get all the user ids of members for a given room, for stripped and
|
||||
/// regular rooms alike.
|
||||
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>>;
|
||||
|
||||
/// Get all the user ids of members that are in the invited state for a
|
||||
/// given room.
|
||||
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>>;
|
||||
/// given room, for stripped and regular rooms alike.
|
||||
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>>;
|
||||
|
||||
/// Get all the user ids of members that are in the joined state for a
|
||||
/// given room.
|
||||
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>>;
|
||||
/// given room, for stripped and regular rooms alike.
|
||||
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<OwnedUserId>>;
|
||||
|
||||
/// Get all the pure `RoomInfo`s the store knows about.
|
||||
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>>;
|
||||
@@ -188,7 +244,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
) -> Result<BTreeSet<UserId>>;
|
||||
) -> Result<BTreeSet<OwnedUserId>>;
|
||||
|
||||
/// Get an event out of the account data store.
|
||||
///
|
||||
@@ -197,7 +253,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
/// * `event_type` - The event type of the account data event.
|
||||
async fn get_account_data_event(
|
||||
&self,
|
||||
event_type: EventType,
|
||||
event_type: GlobalAccountDataEventType,
|
||||
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>>;
|
||||
|
||||
/// Get an event out of the room account data store.
|
||||
@@ -212,7 +268,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
async fn get_room_account_data_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: EventType,
|
||||
event_type: RoomAccountDataEventType,
|
||||
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>>;
|
||||
|
||||
/// Get an event out of the user room receipt store.
|
||||
@@ -230,7 +286,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<(EventId, Receipt)>>;
|
||||
) -> Result<Option<(OwnedEventId, Receipt)>>;
|
||||
|
||||
/// Get events out of the event room receipt store.
|
||||
///
|
||||
@@ -248,7 +304,23 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
event_id: &EventId,
|
||||
) -> Result<Vec<(UserId, Receipt)>>;
|
||||
) -> Result<Vec<(OwnedUserId, Receipt)>>;
|
||||
|
||||
/// Get arbitrary data from the custom store
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to fetch data for
|
||||
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>>;
|
||||
|
||||
/// Put arbitrary data into the custom store
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to insert data into
|
||||
///
|
||||
/// * `value` - The value to insert
|
||||
async fn set_custom_value(&self, key: &[u8], value: Vec<u8>) -> Result<Option<Vec<u8>>>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
@@ -280,6 +352,27 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()>;
|
||||
|
||||
/// Removes a room and all elements associated from the state store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The `RoomId` of the room to delete.
|
||||
async fn remove_room(&self, room_id: &RoomId) -> Result<()>;
|
||||
|
||||
/// Get a stream of the stored timeline
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The `RoomId` of the room to delete.
|
||||
///
|
||||
/// Returns a stream of events and a token that can be used to request
|
||||
/// previous events.
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
async fn room_timeline(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
) -> Result<Option<(BoxStream<Result<SyncRoomEvent>>, Option<String>)>>;
|
||||
}
|
||||
|
||||
/// A state store wrapper for the SDK.
|
||||
@@ -289,27 +382,37 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Store {
|
||||
inner: Arc<dyn StateStore>,
|
||||
pub(crate) session: Arc<RwLock<Option<Session>>>,
|
||||
pub(crate) sync_token: Arc<RwLock<Option<String>>>,
|
||||
rooms: Arc<DashMap<RoomId, Room>>,
|
||||
stripped_rooms: Arc<DashMap<RoomId, Room>>,
|
||||
session: Arc<OnceCell<Session>>,
|
||||
/// The current sync token that should be used for the next sync call.
|
||||
pub(super) sync_token: Arc<RwLock<Option<String>>>,
|
||||
rooms: Arc<DashMap<OwnedRoomId, Room>>,
|
||||
stripped_rooms: Arc<DashMap<OwnedRoomId, Room>>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
fn new(inner: Box<dyn StateStore>) -> Self {
|
||||
let session = Arc::new(RwLock::new(None));
|
||||
let sync_token = Arc::new(RwLock::new(None));
|
||||
/// Create a new Store with the default `MemoryStore`
|
||||
pub fn open_memory_store() -> Self {
|
||||
let inner = Box::new(MemoryStore::new());
|
||||
|
||||
Self::new(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Create a new store, wrappning the given `StateStore`
|
||||
pub fn new(inner: Box<dyn StateStore>) -> Self {
|
||||
Self {
|
||||
inner: inner.into(),
|
||||
session,
|
||||
sync_token,
|
||||
rooms: DashMap::new().into(),
|
||||
stripped_rooms: DashMap::new().into(),
|
||||
session: Default::default(),
|
||||
sync_token: Default::default(),
|
||||
rooms: Default::default(),
|
||||
stripped_rooms: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn restore_session(&self, session: Session) -> Result<()> {
|
||||
/// Restore the access to the Store from the given `Session`, overwrites any
|
||||
/// previously existing access to the Store.
|
||||
pub async fn restore_session(&self, session: Session) -> Result<()> {
|
||||
for info in self.inner.get_room_infos().await? {
|
||||
let room = Room::restore(&session.user_id, self.inner.clone(), info);
|
||||
self.rooms.insert(room.room_id().to_owned(), room);
|
||||
@@ -323,42 +426,15 @@ impl Store {
|
||||
let token = self.get_sync_token().await?;
|
||||
|
||||
*self.sync_token.write().await = token;
|
||||
*self.session.write().await = Some(session);
|
||||
self.session.set(session).expect("A session was already set");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sled_state_store"))]
|
||||
pub(crate) fn open_memory_store() -> Self {
|
||||
let inner = Box::new(MemoryStore::new());
|
||||
|
||||
Self::new(inner)
|
||||
}
|
||||
|
||||
/// Open the default Sled store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - The path where the store should reside in.
|
||||
///
|
||||
/// * `passphrase` - A passphrase that should be used to encrypt the state
|
||||
/// store.
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
pub fn open_default(path: impl AsRef<Path>, passphrase: Option<&str>) -> Result<(Self, Db)> {
|
||||
let inner = if let Some(passphrase) = passphrase {
|
||||
SledStore::open_with_passphrase(path, passphrase)?
|
||||
} else {
|
||||
SledStore::open_with_path(path)?
|
||||
};
|
||||
|
||||
Ok((Self::new(Box::new(inner.clone())), inner.inner))
|
||||
}
|
||||
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
pub(crate) fn open_temporary() -> Result<(Self, Db)> {
|
||||
let inner = SledStore::open()?;
|
||||
|
||||
Ok((Self::new(Box::new(inner.clone())), inner.inner))
|
||||
/// The current [`Session`] containing our user id, device id and access
|
||||
/// token.
|
||||
pub fn session(&self) -> Option<&Session> {
|
||||
self.session.get()
|
||||
}
|
||||
|
||||
/// Get all the rooms this store knows about.
|
||||
@@ -378,26 +454,38 @@ impl Store {
|
||||
.or_else(|| self.get_stripped_room(room_id))
|
||||
}
|
||||
|
||||
fn get_stripped_room(&self, room_id: &RoomId) -> Option<Room> {
|
||||
/// Get all the rooms this store knows about.
|
||||
pub fn get_stripped_rooms(&self) -> Vec<Room> {
|
||||
self.stripped_rooms.iter().filter_map(|r| self.get_stripped_room(r.key())).collect()
|
||||
}
|
||||
|
||||
/// Get the stripped room with the given room id.
|
||||
pub fn get_stripped_room(&self, room_id: &RoomId) -> Option<Room> {
|
||||
self.stripped_rooms.get(room_id).map(|r| r.clone())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_or_create_stripped_room(&self, room_id: &RoomId) -> Room {
|
||||
let session = self.session.read().await;
|
||||
let user_id = &session.as_ref().expect("Creating room while not being logged in").user_id;
|
||||
/// Lookup the stripped Room for the given RoomId, or create one, if it
|
||||
/// didn't exist yet in the store
|
||||
pub async fn get_or_create_stripped_room(&self, room_id: &RoomId) -> Room {
|
||||
let user_id = &self.session.get().expect("Creating room while not being logged in").user_id;
|
||||
|
||||
self.stripped_rooms
|
||||
.entry(room_id.clone())
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, RoomType::Invited))
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomType) -> Room {
|
||||
let session = self.session.read().await;
|
||||
let user_id = &session.as_ref().expect("Creating room while not being logged in").user_id;
|
||||
/// Lookup the Room for the given RoomId, or create one, if it didn't exist
|
||||
/// yet in the store
|
||||
pub async fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomType) -> Room {
|
||||
if room_type == RoomType::Invited {
|
||||
return self.get_or_create_stripped_room(room_id).await;
|
||||
}
|
||||
|
||||
let user_id = &self.session.get().expect("Creating room while not being logged in").user_id;
|
||||
|
||||
self.rooms
|
||||
.entry(room_id.clone())
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, room_type))
|
||||
.clone()
|
||||
}
|
||||
@@ -420,39 +508,49 @@ pub struct StateChanges {
|
||||
/// associated user account.
|
||||
pub session: Option<Session>,
|
||||
/// A mapping of event type string to `AnyBasicEvent`.
|
||||
pub account_data: BTreeMap<String, Raw<AnyGlobalAccountDataEvent>>,
|
||||
pub account_data: BTreeMap<GlobalAccountDataEventType, Raw<AnyGlobalAccountDataEvent>>,
|
||||
/// A mapping of `UserId` to `PresenceEvent`.
|
||||
pub presence: BTreeMap<UserId, Raw<PresenceEvent>>,
|
||||
pub presence: BTreeMap<OwnedUserId, Raw<PresenceEvent>>,
|
||||
|
||||
/// A mapping of `RoomId` to a map of users and their `MemberEvent`.
|
||||
pub members: BTreeMap<RoomId, BTreeMap<UserId, MemberEvent>>,
|
||||
/// A mapping of `RoomId` to a map of users and their `MemberEventContent`.
|
||||
pub profiles: BTreeMap<RoomId, BTreeMap<UserId, MemberEventContent>>,
|
||||
/// A mapping of `RoomId` to a map of users and their `SyncRoomMemberEvent`.
|
||||
pub members: BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, SyncRoomMemberEvent>>,
|
||||
/// A mapping of `RoomId` to a map of users and their
|
||||
/// `MinimalRoomMemberEvent`.
|
||||
pub profiles: BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, MinimalRoomMemberEvent>>,
|
||||
|
||||
/// A mapping of `RoomId` to a map of event type string to a state key and
|
||||
/// `AnySyncStateEvent`.
|
||||
pub state: BTreeMap<RoomId, BTreeMap<String, BTreeMap<String, Raw<AnySyncStateEvent>>>>,
|
||||
pub state:
|
||||
BTreeMap<OwnedRoomId, BTreeMap<StateEventType, BTreeMap<String, Raw<AnySyncStateEvent>>>>,
|
||||
/// A mapping of `RoomId` to a map of event type string to `AnyBasicEvent`.
|
||||
pub room_account_data: BTreeMap<RoomId, BTreeMap<String, Raw<AnyRoomAccountDataEvent>>>,
|
||||
pub room_account_data:
|
||||
BTreeMap<OwnedRoomId, BTreeMap<RoomAccountDataEventType, Raw<AnyRoomAccountDataEvent>>>,
|
||||
/// A map of `RoomId` to `RoomInfo`.
|
||||
pub room_infos: BTreeMap<RoomId, RoomInfo>,
|
||||
pub room_infos: BTreeMap<OwnedRoomId, RoomInfo>,
|
||||
/// A map of `RoomId` to `ReceiptEventContent`.
|
||||
pub receipts: BTreeMap<RoomId, ReceiptEventContent>,
|
||||
pub receipts: BTreeMap<OwnedRoomId, ReceiptEventContent>,
|
||||
|
||||
/// A mapping of `RoomId` to a map of event type to a map of state key to
|
||||
/// `AnyStrippedStateEvent`.
|
||||
pub stripped_state:
|
||||
BTreeMap<RoomId, BTreeMap<String, BTreeMap<String, Raw<AnyStrippedStateEvent>>>>,
|
||||
/// A mapping of `RoomId` to a map of users and their `StrippedMemberEvent`.
|
||||
pub stripped_members: BTreeMap<RoomId, BTreeMap<UserId, StrippedMemberEvent>>,
|
||||
/// A map of `RoomId` to `RoomInfo`.
|
||||
pub invited_room_info: BTreeMap<RoomId, RoomInfo>,
|
||||
pub stripped_state: BTreeMap<
|
||||
OwnedRoomId,
|
||||
BTreeMap<StateEventType, BTreeMap<String, Raw<AnyStrippedStateEvent>>>,
|
||||
>,
|
||||
/// A mapping of `RoomId` to a map of users and their
|
||||
/// `StrippedRoomMemberEvent`.
|
||||
pub stripped_members: BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, StrippedRoomMemberEvent>>,
|
||||
/// A map of `RoomId` to `RoomInfo` for stripped rooms (e.g. for invites or
|
||||
/// while knocking)
|
||||
pub stripped_room_infos: BTreeMap<OwnedRoomId, RoomInfo>,
|
||||
|
||||
/// A map from room id to a map of a display name and a set of user ids that
|
||||
/// share that display name in the given room.
|
||||
pub ambiguity_maps: BTreeMap<RoomId, BTreeMap<String, BTreeSet<UserId>>>,
|
||||
pub ambiguity_maps: BTreeMap<OwnedRoomId, BTreeMap<String, BTreeSet<OwnedUserId>>>,
|
||||
/// A map of `RoomId` to a vector of `Notification`s
|
||||
pub notifications: BTreeMap<RoomId, Vec<Notification>>,
|
||||
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
/// A mapping of `RoomId` to a `TimelineSlice`
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
pub timeline: BTreeMap<OwnedRoomId, TimelineSlice>,
|
||||
}
|
||||
|
||||
impl StateChanges {
|
||||
@@ -473,7 +571,7 @@ impl StateChanges {
|
||||
|
||||
/// Update the `StateChanges` struct with the given `RoomInfo`.
|
||||
pub fn add_stripped_room(&mut self, room: RoomInfo) {
|
||||
self.room_infos.insert(room.room_id.as_ref().to_owned(), room);
|
||||
self.stripped_room_infos.insert(room.room_id.as_ref().to_owned(), room);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given `AnyBasicEvent`.
|
||||
@@ -482,7 +580,7 @@ impl StateChanges {
|
||||
event: AnyGlobalAccountDataEvent,
|
||||
raw_event: Raw<AnyGlobalAccountDataEvent>,
|
||||
) {
|
||||
self.account_data.insert(event.content().event_type().to_owned(), raw_event);
|
||||
self.account_data.insert(event.event_type(), raw_event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
@@ -495,19 +593,16 @@ impl StateChanges {
|
||||
) {
|
||||
self.room_account_data
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.content().event_type().to_owned(), raw_event);
|
||||
.or_default()
|
||||
.insert(event.event_type(), raw_event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `StrippedMemberEvent`.
|
||||
pub fn add_stripped_member(&mut self, room_id: &RoomId, event: StrippedMemberEvent) {
|
||||
pub fn add_stripped_member(&mut self, room_id: &RoomId, event: StrippedRoomMemberEvent) {
|
||||
let user_id = event.state_key.clone();
|
||||
|
||||
self.stripped_members
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(user_id, event);
|
||||
self.stripped_members.entry(room_id.to_owned()).or_default().insert(user_id, event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
@@ -520,16 +615,16 @@ impl StateChanges {
|
||||
) {
|
||||
self.state
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.entry(event.content().event_type().to_string())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.state_key().to_string(), raw_event);
|
||||
.or_default()
|
||||
.entry(event.event_type())
|
||||
.or_default()
|
||||
.insert(event.state_key().to_owned(), raw_event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `Notification`.
|
||||
pub fn add_notification(&mut self, room_id: &RoomId, notification: Notification) {
|
||||
self.notifications.entry(room_id.to_owned()).or_insert_with(Vec::new).push(notification);
|
||||
self.notifications.entry(room_id.to_owned()).or_default().push(notification);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
@@ -537,4 +632,58 @@ impl StateChanges {
|
||||
pub fn add_receipts(&mut self, room_id: &RoomId, event: ReceiptEventContent) {
|
||||
self.receipts.insert(room_id.to_owned(), event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `TimelineSlice`.
|
||||
#[cfg(feature = "experimental-timeline")]
|
||||
pub fn add_timeline(&mut self, room_id: &RoomId, timeline: TimelineSlice) {
|
||||
self.timeline.insert(room_id.to_owned(), timeline);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the state store and, when `encryption` is enabled, for the
|
||||
/// crypto store.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use matrix_sdk_base::store::StoreConfig;
|
||||
///
|
||||
/// let store_config = StoreConfig::new();
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct StoreConfig {
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub(crate) crypto_store: Option<Box<dyn CryptoStore>>,
|
||||
pub(crate) state_store: Option<Box<dyn StateStore>>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for StoreConfig {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> StdResult<(), std::fmt::Error> {
|
||||
fmt.debug_struct("StoreConfig").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreConfig {
|
||||
/// Create a new default `StoreConfig`.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Set a custom implementation of a `CryptoStore`.
|
||||
///
|
||||
/// The crypto store must be opened before being set.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub fn crypto_store(mut self, store: Box<dyn CryptoStore>) -> Self {
|
||||
self.crypto_store = Some(store);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a custom implementation of a `StateStore`.
|
||||
pub fn state_store(mut self, store: Box<dyn StateStore>) -> Self {
|
||||
self.state_store = Some(store);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
use std::{fmt, pin::Pin, sync::Arc};
|
||||
|
||||
use dashmap::DashSet;
|
||||
use futures_channel::mpsc;
|
||||
use futures_core::{
|
||||
stream::{BoxStream, Stream},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use ruma::OwnedEventId;
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::{SyncRoomEvent, TimelineSlice},
|
||||
store::Result,
|
||||
};
|
||||
|
||||
const CHANNEL_LIMIT: usize = 10;
|
||||
|
||||
/// Errors in a timeline stream
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TimelineStreamError {
|
||||
/// The end of the stored timeline was reached.
|
||||
#[error("the end of the stored timeline was reached")]
|
||||
EndCache {
|
||||
/// The given token should be used to request more events from the
|
||||
/// server.
|
||||
fetch_more_token: String,
|
||||
},
|
||||
/// The event in the store produced an error
|
||||
#[error("the event in the store produced an error")]
|
||||
Store(crate::StoreError),
|
||||
}
|
||||
|
||||
/// A `Stream` of timeline of a room
|
||||
pub struct TimelineStreamBackward<'a> {
|
||||
receiver: mpsc::Receiver<TimelineSlice>,
|
||||
stored_events: Option<BoxStream<'a, Result<SyncRoomEvent>>>,
|
||||
pending: Vec<SyncRoomEvent>,
|
||||
event_ids: Arc<DashSet<OwnedEventId>>,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for TimelineStreamBackward<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TimelineStream")
|
||||
.field("event_ids", &self.event_ids)
|
||||
.field("token", &self.token)
|
||||
.field("pending", &self.pending)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TimelineStreamBackward<'a> {
|
||||
/// Creates a tuple containing `Self` and a `Sender` to send new events to
|
||||
/// this stream
|
||||
///
|
||||
/// The stream is terminated when the entire timeline was returned,
|
||||
/// otherwise an error will be returned till the issue is resolved, aka
|
||||
/// new messages are loaded.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_ids` - A set to store `EventId`s already seen by the stream.
|
||||
/// This should be shared
|
||||
/// between the `TimelineStreamBackward` and `TimelineStreamForward` stream.
|
||||
///
|
||||
/// * `token` - The current sync token or the token that identifies the end
|
||||
/// of the `stored_events`.
|
||||
///
|
||||
/// * `stored_events` - A stream of events that are currently stored
|
||||
/// locally.
|
||||
pub(crate) fn new(
|
||||
event_ids: Arc<DashSet<OwnedEventId>>,
|
||||
token: Option<String>,
|
||||
stored_events: Option<BoxStream<'a, Result<SyncRoomEvent>>>,
|
||||
) -> (Self, mpsc::Sender<TimelineSlice>) {
|
||||
let (sender, receiver) = mpsc::channel(CHANNEL_LIMIT);
|
||||
let self_ = Self { event_ids, pending: Vec::new(), stored_events, token, receiver };
|
||||
|
||||
(self_, sender)
|
||||
}
|
||||
|
||||
fn handle_new_slice(
|
||||
&mut self,
|
||||
slice: TimelineSlice,
|
||||
) -> Poll<Option<Result<SyncRoomEvent, TimelineStreamError>>> {
|
||||
// Check if this is the batch we are expecting
|
||||
if self.token.is_some() && self.token != Some(slice.start) {
|
||||
trace!("Store received a timeline batch that wasn't expected");
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
// There is a gap in the timeline. Therefore, terminate the stream.
|
||||
if slice.limited {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
// The end of the timeline was reached
|
||||
if slice.events.is_empty() {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
for event in slice.events.into_iter().rev().filter(|event| {
|
||||
self.event_ids
|
||||
.insert(event.event_id().expect("Timeline events always have an event id."))
|
||||
}) {
|
||||
self.pending.push(event);
|
||||
}
|
||||
self.token = slice.end;
|
||||
|
||||
if let Some(event) = self.pending.pop() {
|
||||
Poll::Ready(Some(Ok(event)))
|
||||
} else if let Some(token) = &self.token {
|
||||
Poll::Ready(Some(Err(TimelineStreamError::EndCache {
|
||||
fetch_more_token: token.to_string(),
|
||||
})))
|
||||
} else {
|
||||
Poll::Ready(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Stream for TimelineStreamBackward<'a> {
|
||||
type Item = Result<SyncRoomEvent, TimelineStreamError>;
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
if let Some(stored_events) = &mut this.stored_events {
|
||||
match Pin::new(stored_events).poll_next(cx) {
|
||||
Poll::Ready(None) => {}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(Some(event)) => {
|
||||
return Poll::Ready(Some(event.map_err(TimelineStreamError::Store)))
|
||||
}
|
||||
}
|
||||
|
||||
// We returned all events the store gave to us
|
||||
this.stored_events = None;
|
||||
}
|
||||
|
||||
if let Some(event) = this.pending.pop() {
|
||||
Poll::Ready(Some(Ok(event)))
|
||||
} else {
|
||||
loop {
|
||||
match Pin::new(&mut this.receiver).poll_next(cx) {
|
||||
Poll::Ready(Some(slice)) => match this.handle_new_slice(slice) {
|
||||
Poll::Pending => continue,
|
||||
other => break other,
|
||||
},
|
||||
Poll::Ready(None) => break Poll::Ready(None),
|
||||
Poll::Pending => {
|
||||
if let Some(token) = &this.token {
|
||||
// We tell the consumer that there are no more evens in cache
|
||||
break Poll::Ready(Some(Err(TimelineStreamError::EndCache {
|
||||
fetch_more_token: token.to_string(),
|
||||
})));
|
||||
} else {
|
||||
break Poll::Ready(None);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Stream` of timeline of a room
|
||||
pub struct TimelineStreamForward {
|
||||
receiver: mpsc::Receiver<TimelineSlice>,
|
||||
pending: Vec<SyncRoomEvent>,
|
||||
event_ids: Arc<DashSet<OwnedEventId>>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for TimelineStreamForward {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TimelineStream")
|
||||
.field("event_ids", &self.event_ids)
|
||||
.field("pending", &self.pending)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineStreamForward {
|
||||
/// Creates a tuple containing `Self` and a `Sender` to send new events to
|
||||
/// this stream
|
||||
///
|
||||
/// The stream is only terminated when there would be a gap in the timeline.
|
||||
/// This happens when a `SyncResponse` is obtained with a limited
|
||||
/// timeline.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_ids` - A set to store `EventId`s already seen by the stream.
|
||||
/// This should be shared
|
||||
/// between the `TimelineStreamBackward` and `TimelineStreamForward` stream.
|
||||
pub(crate) fn new(
|
||||
event_ids: Arc<DashSet<OwnedEventId>>,
|
||||
) -> (Self, mpsc::Sender<TimelineSlice>) {
|
||||
let (sender, receiver) = mpsc::channel(CHANNEL_LIMIT);
|
||||
let self_ = Self { event_ids, pending: Vec::new(), receiver };
|
||||
|
||||
(self_, sender)
|
||||
}
|
||||
|
||||
fn handle_new_slice(&mut self, slice: TimelineSlice) -> Poll<Option<SyncRoomEvent>> {
|
||||
// There is a gap in the timeline. Therefore, terminate the stream.
|
||||
if slice.limited {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
for event in slice.events.into_iter().rev().filter(|event| {
|
||||
self.event_ids
|
||||
.insert(event.event_id().expect("Timeline events always have an event id."))
|
||||
}) {
|
||||
self.pending.push(event);
|
||||
}
|
||||
|
||||
if let Some(event) = self.pending.pop() {
|
||||
Poll::Ready(Some(event))
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for TimelineStreamForward {
|
||||
type Item = SyncRoomEvent;
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
|
||||
if let Some(event) = this.pending.pop() {
|
||||
Poll::Ready(Some(event))
|
||||
} else {
|
||||
loop {
|
||||
match Pin::new(&mut this.receiver).poll_next(cx) {
|
||||
Poll::Ready(Some(slice)) => match this.handle_new_slice(slice) {
|
||||
Poll::Pending => continue,
|
||||
other => break other,
|
||||
},
|
||||
Poll::Ready(None) => break Poll::Ready(None),
|
||||
Poll::Pending => break Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use ruma::{
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
RedactContent, RedactedEventContent, StateEventContent, StrippedStateEvent, SyncStateEvent,
|
||||
},
|
||||
EventId, OwnedEventId, RoomVersionId,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
// #[serde(bound)] instead of DeserializeOwned in type where clause does not
|
||||
// work, it can only be a single bound that replaces the default and if a helper
|
||||
// trait is used, the compiler still complains about Deserialize not being
|
||||
// implemented for C::Redacted.
|
||||
//
|
||||
// It is unclear why a Serialize bound on C::Redacted is not also required.
|
||||
|
||||
/// A minimal state event.
|
||||
///
|
||||
/// This type can holding an possibly-redacted state event with an optional
|
||||
/// event ID. The event ID is optional so this type can also hold events from
|
||||
/// invited rooms, where event IDs are not available.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum MinimalStateEvent<C: StateEventContent + RedactContent>
|
||||
where
|
||||
C::Redacted: StateEventContent + RedactedEventContent + DeserializeOwned,
|
||||
{
|
||||
/// An unredacted event.
|
||||
Original(OriginalMinimalStateEvent<C>),
|
||||
/// A redacted event.
|
||||
Redacted(RedactedMinimalStateEvent<C::Redacted>),
|
||||
}
|
||||
|
||||
/// An unredacted minimal state event.
|
||||
///
|
||||
/// For more details see [`MinimalStateEvent`].
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct OriginalMinimalStateEvent<C>
|
||||
where
|
||||
C: StateEventContent,
|
||||
{
|
||||
/// The event's content.
|
||||
pub content: C,
|
||||
/// The event's ID, if known.
|
||||
pub event_id: Option<OwnedEventId>,
|
||||
}
|
||||
|
||||
/// A redacted minimal state event.
|
||||
///
|
||||
/// For more details see [`MinimalStateEvent`].
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct RedactedMinimalStateEvent<C>
|
||||
where
|
||||
C: StateEventContent + RedactedEventContent,
|
||||
{
|
||||
/// The event's content.
|
||||
pub content: C,
|
||||
/// The event's ID, if known.
|
||||
pub event_id: Option<OwnedEventId>,
|
||||
}
|
||||
|
||||
impl<C> MinimalStateEvent<C>
|
||||
where
|
||||
C: StateEventContent + RedactContent,
|
||||
C::Redacted: StateEventContent + RedactedEventContent + DeserializeOwned,
|
||||
{
|
||||
/// Get the inner event's ID.
|
||||
pub fn event_id(&self) -> Option<&EventId> {
|
||||
match self {
|
||||
MinimalStateEvent::Original(ev) => ev.event_id.as_deref(),
|
||||
MinimalStateEvent::Redacted(ev) => ev.event_id.as_deref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the inner event, if it isn't redacted.
|
||||
pub fn as_original(&self) -> Option<&OriginalMinimalStateEvent<C>> {
|
||||
match self {
|
||||
MinimalStateEvent::Original(ev) => Some(ev),
|
||||
MinimalStateEvent::Redacted(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts `self` to the inner `OriginalMinimalStateEvent<C>`, if it isn't
|
||||
/// redacted.
|
||||
pub fn into_original(self) -> Option<OriginalMinimalStateEvent<C>> {
|
||||
match self {
|
||||
MinimalStateEvent::Original(ev) => Some(ev),
|
||||
MinimalStateEvent::Redacted(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Redacts this event.
|
||||
///
|
||||
/// Does nothing if it is already redacted.
|
||||
pub fn redact(&mut self, room_version: &RoomVersionId)
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
if let MinimalStateEvent::Original(ev) = self {
|
||||
*self = MinimalStateEvent::Redacted(RedactedMinimalStateEvent {
|
||||
content: ev.content.clone().redact(room_version),
|
||||
event_id: ev.event_id.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A minimal `m.room.member` event.
|
||||
pub type MinimalRoomMemberEvent = MinimalStateEvent<RoomMemberEventContent>;
|
||||
|
||||
impl MinimalRoomMemberEvent {
|
||||
/// Obtain the membership state, regardless of whether this event is
|
||||
/// redacted.
|
||||
pub fn membership(&self) -> &MembershipState {
|
||||
match self {
|
||||
MinimalStateEvent::Original(ev) => &ev.content.membership,
|
||||
MinimalStateEvent::Redacted(ev) => &ev.content.membership,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<&SyncStateEvent<C>> for MinimalStateEvent<C>
|
||||
where
|
||||
C: Clone + StateEventContent + RedactContent,
|
||||
C::Redacted: Clone + StateEventContent + RedactedEventContent + DeserializeOwned,
|
||||
{
|
||||
fn from(ev: &SyncStateEvent<C>) -> Self {
|
||||
match ev {
|
||||
SyncStateEvent::Original(ev) => Self::Original(OriginalMinimalStateEvent {
|
||||
content: ev.content.clone(),
|
||||
event_id: Some(ev.event_id.clone()),
|
||||
}),
|
||||
SyncStateEvent::Redacted(ev) => Self::Redacted(RedactedMinimalStateEvent {
|
||||
content: ev.content.clone(),
|
||||
event_id: Some(ev.event_id.clone()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<&StrippedStateEvent<C>> for MinimalStateEvent<C>
|
||||
where
|
||||
C: Clone + StateEventContent + RedactContent,
|
||||
C::Redacted: StateEventContent + RedactedEventContent + DeserializeOwned,
|
||||
{
|
||||
fn from(ev: &StrippedStateEvent<C>) -> Self {
|
||||
Self::Original(OriginalMinimalStateEvent { content: ev.content.clone(), event_id: None })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "Collection of common types and imports used in the matrix-sdk"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = "1.60"
|
||||
version = "0.5.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-target = "x86_64-unknown-linux-gnu"
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
|
||||
[dependencies]
|
||||
ruma = { version = "0.6.2", features = ["client-api-c"] }
|
||||
serde = "1.0.136"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
async-lock = "2.5.0"
|
||||
instant = { version = "0.1.12", features = ["wasm-bindgen", "inaccurate"] }
|
||||
futures-util = { version = "0.3.21", default-features = false, features = ["channel"] }
|
||||
wasm-bindgen-futures = "0.4.30"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.17.0", default-features = false, features = ["rt", "sync"] }
|
||||
instant = { version = "0.1.12", features = ["now"] }
|
||||
@@ -0,0 +1,9 @@
|
||||
Common types that are used in the different [matrix-sdk] layers.
|
||||
|
||||
**NOTE:** These are common dependencies for the different [matrix-sdk] layers,
|
||||
you're probably interested in the main
|
||||
[matrix-sdk](https://docs.rs/matrix-sdk/) crate.
|
||||
|
||||
[matrix-sdk]: https://github.com/matrix-org/matrix-rust-sdk/
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
+148
-100
@@ -1,20 +1,22 @@
|
||||
use std::{collections::BTreeMap, convert::TryFrom};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
api::client::r0::{
|
||||
push::get_notifications::Notification,
|
||||
sync::sync_events::{
|
||||
api::client::{
|
||||
push::get_notifications::v3::Notification,
|
||||
sync::sync_events::v3::{
|
||||
DeviceLists, Ephemeral, GlobalAccountData, InvitedRoom, Presence, RoomAccountData,
|
||||
State, ToDevice, UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
},
|
||||
events::{
|
||||
room::member::MemberEventContent, AnySyncRoomEvent, StateEvent, StrippedStateEvent,
|
||||
SyncStateEvent, Unsigned,
|
||||
room::member::{
|
||||
MembershipState, RoomMemberEvent, RoomMemberEventContent, StrippedRoomMemberEvent,
|
||||
SyncRoomMemberEvent,
|
||||
},
|
||||
AnyRoomEvent, AnySyncRoomEvent,
|
||||
},
|
||||
identifiers::{DeviceKeyAlgorithm, EventId, RoomId, UserId},
|
||||
serde::Raw,
|
||||
DeviceIdBox, MilliSecondsSinceUnixEpoch,
|
||||
DeviceKeyAlgorithm, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -26,9 +28,9 @@ pub struct AmbiguityChange {
|
||||
/// event itself ambiguous because of the event.
|
||||
pub member_ambiguous: bool,
|
||||
/// Has another user been disambiguated because of this event.
|
||||
pub disambiguated_member: Option<UserId>,
|
||||
pub disambiguated_member: Option<OwnedUserId>,
|
||||
/// Has another user become ambiguous because of this event.
|
||||
pub ambiguated_member: Option<UserId>,
|
||||
pub ambiguated_member: Option<OwnedUserId>,
|
||||
}
|
||||
|
||||
/// Collection of ambiguioty changes that room member events trigger.
|
||||
@@ -36,7 +38,7 @@ pub struct AmbiguityChange {
|
||||
pub struct AmbiguityChanges {
|
||||
/// A map from room id to a map of an event id to the `AmbiguityChange` that
|
||||
/// the event with the given id caused.
|
||||
pub changes: BTreeMap<RoomId, BTreeMap<EventId, AmbiguityChange>>,
|
||||
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
/// The verification state of the device that sent an event to us.
|
||||
@@ -73,10 +75,10 @@ pub enum AlgorithmInfo {
|
||||
pub struct EncryptionInfo {
|
||||
/// The user ID of the event sender, note this is untrusted data unless the
|
||||
/// `verification_state` is as well trusted.
|
||||
pub sender: UserId,
|
||||
pub sender: OwnedUserId,
|
||||
/// The device ID of the device that sent us the event, note this is
|
||||
/// untrusted data unless `verification_state` is as well trusted.
|
||||
pub sender_device: DeviceIdBox,
|
||||
pub sender_device: OwnedDeviceId,
|
||||
/// Information about the algorithm that was used to encrypt the event.
|
||||
pub algorithm_info: AlgorithmInfo,
|
||||
/// The verification state of the device that sent us the event, note this
|
||||
@@ -96,12 +98,29 @@ pub struct SyncRoomEvent {
|
||||
pub encryption_info: Option<EncryptionInfo>,
|
||||
}
|
||||
|
||||
impl SyncRoomEvent {
|
||||
/// Get the event id of this `SyncRoomEvent` if the event has any valid id.
|
||||
pub fn event_id(&self) -> Option<OwnedEventId> {
|
||||
self.event.get_field::<OwnedEventId>("event_id").ok().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Raw<AnySyncRoomEvent>> for SyncRoomEvent {
|
||||
fn from(inner: Raw<AnySyncRoomEvent>) -> Self {
|
||||
Self { encryption_info: None, event: inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomEvent> for SyncRoomEvent {
|
||||
fn from(o: RoomEvent) -> Self {
|
||||
// This conversion is unproblematic since a SyncRoomEvent is just a
|
||||
// RoomEvent without the room_id. By converting the raw value in this
|
||||
// way, we simply cause the `room_id` field in the json to be ignored by
|
||||
// a subsequent deserialization.
|
||||
Self { encryption_info: o.encryption_info, event: o.event.cast() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct SyncResponse {
|
||||
/// The batch token to supply in the `since` param of the next `/sync`
|
||||
@@ -125,7 +144,7 @@ pub struct SyncResponse {
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
/// New notifications per room.
|
||||
pub notifications: BTreeMap<RoomId, Vec<Notification>>,
|
||||
pub notifications: BTreeMap<OwnedRoomId, Vec<Notification>>,
|
||||
}
|
||||
|
||||
impl SyncResponse {
|
||||
@@ -134,14 +153,23 @@ impl SyncResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoomEvent {
|
||||
/// The actual event.
|
||||
pub event: Raw<AnyRoomEvent>,
|
||||
/// The encryption info about the event. Will be `None` if the event was not
|
||||
/// encrypted.
|
||||
pub encryption_info: Option<EncryptionInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Rooms {
|
||||
/// The rooms that the user has left or been banned from.
|
||||
pub leave: BTreeMap<RoomId, LeftRoom>,
|
||||
pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
|
||||
/// The rooms that the user has joined.
|
||||
pub join: BTreeMap<RoomId, JoinedRoom>,
|
||||
pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
|
||||
/// The rooms that the user has been invited to.
|
||||
pub invite: BTreeMap<RoomId, InvitedRoom>,
|
||||
pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
|
||||
}
|
||||
|
||||
/// Updates to joined rooms.
|
||||
@@ -235,96 +263,69 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
/// A slice of the timeline in the room.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct TimelineSlice {
|
||||
/// The `next_batch` or `from` token used to obtain this slice
|
||||
pub start: String,
|
||||
|
||||
/// The `prev_batch` or `to` token used to obtain this slice
|
||||
/// If `None` this `TimelineSlice` is the beginning of the room
|
||||
pub end: Option<String>,
|
||||
|
||||
/// Whether the number of events returned for this slice was limited
|
||||
/// by a `limit`-filter when requesting
|
||||
pub limited: bool,
|
||||
|
||||
/// A list of events.
|
||||
pub events: Vec<SyncRoomEvent>,
|
||||
|
||||
/// Whether this is a timeline slice obtained from a `SyncResponse`
|
||||
pub sync: bool,
|
||||
}
|
||||
|
||||
impl TimelineSlice {
|
||||
pub fn new(
|
||||
events: Vec<SyncRoomEvent>,
|
||||
start: String,
|
||||
end: Option<String>,
|
||||
limited: bool,
|
||||
sync: bool,
|
||||
) -> Self {
|
||||
Self { start, end, events, limited, sync }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around both MemberEvent-Types
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(
|
||||
try_from = "SyncStateEvent<MemberEventContent>",
|
||||
into = "SyncStateEvent<MemberEventContent>"
|
||||
)]
|
||||
pub struct MemberEvent {
|
||||
pub content: MemberEventContent,
|
||||
pub event_id: EventId,
|
||||
pub origin_server_ts: MilliSecondsSinceUnixEpoch,
|
||||
pub prev_content: Option<MemberEventContent>,
|
||||
pub sender: UserId,
|
||||
pub state_key: UserId,
|
||||
pub unsigned: Unsigned,
|
||||
pub enum MemberEvent {
|
||||
Sync(SyncRoomMemberEvent),
|
||||
Stripped(StrippedRoomMemberEvent),
|
||||
}
|
||||
|
||||
impl TryFrom<SyncStateEvent<MemberEventContent>> for MemberEvent {
|
||||
type Error = ruma::identifiers::Error;
|
||||
|
||||
fn try_from(event: SyncStateEvent<MemberEventContent>) -> Result<Self, Self::Error> {
|
||||
Ok(MemberEvent {
|
||||
content: event.content,
|
||||
event_id: event.event_id,
|
||||
origin_server_ts: event.origin_server_ts,
|
||||
prev_content: event.prev_content,
|
||||
sender: event.sender,
|
||||
state_key: UserId::try_from(event.state_key)?,
|
||||
unsigned: event.unsigned,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StateEvent<MemberEventContent>> for MemberEvent {
|
||||
type Error = ruma::identifiers::Error;
|
||||
|
||||
fn try_from(event: StateEvent<MemberEventContent>) -> Result<Self, Self::Error> {
|
||||
Ok(MemberEvent {
|
||||
content: event.content,
|
||||
event_id: event.event_id,
|
||||
origin_server_ts: event.origin_server_ts,
|
||||
prev_content: event.prev_content,
|
||||
sender: event.sender,
|
||||
state_key: UserId::try_from(event.state_key)?,
|
||||
unsigned: event.unsigned,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MemberEvent> for SyncStateEvent<MemberEventContent> {
|
||||
fn from(other: MemberEvent) -> SyncStateEvent<MemberEventContent> {
|
||||
SyncStateEvent {
|
||||
content: other.content,
|
||||
event_id: other.event_id,
|
||||
sender: other.sender,
|
||||
origin_server_ts: other.origin_server_ts,
|
||||
state_key: other.state_key.to_string(),
|
||||
prev_content: other.prev_content,
|
||||
unsigned: other.unsigned,
|
||||
impl MemberEvent {
|
||||
/// The inner Content of the wrapped Event
|
||||
pub fn original_content(&self) -> Option<&RoomMemberEventContent> {
|
||||
match self {
|
||||
MemberEvent::Sync(e) => e.as_original().map(|e| &e.content),
|
||||
MemberEvent::Stripped(e) => Some(&e.content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(
|
||||
try_from = "StrippedStateEvent<MemberEventContent>",
|
||||
into = "StrippedStateEvent<MemberEventContent>"
|
||||
)]
|
||||
pub struct StrippedMemberEvent {
|
||||
pub content: MemberEventContent,
|
||||
pub sender: UserId,
|
||||
pub state_key: UserId,
|
||||
}
|
||||
|
||||
impl TryFrom<StrippedStateEvent<MemberEventContent>> for StrippedMemberEvent {
|
||||
type Error = ruma::identifiers::Error;
|
||||
|
||||
fn try_from(event: StrippedStateEvent<MemberEventContent>) -> Result<Self, Self::Error> {
|
||||
Ok(StrippedMemberEvent {
|
||||
content: event.content,
|
||||
sender: event.sender,
|
||||
state_key: UserId::try_from(event.state_key)?,
|
||||
})
|
||||
/// The membership state of the user
|
||||
pub fn membership(&self) -> &MembershipState {
|
||||
match self {
|
||||
MemberEvent::Sync(e) => e.membership(),
|
||||
MemberEvent::Stripped(e) => &e.content.membership,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StrippedMemberEvent> for StrippedStateEvent<MemberEventContent> {
|
||||
fn from(other: StrippedMemberEvent) -> Self {
|
||||
Self {
|
||||
content: other.content,
|
||||
sender: other.sender,
|
||||
state_key: other.state_key.to_string(),
|
||||
/// The user id associated to this member event
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
match self {
|
||||
MemberEvent::Sync(e) => e.state_key(),
|
||||
MemberEvent::Stripped(e) => &e.state_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,7 +336,54 @@ impl From<StrippedMemberEvent> for StrippedStateEvent<MemberEventContent> {
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct MembersResponse {
|
||||
/// The list of members events.
|
||||
pub chunk: Vec<MemberEvent>,
|
||||
pub chunk: Vec<RoomMemberEvent>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::{
|
||||
event_id,
|
||||
events::{
|
||||
room::message::RoomMessageEventContent, AnySyncMessageLikeEvent, AnySyncRoomEvent,
|
||||
MessageLikeUnsigned, OriginalMessageLikeEvent, SyncMessageLikeEvent,
|
||||
},
|
||||
room_id,
|
||||
serde::Raw,
|
||||
user_id, MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
|
||||
use super::{RoomEvent, SyncRoomEvent};
|
||||
|
||||
#[test]
|
||||
fn room_event_to_sync_room_event() {
|
||||
let content = RoomMessageEventContent::text_plain("foobar");
|
||||
|
||||
let event = OriginalMessageLikeEvent {
|
||||
content,
|
||||
event_id: event_id!("$xxxxx:example.org").to_owned(),
|
||||
room_id: room_id!("!someroom:example.com").to_owned(),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
|
||||
sender: user_id!("@carl:example.com").to_owned(),
|
||||
unsigned: MessageLikeUnsigned::default(),
|
||||
};
|
||||
|
||||
let room_event =
|
||||
RoomEvent { event: Raw::new(&event).unwrap().cast(), encryption_info: None };
|
||||
|
||||
let converted_room_event: SyncRoomEvent = room_event.into();
|
||||
|
||||
let converted_event: AnySyncRoomEvent = converted_room_event.event.deserialize().unwrap();
|
||||
|
||||
let sync_event = AnySyncRoomEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
|
||||
SyncMessageLikeEvent::Original(event.into()),
|
||||
));
|
||||
|
||||
// There is no PartialEq implementation for AnySyncRoomEvent, so we
|
||||
// just compare a couple of fields here. The important thing is that
|
||||
// the deserialization above worked.
|
||||
assert_eq!(converted_event.event_id(), sync_event.event_id());
|
||||
assert_eq!(converted_event.sender(), sync_event.sender());
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
//! we do usually.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use futures::{future::RemoteHandle, Future, FutureExt};
|
||||
use futures_util::{future::RemoteHandle, FutureExt};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::spawn;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -26,6 +27,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug)]
|
||||
pub struct JoinHandle<T> {
|
||||
handle: RemoteHandle<Result<T, ()>>,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use async_trait::async_trait;
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![warn(missing_debug_implementations)]
|
||||
|
||||
pub use instant;
|
||||
pub use uuid;
|
||||
|
||||
pub mod deserialized_responses;
|
||||
pub mod executor;
|
||||
@@ -18,6 +19,6 @@ impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
|
||||
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
|
||||
/// implemented, while other targets will.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
|
||||
pub trait AsyncTraitDeps: std::fmt::Debug {}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
|
||||
impl<T: std::fmt::Debug> AsyncTraitDeps for T {}
|
||||
@@ -0,0 +1,4 @@
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use async_lock::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
@@ -0,0 +1,65 @@
|
||||
[package]
|
||||
name = "matrix-sdk-crypto-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2018"
|
||||
rust-version = "1.60"
|
||||
description = "Uniffi based bindings for the Rust SDK crypto crate"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
name = "matrix_crypto_ffi"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.57"
|
||||
base64 = "0.13.0"
|
||||
hmac = "0.12.1"
|
||||
http = "0.2.6"
|
||||
pbkdf2 = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
ruma = { version = "0.6.1", features = ["client-api-c"] }
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
sha2 = "0.10.2"
|
||||
thiserror = "1.0.30"
|
||||
tracing = "0.1.34"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
|
||||
uniffi = "0.17.0"
|
||||
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
|
||||
|
||||
[dependencies.js_int]
|
||||
version = "0.2.2"
|
||||
features = ["lax_deserialize"]
|
||||
|
||||
[dependencies.matrix-sdk-common]
|
||||
path = "../matrix-sdk-common"
|
||||
version = "0.5.0"
|
||||
|
||||
[dependencies.matrix-sdk-crypto]
|
||||
path = "../matrix-sdk-crypto"
|
||||
version = "0.5.0"
|
||||
features = ["qrcode", "backups_v1"]
|
||||
|
||||
[dependencies.matrix-sdk-sled]
|
||||
path = "../matrix-sdk-sled"
|
||||
version = "0.1.0"
|
||||
default_features = false
|
||||
features = ["crypto-store"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.17.0"
|
||||
default_features = false
|
||||
features = ["rt-multi-thread"]
|
||||
|
||||
[dependencies.vodozemac]
|
||||
git = "https://github.com/matrix-org/vodozemac/"
|
||||
rev = "d0e744287a14319c2a9148fef3747548c740fc36"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi_build = { version = "0.17.0", features = ["builtin-bindgen"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
@@ -0,0 +1,84 @@
|
||||
# Uniffi based bindings for the Rust SDK crypto crate.
|
||||
|
||||
This crate contains Uniffi based bindings for the `matrix-sdk-crypto` crate. The
|
||||
README mainly describes how to build and integrate the bindings into a Kotlin
|
||||
based Android project, but the Android specific bits can be skipped if you are
|
||||
targeting an x86 Linux project.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Rust
|
||||
|
||||
To build the bindings [Rust] will be needed it can be either installed using an
|
||||
OS specific package manager or directly with the provided [installer](https://rustup.rs/).
|
||||
|
||||
### Android NDK
|
||||
|
||||
The Android NDK will be required as well, it can be installed either through
|
||||
Android Studio or directly using an [installer](https://developer.android.com/ndk/downloads).
|
||||
|
||||
### Configuring Rust for cross compilation
|
||||
|
||||
First we'll need to install the Rust target for our desired Android architecture,
|
||||
for example:
|
||||
|
||||
```
|
||||
# rustup target add aarch64-linux-android
|
||||
```
|
||||
|
||||
This will add support to cross-compile for the aarch64-linux-android target,
|
||||
Rust supports many different [targets], you'll have to make sure to pick the
|
||||
right one for your device or emulator.
|
||||
|
||||
After this is done, we'll have to configure [Cargo] to use the correct linker
|
||||
for our target. Cargo is configured using a TOML file that will be found in
|
||||
`%USERPROFILE%\.cargo\config.toml` on Windows or `$HOME/.cargo/config` on Unix
|
||||
platforms. More details and configuration options for Cargo can be found in the
|
||||
official docs over [here](https://doc.rust-lang.org/cargo/reference/config.html).
|
||||
|
||||
```
|
||||
[target.aarch64-linux-android]
|
||||
ar = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/ar"
|
||||
linker = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To enable cross compilation for `olm-sys` which builds our `libolm` C library
|
||||
we'll need to set the `ANDROID_NDK` environment variable to the location of our
|
||||
Android NDK installation.
|
||||
|
||||
```
|
||||
$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/22.0.7026061/
|
||||
```
|
||||
|
||||
### Building for a target
|
||||
|
||||
The bindings can built for the `aarch64` target with:
|
||||
|
||||
```
|
||||
$ cargo build --target aarch64-linux-android
|
||||
```
|
||||
|
||||
After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory.
|
||||
The library will be called `libmatrix_crypto.so` and needs to be renamed and
|
||||
copied into the `jniLibs` directory of your Android project, for Element Android:
|
||||
|
||||
```
|
||||
$ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
|
||||
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
|
||||
```
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
These crates are built with the Rust language version 2021 and require a minimum compiler version of `1.60`.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[installer]: https://rustup.rs/
|
||||
[targets]: https://doc.rust-lang.org/nightly/rustc/platform-support.html
|
||||
[Cargo]: https://doc.rust-lang.org/cargo/
|
||||
[uniffi]: https://github.com/mozilla/uniffi-rs/
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
uniffi_build::generate_scaffolding("./src/olm.udl").unwrap();
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
use std::{collections::HashMap, iter};
|
||||
|
||||
use hmac::Hmac;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::OlmPkDecryptionError,
|
||||
store::{CryptoStoreError as InnerStoreError, RecoveryKey},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// The private part of the backup key, the one used for recovery.
|
||||
pub struct BackupRecoveryKey {
|
||||
pub(crate) inner: RecoveryKey,
|
||||
pub(crate) passphrase_info: Option<PassphraseInfo>,
|
||||
}
|
||||
|
||||
/// Error type for the decryption of backed up room keys.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PkDecryptionError {
|
||||
/// An internal libolm error happened during decryption.
|
||||
#[error("Error decryption a PkMessage {0}")]
|
||||
Olm(#[from] OlmPkDecryptionError),
|
||||
}
|
||||
|
||||
/// Error type for the decoding and storing of the backup key.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DecodeError {
|
||||
/// An error happened while decoding the recovery key.
|
||||
#[error(transparent)]
|
||||
Decode(#[from] matrix_sdk_crypto::backups::DecodeError),
|
||||
/// An error happened in the storage layer while trying to save the
|
||||
/// decoded recovery key.
|
||||
#[error(transparent)]
|
||||
CryptoStore(#[from] InnerStoreError),
|
||||
}
|
||||
|
||||
/// Struct containing info about the way the backup key got derived from a
|
||||
/// passphrase.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PassphraseInfo {
|
||||
/// The salt that was used during key derivation.
|
||||
pub private_key_salt: String,
|
||||
/// The number of PBKDF rounds that were used for key derivation.
|
||||
pub private_key_iterations: i32,
|
||||
}
|
||||
|
||||
/// The public part of the backup key.
|
||||
pub struct MegolmV1BackupKey {
|
||||
/// The actual base64 encoded public key.
|
||||
pub public_key: String,
|
||||
/// Signatures that have signed our backup key.
|
||||
pub signatures: HashMap<String, HashMap<String, String>>,
|
||||
/// The passphrase info, if the key was derived from one.
|
||||
pub passphrase_info: Option<PassphraseInfo>,
|
||||
/// Get the full name of the backup algorithm this backup key supports.
|
||||
pub backup_algorithm: String,
|
||||
}
|
||||
|
||||
impl BackupRecoveryKey {
|
||||
const KEY_SIZE: usize = 32;
|
||||
const SALT_SIZE: usize = 32;
|
||||
const PBKDF_ROUNDS: i32 = 500_000;
|
||||
|
||||
/// Create a new random [`BackupRecoveryKey`].
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: RecoveryKey::new()
|
||||
.expect("Can't gather enough randomness to create a recovery key"),
|
||||
passphrase_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
|
||||
pub fn from_base64(key: String) -> Result<Self, DecodeError> {
|
||||
Ok(Self { inner: RecoveryKey::from_base64(&key)?, passphrase_info: None })
|
||||
}
|
||||
|
||||
/// Try to create a [`BackupRecoveryKey`] from a base 58 encoded string.
|
||||
pub fn from_base58(key: String) -> Result<Self, DecodeError> {
|
||||
Ok(Self { inner: RecoveryKey::from_base58(&key)?, passphrase_info: None })
|
||||
}
|
||||
|
||||
/// Create a new [`BackupRecoveryKey`] from the given passphrase.
|
||||
pub fn new_from_passphrase(passphrase: String) -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let salt: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(Self::SALT_SIZE)
|
||||
.collect();
|
||||
|
||||
Self::from_passphrase(passphrase, salt, Self::PBKDF_ROUNDS)
|
||||
}
|
||||
|
||||
/// Restore a [`BackupRecoveryKey`] from the given passphrase.
|
||||
pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Self {
|
||||
let mut key = Box::new([0u8; Self::KEY_SIZE]);
|
||||
let rounds = rounds as u32;
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, &mut *key);
|
||||
|
||||
let recovery_key = RecoveryKey::from_bytes(&key);
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Self {
|
||||
inner: recovery_key,
|
||||
passphrase_info: Some(PassphraseInfo {
|
||||
private_key_salt: salt,
|
||||
private_key_iterations: rounds as i32,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the public part of the backup key.
|
||||
pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
|
||||
let public_key = self.inner.megolm_v1_public_key();
|
||||
|
||||
let signatures: HashMap<String, HashMap<String, String>> = public_key
|
||||
.signatures()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect()))
|
||||
.collect();
|
||||
|
||||
MegolmV1BackupKey {
|
||||
public_key: public_key.to_base64(),
|
||||
signatures,
|
||||
passphrase_info: self.passphrase_info.clone(),
|
||||
backup_algorithm: public_key.backup_algorithm().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 58 encoded string.
|
||||
pub fn to_base58(&self) -> String {
|
||||
self.inner.to_base58()
|
||||
}
|
||||
|
||||
/// Convert the recovery key to a base 64 encoded string.
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
|
||||
/// Try to decrypt a message that was encrypted using the public part of the
|
||||
/// backup key.
|
||||
pub fn decrypt_v1(
|
||||
&self,
|
||||
ephemeral_key: String,
|
||||
mac: String,
|
||||
ciphertext: String,
|
||||
) -> Result<String, PkDecryptionError> {
|
||||
self.inner.decrypt_v1(ephemeral_key, mac, ciphertext).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk_crypto::Device as InnerDevice;
|
||||
|
||||
/// An E2EE capable Matrix device.
|
||||
pub struct Device {
|
||||
/// The device owner.
|
||||
pub user_id: String,
|
||||
/// The unique ID of the device.
|
||||
pub device_id: String,
|
||||
/// The published public identity keys of the devices
|
||||
///
|
||||
/// A map from the key type (e.g. curve25519) to the base64 encoded key.
|
||||
pub keys: HashMap<String, String>,
|
||||
/// The supported algorithms of the device.
|
||||
pub algorithms: Vec<String>,
|
||||
/// The human readable name of the device.
|
||||
pub display_name: Option<String>,
|
||||
/// A flag indicating if the device has been blocked, blocked devices don't
|
||||
/// receive any room keys from us.
|
||||
pub is_blocked: bool,
|
||||
/// Is the device locally trusted
|
||||
pub locally_trusted: bool,
|
||||
/// Is our cross signing identity trusted and does the identity trust the
|
||||
/// device.
|
||||
pub cross_signing_trusted: bool,
|
||||
}
|
||||
|
||||
impl From<InnerDevice> for Device {
|
||||
fn from(d: InnerDevice) -> Self {
|
||||
Device {
|
||||
user_id: d.user_id().to_string(),
|
||||
device_id: d.device_id().to_string(),
|
||||
keys: d.keys().iter().map(|(k, v)| (k.to_string(), v.to_base64())).collect(),
|
||||
algorithms: d.algorithms().iter().map(|a| a.to_string()).collect(),
|
||||
display_name: d.display_name().map(|d| d.to_owned()),
|
||||
is_blocked: d.is_blacklisted(),
|
||||
locally_trusted: d.is_locally_trusted(),
|
||||
cross_signing_trusted: d.is_cross_signing_trusted(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
|
||||
SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
|
||||
};
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum KeyImportError {
|
||||
#[error(transparent)]
|
||||
Export(#[from] KeyExportError),
|
||||
#[error(transparent)]
|
||||
CryptoStore(#[from] InnerStoreError),
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SecretImportError {
|
||||
#[error(transparent)]
|
||||
CryptoStore(#[from] InnerStoreError),
|
||||
#[error(transparent)]
|
||||
Import(#[from] RustSecretImportError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SignatureError {
|
||||
#[error(transparent)]
|
||||
Signature(#[from] InnerSignatureError),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
#[error(transparent)]
|
||||
CryptoStore(#[from] InnerStoreError),
|
||||
#[error("Unknown device {0} {1}")]
|
||||
UnknownDevice(OwnedUserId, String),
|
||||
#[error("Unknown user identity {0}")]
|
||||
UnknownUserIdentity(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CryptoStoreError {
|
||||
#[error(transparent)]
|
||||
CryptoStore(#[from] InnerStoreError),
|
||||
#[error(transparent)]
|
||||
OlmError(#[from] OlmError),
|
||||
#[error(transparent)]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
#[error("The given string is not a valid user ID: source {0}, error {1}")]
|
||||
InvalidUserId(String, IdParseError),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DecryptionError {
|
||||
#[error(transparent)]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdParseError),
|
||||
#[error(transparent)]
|
||||
Megolm(#[from] MegolmError),
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
//! Uniffi based bindings for the `matrix-sdk-crypto` crate.
|
||||
//!
|
||||
//! This crate can be used to introduce E2EE support into an existing Matrix
|
||||
//! client or client library in any of the language targets Uniffi supports.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod backup_recovery_key;
|
||||
mod device;
|
||||
mod error;
|
||||
mod logger;
|
||||
mod machine;
|
||||
mod responses;
|
||||
mod users;
|
||||
mod verification;
|
||||
|
||||
use std::{collections::HashMap, convert::TryFrom, str::FromStr, sync::Arc};
|
||||
|
||||
pub use backup_recovery_key::{
|
||||
BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
|
||||
};
|
||||
pub use device::Device;
|
||||
pub use error::{
|
||||
CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError,
|
||||
};
|
||||
use js_int::UInt;
|
||||
pub use logger::{set_logger, Logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine};
|
||||
pub use responses::{
|
||||
BootstrapCrossSigningResult, DeviceLists, KeysImportResult, OutgoingVerificationRequest,
|
||||
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{DeviceId, DeviceKeyAlgorithm, OwnedUserId, RoomId, SecondsSinceUnixEpoch, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use users::UserIdentity;
|
||||
pub use verification::{
|
||||
CancelInfo, ConfirmVerificationResult, QrCode, RequestVerificationResult, Sas, ScanResult,
|
||||
StartSasResult, Verification, VerificationRequest,
|
||||
};
|
||||
|
||||
/// Struct collecting data that is important to migrate to the rust-sdk
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct MigrationData {
|
||||
/// The pickled version of the Olm Account
|
||||
account: PickledAccount,
|
||||
/// The list of pickleds Olm Sessions.
|
||||
sessions: Vec<PickledSession>,
|
||||
/// The list of Megolm inbound group sessions.
|
||||
inbound_group_sessions: Vec<PickledInboundGroupSession>,
|
||||
/// The Olm pickle key that was used to pickle all the Olm objects.
|
||||
pickle_key: Vec<u8>,
|
||||
/// The backup version that is currently active.
|
||||
backup_version: Option<String>,
|
||||
// The backup recovery key, as a base58 encoded string.
|
||||
backup_recovery_key: Option<String>,
|
||||
/// The private cross signing keys.
|
||||
cross_signing: CrossSigningKeyExport,
|
||||
/// The list of users that the Rust SDK should track.
|
||||
tracked_users: Vec<String>,
|
||||
}
|
||||
|
||||
/// A pickled version of an `Account`.
|
||||
///
|
||||
/// Holds all the information that needs to be stored in a database to restore
|
||||
/// an account.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct PickledAccount {
|
||||
/// The user id of the account owner.
|
||||
pub user_id: String,
|
||||
/// The device id of the account owner.
|
||||
pub device_id: String,
|
||||
/// The pickled version of the Olm account.
|
||||
pub pickle: String,
|
||||
/// Was the account shared.
|
||||
pub shared: bool,
|
||||
/// The number of uploaded one-time keys we have on the server.
|
||||
pub uploaded_signed_key_count: i64,
|
||||
}
|
||||
|
||||
/// A pickled version of a `Session`.
|
||||
///
|
||||
/// Holds all the information that needs to be stored in a database to restore
|
||||
/// a Session.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct PickledSession {
|
||||
/// The pickle string holding the Olm Session.
|
||||
pub pickle: String,
|
||||
/// The curve25519 key of the other user that we share this session with.
|
||||
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,
|
||||
}
|
||||
|
||||
/// A pickled version of an `InboundGroupSession`.
|
||||
///
|
||||
/// Holds all the information that needs to be stored in a database to restore
|
||||
/// an InboundGroupSession.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct PickledInboundGroupSession {
|
||||
/// The pickle string holding the InboundGroupSession.
|
||||
pub pickle: String,
|
||||
/// The public curve25519 key of the account that sent us the session
|
||||
pub sender_key: String,
|
||||
/// The public ed25519 key of the account that sent us the session.
|
||||
pub signing_key: HashMap<String, String>,
|
||||
/// The id of the room that the session is used in.
|
||||
pub room_id: String,
|
||||
/// The list of claimed ed25519 that forwarded us this key. Will be empty if
|
||||
/// we directly received this session.
|
||||
pub forwarding_chains: Vec<String>,
|
||||
/// Flag remembering if the session was directly sent to us by the sender
|
||||
/// or if it was imported.
|
||||
pub imported: bool,
|
||||
/// Flag remembering if the session has been backed up.
|
||||
pub backed_up: bool,
|
||||
}
|
||||
|
||||
/// Error type for the migration process.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum MigrationError {
|
||||
/// Generic catch all error variant.
|
||||
#[error("error migrating database: {error_message}")]
|
||||
Generic {
|
||||
/// The error message
|
||||
error_message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for MigrationError {
|
||||
fn from(e: anyhow::Error) -> MigrationError {
|
||||
MigrationError::Generic { error_message: e.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrate a libolm based setup to a vodozemac based setup stored in a Sled
|
||||
/// store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `data` - The data that should be migrated over to the Sled store.
|
||||
///
|
||||
/// * `path` - The path where the Sled store should be created.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the data at
|
||||
/// rest in the Sled store. **Warning**, if no passphrase is given, the store
|
||||
/// and all its data will remain unencrypted.
|
||||
///
|
||||
/// * `progress_listener` - A callback that can be used to introspect the
|
||||
/// progress of the migration.
|
||||
pub fn migrate(
|
||||
mut data: MigrationData,
|
||||
path: &str,
|
||||
passphrase: Option<String>,
|
||||
progress_listener: Box<dyn ProgressListener>,
|
||||
) -> anyhow::Result<()> {
|
||||
use matrix_sdk_crypto::{
|
||||
olm::PrivateCrossSigningIdentity,
|
||||
store::{Changes as RustChanges, CryptoStore, RecoveryKey},
|
||||
};
|
||||
use matrix_sdk_sled::CryptoStore as SledStore;
|
||||
use tokio::runtime::Runtime;
|
||||
use vodozemac::{
|
||||
megolm::InboundGroupSession,
|
||||
olm::{Account, Session},
|
||||
Curve25519PublicKey,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// The total steps here include all the sessions/inbound group sessions and
|
||||
// additionally some static number of steps:
|
||||
//
|
||||
// 1. opening the store
|
||||
// 2. the Account
|
||||
// 3. the cross signing keys
|
||||
// 4. the tracked users
|
||||
// 5. the final save operation
|
||||
let total_steps = 5 + data.sessions.len() + data.inbound_group_sessions.len();
|
||||
let mut processed_steps = 0;
|
||||
let listener = |progress: usize, total: usize| {
|
||||
progress_listener.on_progress(progress as i32, total as i32)
|
||||
};
|
||||
|
||||
let store = SledStore::open_with_passphrase(path, passphrase.as_deref())?;
|
||||
let runtime = Runtime::new()?;
|
||||
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
let user_id: Arc<UserId> = (&*parse_user_id(&data.account.user_id)?).into();
|
||||
let device_id: Box<DeviceId> = data.account.device_id.into();
|
||||
let device_id: Arc<DeviceId> = device_id.into();
|
||||
|
||||
let account = Account::from_libolm_pickle(&data.account.pickle, &data.pickle_key)?;
|
||||
let pickle = account.pickle();
|
||||
let identity_keys = Arc::new(account.identity_keys());
|
||||
let pickled_account = matrix_sdk_crypto::olm::PickledAccount {
|
||||
user_id: parse_user_id(&data.account.user_id)?,
|
||||
device_id: device_id.as_ref().to_owned(),
|
||||
pickle,
|
||||
shared: data.account.shared,
|
||||
uploaded_signed_key_count: data.account.uploaded_signed_key_count as u64,
|
||||
};
|
||||
let account = matrix_sdk_crypto::olm::ReadOnlyAccount::from_pickle(pickled_account)?;
|
||||
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
for session_pickle in data.sessions {
|
||||
let pickle =
|
||||
Session::from_libolm_pickle(&session_pickle.pickle, &data.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 pickle = matrix_sdk_crypto::olm::PickledSession {
|
||||
pickle,
|
||||
sender_key: Curve25519PublicKey::from_base64(&session_pickle.sender_key)?,
|
||||
created_using_fallback_key: session_pickle.created_using_fallback_key,
|
||||
creation_time,
|
||||
last_use_time,
|
||||
};
|
||||
|
||||
let session = matrix_sdk_crypto::olm::Session::from_pickle(
|
||||
user_id.clone(),
|
||||
device_id.clone(),
|
||||
identity_keys.clone(),
|
||||
pickle,
|
||||
);
|
||||
|
||||
sessions.push(session);
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
}
|
||||
|
||||
let mut inbound_group_sessions = Vec::new();
|
||||
|
||||
for session in data.inbound_group_sessions {
|
||||
let pickle =
|
||||
InboundGroupSession::from_libolm_pickle(&session.pickle, &data.pickle_key)?.pickle();
|
||||
|
||||
let pickle = matrix_sdk_crypto::olm::PickledInboundGroupSession {
|
||||
pickle,
|
||||
sender_key: session.sender_key,
|
||||
signing_key: session
|
||||
.signing_key
|
||||
.into_iter()
|
||||
.map(|(k, v)| Ok((DeviceKeyAlgorithm::try_from(k)?, v)))
|
||||
.collect::<anyhow::Result<_>>()?,
|
||||
room_id: RoomId::parse(session.room_id)?,
|
||||
forwarding_chains: session.forwarding_chains,
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
};
|
||||
|
||||
let session = matrix_sdk_crypto::olm::InboundGroupSession::from_pickle(pickle)?;
|
||||
|
||||
inbound_group_sessions.push(session);
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
}
|
||||
|
||||
let recovery_key =
|
||||
data.backup_recovery_key.map(|k| RecoveryKey::from_base58(k.as_str())).transpose()?;
|
||||
|
||||
let cross_signing = PrivateCrossSigningIdentity::empty((*user_id).into());
|
||||
runtime.block_on(cross_signing.import_secrets_unchecked(
|
||||
data.cross_signing.master_key.as_deref(),
|
||||
data.cross_signing.self_signing_key.as_deref(),
|
||||
data.cross_signing.user_signing_key.as_deref(),
|
||||
))?;
|
||||
|
||||
data.cross_signing.master_key.zeroize();
|
||||
data.cross_signing.self_signing_key.zeroize();
|
||||
data.cross_signing.user_signing_key.zeroize();
|
||||
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
let tracked_users: Vec<_> = data
|
||||
.tracked_users
|
||||
.into_iter()
|
||||
.map(|u| Ok(((parse_user_id(&u)?), true)))
|
||||
.collect::<anyhow::Result<_>>()?;
|
||||
|
||||
let tracked_users: Vec<_> = tracked_users.iter().map(|(u, d)| (&**u, *d)).collect();
|
||||
|
||||
runtime.block_on(store.save_tracked_users(tracked_users.as_slice()))?;
|
||||
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
let changes = RustChanges {
|
||||
account: Some(account),
|
||||
private_identity: Some(cross_signing),
|
||||
sessions,
|
||||
inbound_group_sessions,
|
||||
recovery_key,
|
||||
backup_version: data.backup_version,
|
||||
..Default::default()
|
||||
};
|
||||
runtime.block_on(store.save_changes(changes))?;
|
||||
|
||||
processed_steps += 1;
|
||||
listener(processed_steps, total_steps);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Callback that will be passed over the FFI to report progress
|
||||
pub trait ProgressListener {
|
||||
/// The callback that should be called on the Rust side
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `progress` - The current number of items that have been handled
|
||||
///
|
||||
/// * `total` - The total number of items that will be handled
|
||||
fn on_progress(&self, progress: i32, total: i32);
|
||||
}
|
||||
|
||||
impl<T: Fn(i32, i32)> ProgressListener for T {
|
||||
fn on_progress(&self, progress: i32, total: i32) {
|
||||
self(progress, total)
|
||||
}
|
||||
}
|
||||
|
||||
/// An event that was successfully decrypted.
|
||||
pub struct DecryptedEvent {
|
||||
/// The decrypted version of the event.
|
||||
pub clear_event: String,
|
||||
/// The claimed curve25519 key of the sender.
|
||||
pub sender_curve25519_key: String,
|
||||
/// The claimed ed25519 key of the sender.
|
||||
pub claimed_ed25519_key: Option<String>,
|
||||
/// The curve25519 chain of the senders that forwarded the Megolm decryption
|
||||
/// key to us. Is empty if the key came directly from the sender of the
|
||||
/// event.
|
||||
pub forwarding_curve25519_chain: Vec<String>,
|
||||
}
|
||||
|
||||
/// Struct representing the state of our private cross signing keys, it shows
|
||||
/// which private cross signing keys we have locally stored.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrossSigningStatus {
|
||||
/// Do we have the master key.
|
||||
pub has_master: bool,
|
||||
/// Do we have the self signing key, this one is necessary to sign our own
|
||||
/// devices.
|
||||
pub has_self_signing: bool,
|
||||
/// Do we have the user signing key, this one is necessary to sign other
|
||||
/// users.
|
||||
pub has_user_signing: bool,
|
||||
}
|
||||
|
||||
/// A struct containing private cross signing keys that can be backed up or
|
||||
/// uploaded to the secret store.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct CrossSigningKeyExport {
|
||||
/// The seed of the master key encoded as unpadded base64.
|
||||
pub master_key: Option<String>,
|
||||
/// The seed of the self signing key encoded as unpadded base64.
|
||||
pub self_signing_key: Option<String>,
|
||||
/// The seed of the user signing key encoded as unpadded base64.
|
||||
pub user_signing_key: Option<String>,
|
||||
}
|
||||
|
||||
/// Struct holding the number of room keys we have.
|
||||
pub struct RoomKeyCounts {
|
||||
/// The total number of room keys.
|
||||
pub total: i64,
|
||||
/// The number of backed up room keys.
|
||||
pub backed_up: i64,
|
||||
}
|
||||
|
||||
/// Backup keys and information we load from the store.
|
||||
pub struct BackupKeys {
|
||||
/// The recovery key as a base64 encoded string.
|
||||
recovery_key: Arc<BackupRecoveryKey>,
|
||||
/// The version that is used with the recovery key.
|
||||
backup_version: String,
|
||||
}
|
||||
|
||||
impl BackupKeys {
|
||||
/// Get the recovery key that we're holding on to.
|
||||
pub fn recovery_key(&self) -> Arc<BackupRecoveryKey> {
|
||||
self.recovery_key.clone()
|
||||
}
|
||||
|
||||
/// Get the backups version that we're holding on to.
|
||||
pub fn backup_version(&self) -> String {
|
||||
self.backup_version.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
recovery_key: BackupRecoveryKey {
|
||||
inner: keys.recovery_key.ok_or(())?,
|
||||
passphrase_info: None,
|
||||
}
|
||||
.into(),
|
||||
backup_version: keys.backup_version.ok_or(())?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::store::RoomKeyCounts> for RoomKeyCounts {
|
||||
fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self {
|
||||
Self { total: count.total as i64, backed_up: count.backed_up as i64 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::CrossSigningKeyExport> for CrossSigningKeyExport {
|
||||
fn from(e: matrix_sdk_crypto::CrossSigningKeyExport) -> Self {
|
||||
Self {
|
||||
master_key: e.master_key.clone(),
|
||||
self_signing_key: e.self_signing_key.clone(),
|
||||
user_signing_key: e.user_signing_key.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CrossSigningKeyExport> for matrix_sdk_crypto::CrossSigningKeyExport {
|
||||
fn from(e: CrossSigningKeyExport) -> Self {
|
||||
matrix_sdk_crypto::CrossSigningKeyExport {
|
||||
master_key: e.master_key,
|
||||
self_signing_key: e.self_signing_key,
|
||||
user_signing_key: e.user_signing_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::CrossSigningStatus> for CrossSigningStatus {
|
||||
fn from(s: matrix_sdk_crypto::CrossSigningStatus) -> Self {
|
||||
Self {
|
||||
has_master: s.has_master,
|
||||
has_self_signing: s.has_self_signing,
|
||||
has_user_signing: s.has_user_signing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_user_id(user_id: &str) -> Result<OwnedUserId, CryptoStoreError> {
|
||||
ruma::UserId::parse(user_id).map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e))
|
||||
}
|
||||
|
||||
#[allow(warnings)]
|
||||
mod generated {
|
||||
use super::*;
|
||||
include!(concat!(env!("OUT_DIR"), "/olm.uniffi.rs"));
|
||||
}
|
||||
|
||||
pub use generated::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::MigrationData;
|
||||
use crate::{migrate, OlmMachine};
|
||||
|
||||
#[test]
|
||||
fn android_migration() -> Result<()> {
|
||||
let data: Value = json!({
|
||||
"account":{
|
||||
"user_id":"@ganfra146:matrix.org",
|
||||
"device_id":"DEWRCMENGS",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"inbound_group_sessions":[
|
||||
{
|
||||
"pickle":"KoA0sxDNQ7lz0vylU9zlmar0VCVQRMCfRrIfTh1bdMhlAgy8/D2ToT+oKRaKy1HiW6H862bzdpgxprlseSjmip9OfLbIuyd2YZcEinwc2666oEI/hpk4oTlE61uE1M+ThfdFf41yGCmaAP7mhjwF234ZrZ6i/F/qx42TLQ8Unc30wDJaJgyheO5eW85SD/0g0cdg2WnEKrx2/wl7Vg/YijT3JMDZ+OsdfJfSZtxBNjlG+PQ/9D31qb1eHfaovc8vFZh5QLfAFg/5rBrF1PhRsC7xOAZbUNdrTbvypNfMM4mznf84C2SzZRSMeAfg5v/YticM3Keg4eHuEj1WO9DrmRXYl6b/pITdf1xuk5euVT0pyxJpXmq41AoAZKAo1l94HGy1LG1RpruD1uQPhiponh5PGHSOf43Q",
|
||||
"sender_key":"vJfH7wiYmGos3C8U1XcJ//YWSmkueAYqrmUA6/ukfAM",
|
||||
"signing_key":{
|
||||
"ed25519":"JGgPQRuYj3ScMdPS+A0P+k/1qS9Hr3qeKXLscI+hS78"
|
||||
},
|
||||
"room_id":"!AZkqtjvtwPAuyNOXEt:matrix.org",
|
||||
"forwarding_chains":[
|
||||
],
|
||||
"imported":true,
|
||||
"backed_up":true
|
||||
},
|
||||
{
|
||||
"pickle":"9RF6GBu9CvjZZx2hxIlw2gMdKs36LFhXhLTHAPrLSjT2OTbeE/jK263+iiFdSpF7Cblp/lXzljPKJN6sL8JGzoT7ssYh56nI0kKsp7/y88z+tTOH/5NYYTmZzHYw6yy4Cmaxh0pdHDs+RQpSSIe9jhF/EJJna5jcKYXxDY52m8H4LECQzVuDlYfblCr9zoYWhQrVhiRDGy7eLhk4X6Rp0Yoek4YUKcCQArDfZ/Vf43qfHUpOJgRpm5Oyj42HA/j4xZBb5U0Fmo6YHRPt0/KuWrDfpgJSGiN0zza7641IfADg8f3WdhlPAWMyri7k4vOZMBjlwFNcMpc0wM2TaTmbi2zqXEKZy9Oh/eJqBapFx0oNWaQ1VQ++iXxGUbZhwy7x2vd6UkqUTwYeym+aP23ee3TCtnNWN0aC",
|
||||
"sender_key":"EB9SC4jVAydKhM6/GcwMc9biKwVNywqW3TerNTrtb1M",
|
||||
"signing_key":{
|
||||
"ed25519":"1NXa5GyJ+p2ruAClEque+TL1VktrBzMW4dZFNfNGrvc"
|
||||
},
|
||||
"room_id":"!CWLUCoEWXSFyTCOtfL:matrix.org",
|
||||
"forwarding_chains":[],
|
||||
"imported":true,
|
||||
"backed_up":true
|
||||
}
|
||||
],
|
||||
"pickle_key": [17, 36, 120, 74, 95, 78, 56, 36, 62, 123, 5, 105, 74,
|
||||
111, 70, 48, 51, 101, 66, 86, 116, 14, 114, 85, 85,
|
||||
92, 44, 71, 89, 99, 55, 74],
|
||||
"backup_version":"3",
|
||||
"backup_recovery_key":"EsTHScmRV5oT1WBhe2mj2Gn3odeYantZ4NEk7L51p6L8hrmB",
|
||||
"cross_signing":{
|
||||
"master_key":"trnK/dBv/M2x2zZt8lnORHQqmFHWvjYE6rdlAONRUPY",
|
||||
"self_signing_key":"SJhsj9jXC4hxhqS/1B3RZ65zWMHuF+1fUjWHrzVRh6w",
|
||||
"user_signing_key":"LPYrV11T9Prm4ZIUxrq2a8Y/F64R1+NaGNyo6GlXjGg"
|
||||
},
|
||||
"tracked_users":[
|
||||
"@ganfra146:matrix.org",
|
||||
"@this-is-me:matrix.org",
|
||||
"@Amandine:matrix.org",
|
||||
"@ganfra:matrix.org"
|
||||
]
|
||||
});
|
||||
|
||||
let migration_data: MigrationData = serde_json::from_value(data)?;
|
||||
|
||||
let dir = tempdir()?;
|
||||
let path =
|
||||
dir.path().to_str().expect("Creating a string from the tempdir path should not fail");
|
||||
|
||||
migrate(migration_data, path, None, Box::new(|_, _| {}))?;
|
||||
|
||||
let machine = OlmMachine::new("@ganfra146:matrix.org", "DEWRCMENGS", path, None)?;
|
||||
|
||||
assert_eq!(
|
||||
machine.identity_keys()["ed25519"],
|
||||
"JGgPQRuYj3ScMdPS+A0P+k/1qS9Hr3qeKXLscI+hS78"
|
||||
);
|
||||
|
||||
let room_keys = machine.runtime.block_on(machine.inner.export_keys(|_| true))?;
|
||||
assert_eq!(room_keys.len(), 2);
|
||||
|
||||
let cross_signing_status = machine.cross_signing_status();
|
||||
assert!(cross_signing_status.has_master);
|
||||
assert!(cross_signing_status.has_user_signing);
|
||||
assert!(cross_signing_status.has_self_signing);
|
||||
|
||||
let backup_keys = machine.get_backup_keys()?;
|
||||
assert!(backup_keys.is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::{
|
||||
io::{Result, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
pub trait Logger: Send {
|
||||
/// Called every time the Rust side wants to post a log line.
|
||||
fn log(&self, log_line: String);
|
||||
// TODO add support for different log levels, do this by adding more methods
|
||||
// to the trait.
|
||||
}
|
||||
|
||||
impl Write for LoggerWrapper {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||
let data = String::from_utf8_lossy(buf).to_string();
|
||||
self.inner.lock().unwrap().log(data);
|
||||
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MakeWriter<'_> for LoggerWrapper {
|
||||
type Writer = LoggerWrapper;
|
||||
|
||||
fn make_writer(&self) -> Self::Writer {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoggerWrapper {
|
||||
inner: Arc<Mutex<Box<dyn Logger>>>,
|
||||
}
|
||||
|
||||
/// Set the logger that should be used to forward Rust logs over FFI.
|
||||
pub fn set_logger(logger: Box<dyn Logger>) {
|
||||
let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) };
|
||||
|
||||
let filter = EnvFilter::from_default_env().add_directive(
|
||||
"matrix_sdk_crypto=trace".parse().expect("Can't parse logging filter directive"),
|
||||
);
|
||||
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_writer(logger)
|
||||
.with_env_filter(filter)
|
||||
.without_time()
|
||||
.try_init();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,483 @@
|
||||
namespace olm {
|
||||
void set_logger(Logger logger);
|
||||
[Throws=MigrationError]
|
||||
void migrate(
|
||||
MigrationData data,
|
||||
[ByRef] string path,
|
||||
string? passphrase,
|
||||
ProgressListener progress_listener
|
||||
);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface MigrationError {
|
||||
Generic(string error_message);
|
||||
};
|
||||
|
||||
callback interface Logger {
|
||||
void log(string log_line);
|
||||
};
|
||||
|
||||
callback interface ProgressListener {
|
||||
void on_progress(i32 progress, i32 total);
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum PkDecryptionError {
|
||||
"Olm",
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum KeyImportError {
|
||||
"Export",
|
||||
"CryptoStore",
|
||||
"Json",
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum SignatureError {
|
||||
"Signature",
|
||||
"Identifier",
|
||||
"CryptoStore",
|
||||
"UnknownDevice",
|
||||
"UnknownUserIdentity",
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum SecretImportError {
|
||||
"Import",
|
||||
"CryptoStore",
|
||||
};
|
||||
|
||||
|
||||
[Error]
|
||||
enum CryptoStoreError {
|
||||
"CryptoStore",
|
||||
"OlmError",
|
||||
"Serialization",
|
||||
"Identifier",
|
||||
"InvalidUserId",
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum DecryptionError {
|
||||
"Identifier",
|
||||
"Serialization",
|
||||
"Megolm",
|
||||
};
|
||||
|
||||
dictionary DeviceLists {
|
||||
sequence<string> changed;
|
||||
sequence<string> left;
|
||||
};
|
||||
|
||||
dictionary KeysImportResult {
|
||||
i64 imported;
|
||||
i64 total;
|
||||
record<DOMString, record<DOMString, sequence<string>>> keys;
|
||||
};
|
||||
|
||||
dictionary DecryptedEvent {
|
||||
string clear_event;
|
||||
string sender_curve25519_key;
|
||||
string? claimed_ed25519_key;
|
||||
sequence<string> forwarding_curve25519_chain;
|
||||
};
|
||||
|
||||
dictionary Device {
|
||||
string user_id;
|
||||
string device_id;
|
||||
record<DOMString, string> keys;
|
||||
sequence<string> algorithms;
|
||||
string? display_name;
|
||||
boolean is_blocked;
|
||||
boolean locally_trusted;
|
||||
boolean cross_signing_trusted;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface UserIdentity {
|
||||
Own(
|
||||
string user_id,
|
||||
boolean trusts_our_own_device,
|
||||
string master_key,
|
||||
string self_signing_key,
|
||||
string user_signing_key
|
||||
);
|
||||
Other(
|
||||
string user_id,
|
||||
string master_key,
|
||||
string self_signing_key
|
||||
);
|
||||
};
|
||||
|
||||
dictionary CrossSigningStatus {
|
||||
boolean has_master;
|
||||
boolean has_self_signing;
|
||||
boolean has_user_signing;
|
||||
};
|
||||
|
||||
dictionary CrossSigningKeyExport {
|
||||
string? master_key;
|
||||
string? self_signing_key;
|
||||
string? user_signing_key;
|
||||
};
|
||||
|
||||
dictionary UploadSigningKeysRequest {
|
||||
string master_key;
|
||||
string self_signing_key;
|
||||
string user_signing_key;
|
||||
};
|
||||
|
||||
dictionary BootstrapCrossSigningResult {
|
||||
UploadSigningKeysRequest upload_signing_keys_request;
|
||||
SignatureUploadRequest signature_request;
|
||||
};
|
||||
|
||||
dictionary CancelInfo {
|
||||
string cancel_code;
|
||||
string reason;
|
||||
boolean cancelled_by_us;
|
||||
};
|
||||
|
||||
dictionary StartSasResult {
|
||||
Sas sas;
|
||||
OutgoingVerificationRequest request;
|
||||
};
|
||||
|
||||
dictionary Sas {
|
||||
string other_user_id;
|
||||
string other_device_id;
|
||||
string flow_id;
|
||||
string? room_id;
|
||||
boolean we_started;
|
||||
boolean has_been_accepted;
|
||||
boolean can_be_presented;
|
||||
boolean supports_emoji;
|
||||
boolean have_we_confirmed;
|
||||
boolean is_done;
|
||||
boolean is_cancelled;
|
||||
CancelInfo? cancel_info;
|
||||
};
|
||||
|
||||
dictionary ScanResult {
|
||||
QrCode qr;
|
||||
OutgoingVerificationRequest request;
|
||||
};
|
||||
|
||||
dictionary QrCode {
|
||||
string other_user_id;
|
||||
string other_device_id;
|
||||
string flow_id;
|
||||
string? room_id;
|
||||
boolean we_started;
|
||||
boolean other_side_scanned;
|
||||
boolean has_been_confirmed;
|
||||
boolean reciprocated;
|
||||
boolean is_done;
|
||||
boolean is_cancelled;
|
||||
CancelInfo? cancel_info;
|
||||
};
|
||||
|
||||
dictionary VerificationRequest {
|
||||
string other_user_id;
|
||||
string? other_device_id;
|
||||
string flow_id;
|
||||
string? room_id;
|
||||
boolean we_started;
|
||||
boolean is_ready;
|
||||
boolean is_passive;
|
||||
boolean is_done;
|
||||
boolean is_cancelled;
|
||||
CancelInfo? cancel_info;
|
||||
sequence<string>? their_methods;
|
||||
sequence<string>? our_methods;
|
||||
|
||||
};
|
||||
|
||||
dictionary RequestVerificationResult {
|
||||
VerificationRequest verification;
|
||||
OutgoingVerificationRequest request;
|
||||
};
|
||||
|
||||
dictionary ConfirmVerificationResult {
|
||||
sequence<OutgoingVerificationRequest> requests;
|
||||
SignatureUploadRequest? signature_request;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface Verification {
|
||||
SasV1(Sas sas);
|
||||
QrCodeV1(QrCode qrcode);
|
||||
};
|
||||
|
||||
dictionary KeyRequestPair {
|
||||
Request? cancellation;
|
||||
Request key_request;
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface OutgoingVerificationRequest {
|
||||
ToDevice(string request_id, string event_type, string body);
|
||||
InRoom(string request_id, string room_id, string event_type, string content);
|
||||
};
|
||||
|
||||
[Enum]
|
||||
interface Request {
|
||||
ToDevice(string request_id, string event_type, string body);
|
||||
KeysUpload(string request_id, string body);
|
||||
KeysQuery(string request_id, sequence<string> users);
|
||||
KeysClaim(string request_id, record<DOMString, record<DOMString, string>> one_time_keys);
|
||||
KeysBackup(string request_id, string version, string rooms);
|
||||
RoomMessage(string request_id, string room_id, string event_type, string content);
|
||||
SignatureUpload(string request_id, string body);
|
||||
};
|
||||
|
||||
dictionary SignatureUploadRequest {
|
||||
string body;
|
||||
};
|
||||
|
||||
enum RequestType {
|
||||
"KeysQuery",
|
||||
"KeysClaim",
|
||||
"KeysUpload",
|
||||
"ToDevice",
|
||||
"SignatureUpload",
|
||||
"KeysBackup",
|
||||
"RoomMessage",
|
||||
};
|
||||
|
||||
interface OlmMachine {
|
||||
[Throws=CryptoStoreError]
|
||||
constructor(
|
||||
[ByRef] string user_id,
|
||||
[ByRef] string device_id,
|
||||
[ByRef] string path,
|
||||
string? passphrase
|
||||
);
|
||||
|
||||
record<DOMString, string> identity_keys();
|
||||
string user_id();
|
||||
string device_id();
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
string receive_sync_changes([ByRef] string events,
|
||||
DeviceLists device_changes,
|
||||
record<DOMString, i32> key_counts,
|
||||
sequence<string>? unused_fallback_keys);
|
||||
[Throws=CryptoStoreError]
|
||||
sequence<Request> outgoing_requests();
|
||||
[Throws=CryptoStoreError]
|
||||
void mark_request_as_sent(
|
||||
[ByRef] string request_id,
|
||||
RequestType request_type,
|
||||
[ByRef] string response
|
||||
);
|
||||
|
||||
[Throws=DecryptionError]
|
||||
DecryptedEvent decrypt_room_event([ByRef] string event, [ByRef] string room_id);
|
||||
[Throws=CryptoStoreError]
|
||||
string encrypt([ByRef] string room_id, [ByRef] string event_type, [ByRef] string content);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
UserIdentity? get_identity([ByRef] string user_id, u32 timeout);
|
||||
[Throws=SignatureError]
|
||||
SignatureUploadRequest verify_identity([ByRef] string user_id);
|
||||
[Throws=CryptoStoreError]
|
||||
Device? get_device([ByRef] string user_id, [ByRef] string device_id, u32 timeout);
|
||||
[Throws=CryptoStoreError]
|
||||
void mark_device_as_trusted([ByRef] string user_id, [ByRef] string device_id);
|
||||
[Throws=SignatureError]
|
||||
SignatureUploadRequest verify_device([ByRef] string user_id, [ByRef] string device_id);
|
||||
[Throws=CryptoStoreError]
|
||||
sequence<Device> get_user_devices([ByRef] string user_id, u32 timeout);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
boolean is_user_tracked([ByRef] string user_id);
|
||||
void update_tracked_users(sequence<string> users);
|
||||
[Throws=CryptoStoreError]
|
||||
Request? get_missing_sessions(sequence<string> users);
|
||||
[Throws=CryptoStoreError]
|
||||
sequence<Request> share_room_key([ByRef] string room_id, sequence<string> users);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
void receive_unencrypted_verification_event([ByRef] string event, [ByRef] string room_id);
|
||||
sequence<VerificationRequest> get_verification_requests([ByRef] string user_id);
|
||||
VerificationRequest? get_verification_request([ByRef] string user_id, [ByRef] string flow_id);
|
||||
Verification? get_verification([ByRef] string user_id, [ByRef] string flow_id);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
VerificationRequest? request_verification(
|
||||
[ByRef] string user_id,
|
||||
[ByRef] string room_id,
|
||||
[ByRef] string event_id,
|
||||
sequence<string> methods
|
||||
);
|
||||
[Throws=CryptoStoreError]
|
||||
string? verification_request_content(
|
||||
[ByRef] string user_id,
|
||||
sequence<string> methods
|
||||
);
|
||||
[Throws=CryptoStoreError]
|
||||
RequestVerificationResult? request_self_verification(sequence<string> methods);
|
||||
[Throws=CryptoStoreError]
|
||||
RequestVerificationResult? request_verification_with_device(
|
||||
[ByRef] string user_id,
|
||||
[ByRef] string device_id,
|
||||
sequence<string> methods
|
||||
);
|
||||
|
||||
OutgoingVerificationRequest? accept_verification_request(
|
||||
[ByRef] string user_id,
|
||||
[ByRef] string flow_id,
|
||||
sequence<string> methods
|
||||
);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
ConfirmVerificationResult? confirm_verification([ByRef] string user_id, [ByRef] string flow_id);
|
||||
OutgoingVerificationRequest? cancel_verification(
|
||||
[ByRef] string user_id,
|
||||
[ByRef] string flow_id,
|
||||
[ByRef] string cancel_code
|
||||
);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
StartSasResult? start_sas_with_device([ByRef] string user_id, [ByRef] string device_id);
|
||||
[Throws=CryptoStoreError]
|
||||
StartSasResult? start_sas_verification([ByRef] string user_id, [ByRef] string flow_id);
|
||||
OutgoingVerificationRequest? accept_sas_verification([ByRef] string user_id, [ByRef] string flow_id);
|
||||
sequence<i32>? get_emoji_index([ByRef] string user_id, [ByRef] string flow_id);
|
||||
sequence<i32>? get_decimals([ByRef] string user_id, [ByRef] string flow_id);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
QrCode? start_qr_verification([ByRef] string user_id, [ByRef] string flow_id);
|
||||
ScanResult? scan_qr_code([ByRef] string user_id, [ByRef] string flow_id, [ByRef] string data);
|
||||
string? generate_qr_code([ByRef] string user_id, [ByRef] string flow_id);
|
||||
|
||||
[Throws=DecryptionError]
|
||||
KeyRequestPair request_room_key([ByRef] string event, [ByRef] string room_id);
|
||||
|
||||
[Throws=CryptoStoreError]
|
||||
string export_keys([ByRef] string passphrase, i32 rounds);
|
||||
[Throws=KeyImportError]
|
||||
KeysImportResult import_keys(
|
||||
[ByRef] string keys,
|
||||
[ByRef] string passphrase,
|
||||
ProgressListener progress_listener
|
||||
);
|
||||
[Throws=KeyImportError]
|
||||
KeysImportResult import_decrypted_keys(
|
||||
[ByRef] string keys,
|
||||
ProgressListener progress_listener
|
||||
);
|
||||
[Throws=CryptoStoreError]
|
||||
void discard_room_key([ByRef] string room_id);
|
||||
|
||||
CrossSigningStatus cross_signing_status();
|
||||
[Throws=CryptoStoreError]
|
||||
BootstrapCrossSigningResult bootstrap_cross_signing();
|
||||
CrossSigningKeyExport? export_cross_signing_keys();
|
||||
[Throws=SecretImportError]
|
||||
void import_cross_signing_keys(CrossSigningKeyExport export);
|
||||
[Throws=CryptoStoreError]
|
||||
boolean is_identity_verified([ByRef] string user_id);
|
||||
|
||||
record<DOMString, record<DOMString, string>> sign([ByRef] string message);
|
||||
[Throws=DecodeError]
|
||||
void enable_backup_v1(MegolmV1BackupKey key, string version);
|
||||
[Throws=CryptoStoreError]
|
||||
void disable_backup();
|
||||
[Throws=CryptoStoreError]
|
||||
Request? backup_room_keys();
|
||||
[Throws=CryptoStoreError]
|
||||
void save_recovery_key(BackupRecoveryKey? key, string? version);
|
||||
[Throws=CryptoStoreError]
|
||||
RoomKeyCounts room_key_counts();
|
||||
[Throws=CryptoStoreError]
|
||||
BackupKeys? get_backup_keys();
|
||||
boolean backup_enabled();
|
||||
[Throws=CryptoStoreError]
|
||||
boolean verify_backup([ByRef] string auth_data);
|
||||
};
|
||||
|
||||
dictionary PassphraseInfo {
|
||||
string private_key_salt;
|
||||
i32 private_key_iterations;
|
||||
};
|
||||
|
||||
dictionary MegolmV1BackupKey {
|
||||
string public_key;
|
||||
record<DOMString, record<DOMString, string>> signatures;
|
||||
PassphraseInfo? passphrase_info;
|
||||
string backup_algorithm;
|
||||
};
|
||||
|
||||
interface BackupKeys {
|
||||
BackupRecoveryKey recovery_key();
|
||||
string backup_version();
|
||||
};
|
||||
|
||||
dictionary RoomKeyCounts {
|
||||
i64 total;
|
||||
i64 backed_up;
|
||||
};
|
||||
|
||||
[Error]
|
||||
enum DecodeError {
|
||||
"Decode",
|
||||
"CryptoStore",
|
||||
};
|
||||
|
||||
interface BackupRecoveryKey {
|
||||
constructor();
|
||||
[Name=from_passphrase]
|
||||
constructor(string passphrase, string salt, i32 rounds);
|
||||
[Name=new_from_passphrase]
|
||||
constructor(string passphrase);
|
||||
[Name=from_base64, Throws=DecodeError]
|
||||
constructor(string key);
|
||||
[Name=from_base58, Throws=DecodeError]
|
||||
constructor(string key);
|
||||
string to_base58();
|
||||
string to_base64();
|
||||
MegolmV1BackupKey megolm_v1_public_key();
|
||||
[Throws=PkDecryptionError]
|
||||
string decrypt_v1(string ephemeral_key, string mac, string ciphertext);
|
||||
};
|
||||
|
||||
dictionary MigrationData {
|
||||
PickledAccount account;
|
||||
sequence<PickledSession> sessions;
|
||||
sequence<PickledInboundGroupSession> inbound_group_sessions;
|
||||
string? backup_version;
|
||||
string? backup_recovery_key;
|
||||
sequence<u8> pickle_key;
|
||||
CrossSigningKeyExport cross_signing;
|
||||
sequence<string> tracked_users;
|
||||
};
|
||||
|
||||
dictionary PickledAccount {
|
||||
string user_id;
|
||||
string device_id;
|
||||
string pickle;
|
||||
boolean shared;
|
||||
i64 uploaded_signed_key_count;
|
||||
};
|
||||
|
||||
dictionary PickledSession {
|
||||
string pickle;
|
||||
string sender_key;
|
||||
boolean created_using_fallback_key;
|
||||
string creation_time;
|
||||
string last_use_time;
|
||||
};
|
||||
|
||||
dictionary PickledInboundGroupSession {
|
||||
string pickle;
|
||||
string sender_key;
|
||||
record<DOMString, string> signing_key;
|
||||
string room_id;
|
||||
sequence<string> forwarding_chains;
|
||||
boolean imported;
|
||||
boolean backed_up;
|
||||
};
|
||||
@@ -0,0 +1,331 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
IncomingResponse, OutgoingRequest, OutgoingVerificationRequest as SdkVerificationRequest,
|
||||
RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
claim_keys::v3::{Request as KeysClaimRequest, Response as KeysClaimResponse},
|
||||
get_keys::v3::Response as KeysQueryResponse,
|
||||
upload_keys::v3::Response as KeysUploadResponse,
|
||||
upload_signatures::v3::{
|
||||
Request as RustSignatureUploadRequest, Response as SignatureUploadResponse,
|
||||
},
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomMessageResponse,
|
||||
sync::sync_events::v3::DeviceLists as RumaDeviceLists,
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
assign,
|
||||
events::EventContent,
|
||||
OwnedTransactionId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub struct SignatureUploadRequest {
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
impl From<RustSignatureUploadRequest> for SignatureUploadRequest {
|
||||
fn from(r: RustSignatureUploadRequest) -> Self {
|
||||
Self {
|
||||
body: serde_json::to_string(&r.signed_keys)
|
||||
.expect("Can't serialize signature upload request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UploadSigningKeysRequest {
|
||||
pub master_key: String,
|
||||
pub self_signing_key: String,
|
||||
pub user_signing_key: String,
|
||||
}
|
||||
|
||||
impl From<RustUploadSigningKeysRequest> for UploadSigningKeysRequest {
|
||||
fn from(r: RustUploadSigningKeysRequest) -> Self {
|
||||
Self {
|
||||
master_key: serde_json::to_string(
|
||||
&r.master_key.expect("Request didn't contain a master key"),
|
||||
)
|
||||
.expect("Can't serialize cross signing master key"),
|
||||
self_signing_key: serde_json::to_string(
|
||||
&r.self_signing_key.expect("Request didn't contain a self-signing key"),
|
||||
)
|
||||
.expect("Can't serialize cross signing self-signing key"),
|
||||
user_signing_key: serde_json::to_string(
|
||||
&r.user_signing_key.expect("Request didn't contain a user-signing key"),
|
||||
)
|
||||
.expect("Can't serialize cross signing user-signing key"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BootstrapCrossSigningResult {
|
||||
pub upload_signing_keys_request: UploadSigningKeysRequest,
|
||||
pub signature_request: SignatureUploadRequest,
|
||||
}
|
||||
|
||||
impl From<(RustUploadSigningKeysRequest, RustSignatureUploadRequest)>
|
||||
for BootstrapCrossSigningResult
|
||||
{
|
||||
fn from(requests: (RustUploadSigningKeysRequest, RustSignatureUploadRequest)) -> Self {
|
||||
Self {
|
||||
upload_signing_keys_request: requests.0.into(),
|
||||
signature_request: requests.1.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum OutgoingVerificationRequest {
|
||||
ToDevice { request_id: String, event_type: String, body: String },
|
||||
InRoom { request_id: String, room_id: String, event_type: String, content: String },
|
||||
}
|
||||
|
||||
impl From<SdkVerificationRequest> for OutgoingVerificationRequest {
|
||||
fn from(r: SdkVerificationRequest) -> Self {
|
||||
match r {
|
||||
SdkVerificationRequest::ToDevice(r) => r.into(),
|
||||
SdkVerificationRequest::InRoom(r) => Self::InRoom {
|
||||
request_id: r.txn_id.to_string(),
|
||||
room_id: r.room_id.to_string(),
|
||||
content: serde_json::to_string(&r.content)
|
||||
.expect("Can't serialize message content"),
|
||||
event_type: r.content.event_type().to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceRequest> for OutgoingVerificationRequest {
|
||||
fn from(r: ToDeviceRequest) -> Self {
|
||||
Self::ToDevice {
|
||||
request_id: r.txn_id.to_string(),
|
||||
event_type: r.event_type.to_string(),
|
||||
body: serde_json::to_string(&r.messages).expect("Can't serialize to-device body"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Request {
|
||||
ToDevice { request_id: String, event_type: String, body: String },
|
||||
KeysUpload { request_id: String, body: String },
|
||||
KeysQuery { request_id: String, users: Vec<String> },
|
||||
KeysClaim { request_id: String, one_time_keys: HashMap<String, HashMap<String, String>> },
|
||||
RoomMessage { request_id: String, room_id: String, event_type: String, content: String },
|
||||
SignatureUpload { request_id: String, body: String },
|
||||
KeysBackup { request_id: String, version: String, rooms: String },
|
||||
}
|
||||
|
||||
impl From<OutgoingRequest> for Request {
|
||||
fn from(r: OutgoingRequest) -> Self {
|
||||
use matrix_sdk_crypto::OutgoingRequests::*;
|
||||
|
||||
match r.request() {
|
||||
KeysUpload(u) => {
|
||||
let body = json!({
|
||||
"device_keys": u.device_keys,
|
||||
"one_time_keys": u.one_time_keys,
|
||||
});
|
||||
|
||||
Request::KeysUpload {
|
||||
request_id: r.request_id().to_string(),
|
||||
body: serde_json::to_string(&body)
|
||||
.expect("Can't serialize keys upload request"),
|
||||
}
|
||||
}
|
||||
KeysQuery(k) => {
|
||||
let users: Vec<String> = k.device_keys.keys().map(|u| u.to_string()).collect();
|
||||
Request::KeysQuery { request_id: r.request_id().to_string(), users }
|
||||
}
|
||||
ToDeviceRequest(t) => Request::from(t),
|
||||
SignatureUpload(t) => Request::SignatureUpload {
|
||||
request_id: r.request_id().to_string(),
|
||||
body: serde_json::to_string(&t.signed_keys)
|
||||
.expect("Can't serialize signature upload request"),
|
||||
},
|
||||
RoomMessage(r) => Request::from(r),
|
||||
KeysClaim(c) => (r.request_id().to_owned(), c.clone()).into(),
|
||||
KeysBackup(b) => Request::KeysBackup {
|
||||
request_id: r.request_id().to_string(),
|
||||
version: b.version.to_owned(),
|
||||
rooms: serde_json::to_string(&b.rooms)
|
||||
.expect("Can't serialize keys backup request"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceRequest> for Request {
|
||||
fn from(r: ToDeviceRequest) -> Self {
|
||||
Request::ToDevice {
|
||||
request_id: r.txn_id.to_string(),
|
||||
event_type: r.event_type.to_string(),
|
||||
body: serde_json::to_string(&r.messages).expect("Can't serialize to-device body"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(OwnedTransactionId, KeysClaimRequest)> for Request {
|
||||
fn from(request_tuple: (OwnedTransactionId, KeysClaimRequest)) -> Self {
|
||||
let (request_id, request) = request_tuple;
|
||||
|
||||
Request::KeysClaim {
|
||||
request_id: request_id.to_string(),
|
||||
one_time_keys: request
|
||||
.one_time_keys
|
||||
.into_iter()
|
||||
.map(|(u, d)| {
|
||||
(
|
||||
u.to_string(),
|
||||
d.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ToDeviceRequest> for Request {
|
||||
fn from(r: &ToDeviceRequest) -> Self {
|
||||
Request::ToDevice {
|
||||
request_id: r.txn_id.to_string(),
|
||||
event_type: r.event_type.to_string(),
|
||||
body: serde_json::to_string(&r.messages).expect("Can't serialize to-device body"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RoomMessageRequest> for Request {
|
||||
fn from(r: &RoomMessageRequest) -> Self {
|
||||
Self::RoomMessage {
|
||||
request_id: r.txn_id.to_string(),
|
||||
room_id: r.room_id.to_string(),
|
||||
event_type: r.content.event_type().to_string(),
|
||||
content: serde_json::to_string(&r.content).expect("Can't serialize message content"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn response_from_string(body: &str) -> Response<Vec<u8>> {
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.body(body.as_bytes().to_vec())
|
||||
.expect("Can't create HTTP response")
|
||||
}
|
||||
|
||||
pub enum RequestType {
|
||||
KeysQuery,
|
||||
KeysClaim,
|
||||
KeysUpload,
|
||||
ToDevice,
|
||||
SignatureUpload,
|
||||
KeysBackup,
|
||||
RoomMessage,
|
||||
}
|
||||
|
||||
pub struct DeviceLists {
|
||||
pub changed: Vec<String>,
|
||||
pub left: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<DeviceLists> for RumaDeviceLists {
|
||||
fn from(d: DeviceLists) -> Self {
|
||||
assign!(RumaDeviceLists::new(), {
|
||||
changed: d
|
||||
.changed
|
||||
.into_iter()
|
||||
.filter_map(|u| UserId::parse(u).ok())
|
||||
.collect(),
|
||||
left: d
|
||||
.left
|
||||
.into_iter()
|
||||
.filter_map(|u| UserId::parse(u).ok())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KeysImportResult {
|
||||
/// The number of room keys that were imported.
|
||||
pub imported: i64,
|
||||
/// The total number of room keys that were found in the export.
|
||||
pub total: i64,
|
||||
/// The map of keys that were imported.
|
||||
///
|
||||
/// It's a map from room id to a map of the sender key to a list of session
|
||||
/// ids.
|
||||
pub keys: HashMap<String, HashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
pub(crate) enum OwnedResponse {
|
||||
KeysClaim(KeysClaimResponse),
|
||||
KeysUpload(KeysUploadResponse),
|
||||
KeysQuery(KeysQueryResponse),
|
||||
ToDevice(ToDeviceResponse),
|
||||
SignatureUpload(SignatureUploadResponse),
|
||||
KeysBackup(KeysBackupResponse),
|
||||
RoomMessage(RoomMessageResponse),
|
||||
}
|
||||
|
||||
impl From<KeysClaimResponse> for OwnedResponse {
|
||||
fn from(response: KeysClaimResponse) -> Self {
|
||||
OwnedResponse::KeysClaim(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysQueryResponse> for OwnedResponse {
|
||||
fn from(response: KeysQueryResponse) -> Self {
|
||||
OwnedResponse::KeysQuery(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysUploadResponse> for OwnedResponse {
|
||||
fn from(response: KeysUploadResponse) -> Self {
|
||||
OwnedResponse::KeysUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceResponse> for OwnedResponse {
|
||||
fn from(response: ToDeviceResponse) -> Self {
|
||||
OwnedResponse::ToDevice(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureUploadResponse> for OwnedResponse {
|
||||
fn from(response: SignatureUploadResponse) -> Self {
|
||||
Self::SignatureUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysBackupResponse> for OwnedResponse {
|
||||
fn from(r: KeysBackupResponse) -> Self {
|
||||
Self::KeysBackup(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomMessageResponse> for OwnedResponse {
|
||||
fn from(response: RoomMessageResponse) -> Self {
|
||||
OwnedResponse::RoomMessage(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> {
|
||||
fn from(r: &'a OwnedResponse) -> Self {
|
||||
match r {
|
||||
OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r),
|
||||
OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r),
|
||||
OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r),
|
||||
OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r),
|
||||
OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r),
|
||||
OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r),
|
||||
OwnedResponse::RoomMessage(r) => IncomingResponse::RoomMessage(r),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentities};
|
||||
|
||||
use crate::CryptoStoreError;
|
||||
|
||||
/// Enum representing cross signing identities of our own user or some other
|
||||
/// user.
|
||||
pub enum UserIdentity {
|
||||
/// Our own user identity.
|
||||
Own {
|
||||
/// The unique id of our own user.
|
||||
user_id: String,
|
||||
/// Does our own user identity trust our own device.
|
||||
trusts_our_own_device: bool,
|
||||
/// The public master key of our identity.
|
||||
master_key: String,
|
||||
/// The public user-signing key of our identity.
|
||||
user_signing_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
},
|
||||
/// The user identity of other users.
|
||||
Other {
|
||||
/// The unique id of the user.
|
||||
user_id: String,
|
||||
/// The public master key of the identity.
|
||||
master_key: String,
|
||||
/// The public self-signing key of our identity.
|
||||
self_signing_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl UserIdentity {
|
||||
pub(crate) async fn from_rust(i: UserIdentities) -> Result<Self, CryptoStoreError> {
|
||||
Ok(match i {
|
||||
UserIdentities::Own(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let user_signing: CrossSigningKey = i.user_signing_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
|
||||
UserIdentity::Own {
|
||||
user_id: i.user_id().to_string(),
|
||||
trusts_our_own_device: i.trusts_our_own_device().await?,
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
user_signing_key: serde_json::to_string(&user_signing)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
}
|
||||
}
|
||||
UserIdentities::Other(i) => {
|
||||
let master: CrossSigningKey = i.master_key().as_ref().to_owned();
|
||||
let self_signing: CrossSigningKey = i.self_signing_key().as_ref().to_owned();
|
||||
|
||||
UserIdentity::Other {
|
||||
user_id: i.user_id().to_string(),
|
||||
master_key: serde_json::to_string(&master)?,
|
||||
self_signing_key: serde_json::to_string(&self_signing)?,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
use matrix_sdk_crypto::{
|
||||
CancelInfo as RustCancelInfo, QrVerification as InnerQr, Sas as InnerSas,
|
||||
VerificationRequest as InnerVerificationRequest,
|
||||
};
|
||||
|
||||
use crate::{OutgoingVerificationRequest, SignatureUploadRequest};
|
||||
|
||||
/// Enum representing the different verification flows we support.
|
||||
pub enum Verification {
|
||||
/// The `m.sas.v1` verification flow.
|
||||
SasV1 {
|
||||
#[allow(missing_docs)]
|
||||
sas: Sas,
|
||||
},
|
||||
/// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1`
|
||||
/// verification flow.
|
||||
QrCodeV1 {
|
||||
#[allow(missing_docs)]
|
||||
qrcode: QrCode,
|
||||
},
|
||||
}
|
||||
|
||||
/// The `m.sas.v1` verification flow.
|
||||
pub struct Sas {
|
||||
/// The other user that is participating in the verification flow
|
||||
pub other_user_id: String,
|
||||
/// The other user's device that is participating in the verification flow
|
||||
pub other_device_id: String,
|
||||
/// The unique ID of this verification flow, will be a random string for
|
||||
/// to-device events or a event ID for in-room events.
|
||||
pub flow_id: String,
|
||||
/// The room ID where this verification is happening, will be `None` if the
|
||||
/// verification is going through to-device messages
|
||||
pub room_id: Option<String>,
|
||||
/// Did we initiate the verification flow
|
||||
pub we_started: bool,
|
||||
/// Has the non-initiating side accepted the verification flow
|
||||
pub has_been_accepted: bool,
|
||||
/// Can the short auth string be presented
|
||||
pub can_be_presented: bool,
|
||||
/// Does the flow support the emoji representation of the short auth string
|
||||
pub supports_emoji: bool,
|
||||
/// Have we confirmed that the short auth strings match
|
||||
pub have_we_confirmed: bool,
|
||||
/// Has the verification completed successfully
|
||||
pub is_done: bool,
|
||||
/// Has the flow been cancelled
|
||||
pub is_cancelled: bool,
|
||||
/// Information about the cancellation of the flow, will be `None` if the
|
||||
/// flow hasn't been cancelled
|
||||
pub cancel_info: Option<CancelInfo>,
|
||||
}
|
||||
|
||||
/// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1`
|
||||
/// verification flow.
|
||||
pub struct QrCode {
|
||||
/// The other user that is participating in the verification flow
|
||||
pub other_user_id: String,
|
||||
/// The other user's device that is participating in the verification flow
|
||||
pub other_device_id: String,
|
||||
/// The unique ID of this verification flow, will be a random string for
|
||||
/// to-device events or a event ID for in-room events.
|
||||
pub flow_id: String,
|
||||
/// The room ID where this verification is happening, will be `None` if the
|
||||
/// verification is going through to-device messages
|
||||
pub room_id: Option<String>,
|
||||
/// Did we initiate the verification flow
|
||||
pub we_started: bool,
|
||||
/// Has the QR code been scanned by the other side
|
||||
pub other_side_scanned: bool,
|
||||
/// Has the scanning of the QR code been confirmed by us
|
||||
pub has_been_confirmed: bool,
|
||||
/// Did we scan the QR code and sent out a reciprocation
|
||||
pub reciprocated: bool,
|
||||
/// Has the verification completed successfully
|
||||
pub is_done: bool,
|
||||
/// Has the flow been cancelled
|
||||
pub is_cancelled: bool,
|
||||
/// Information about the cancellation of the flow, will be `None` if the
|
||||
/// flow hasn't been cancelled
|
||||
pub cancel_info: Option<CancelInfo>,
|
||||
}
|
||||
|
||||
impl From<InnerQr> for QrCode {
|
||||
fn from(qr: InnerQr) -> Self {
|
||||
Self {
|
||||
other_user_id: qr.other_user_id().to_string(),
|
||||
flow_id: qr.flow_id().as_str().to_owned(),
|
||||
is_cancelled: qr.is_cancelled(),
|
||||
is_done: qr.is_done(),
|
||||
cancel_info: qr.cancel_info().map(|c| c.into()),
|
||||
reciprocated: qr.reciprocated(),
|
||||
we_started: qr.we_started(),
|
||||
other_side_scanned: qr.has_been_scanned(),
|
||||
has_been_confirmed: qr.has_been_confirmed(),
|
||||
other_device_id: qr.other_device_id().to_string(),
|
||||
room_id: qr.room_id().map(|r| r.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information on why a verification flow has been cancelled and by whom.
|
||||
pub struct CancelInfo {
|
||||
/// The textual representation of the cancel reason
|
||||
pub reason: String,
|
||||
/// The code describing the cancel reason
|
||||
pub cancel_code: String,
|
||||
/// Was the verification flow cancelled by us
|
||||
pub cancelled_by_us: bool,
|
||||
}
|
||||
|
||||
impl From<RustCancelInfo> for CancelInfo {
|
||||
fn from(c: RustCancelInfo) -> Self {
|
||||
Self {
|
||||
reason: c.reason().to_owned(),
|
||||
cancel_code: c.cancel_code().to_string(),
|
||||
cancelled_by_us: c.cancelled_by_us(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A result type for starting SAS verifications.
|
||||
pub struct StartSasResult {
|
||||
/// The SAS verification object that got created.
|
||||
pub sas: Sas,
|
||||
/// The request that needs to be sent out to notify the other side that a
|
||||
/// SAS verification should start.
|
||||
pub request: OutgoingVerificationRequest,
|
||||
}
|
||||
|
||||
/// A result type for scanning QR codes.
|
||||
pub struct ScanResult {
|
||||
/// The QR code verification object that got created.
|
||||
pub qr: QrCode,
|
||||
/// The request that needs to be sent out to notify the other side that a
|
||||
/// QR code verification should start.
|
||||
pub request: OutgoingVerificationRequest,
|
||||
}
|
||||
|
||||
impl From<InnerSas> for Sas {
|
||||
fn from(sas: InnerSas) -> Self {
|
||||
Self {
|
||||
other_user_id: sas.other_user_id().to_string(),
|
||||
other_device_id: sas.other_device_id().to_string(),
|
||||
flow_id: sas.flow_id().as_str().to_owned(),
|
||||
is_cancelled: sas.is_cancelled(),
|
||||
is_done: sas.is_done(),
|
||||
can_be_presented: sas.can_be_presented(),
|
||||
supports_emoji: sas.supports_emoji(),
|
||||
have_we_confirmed: sas.have_we_confirmed(),
|
||||
we_started: sas.we_started(),
|
||||
room_id: sas.room_id().map(|r| r.to_string()),
|
||||
has_been_accepted: sas.has_been_accepted(),
|
||||
cancel_info: sas.cancel_info().map(|c| c.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A result type for requesting verifications.
|
||||
pub struct RequestVerificationResult {
|
||||
/// The verification request object that got created.
|
||||
pub verification: VerificationRequest,
|
||||
/// The request that needs to be sent out to notify the other side that
|
||||
/// we're requesting verification to begin.
|
||||
pub request: OutgoingVerificationRequest,
|
||||
}
|
||||
|
||||
/// A result type for confirming verifications.
|
||||
pub struct ConfirmVerificationResult {
|
||||
/// The requests that needs to be sent out to notify the other side that we
|
||||
/// confirmed the verification.
|
||||
pub requests: Vec<OutgoingVerificationRequest>,
|
||||
/// A request that will upload signatures of the verified device or user, if
|
||||
/// the verification is completed and we're able to sign devices or users
|
||||
pub signature_request: Option<SignatureUploadRequest>,
|
||||
}
|
||||
|
||||
/// The verificatoin request object which then can transition into some concrete
|
||||
/// verification method
|
||||
pub struct VerificationRequest {
|
||||
/// The other user that is participating in the verification flow
|
||||
pub other_user_id: String,
|
||||
/// The other user's device that is participating in the verification flow
|
||||
pub other_device_id: Option<String>,
|
||||
/// The unique ID of this verification flow, will be a random string for
|
||||
/// to-device events or a event ID for in-room events.
|
||||
pub flow_id: String,
|
||||
/// The room ID where this verification is happening, will be `None` if the
|
||||
/// verification is going through to-device messages
|
||||
pub room_id: Option<String>,
|
||||
/// Did we initiate the verification flow
|
||||
pub we_started: bool,
|
||||
/// Did both parties aggree to verification
|
||||
pub is_ready: bool,
|
||||
/// Did another device respond to the verification request
|
||||
pub is_passive: bool,
|
||||
/// Has the verification completed successfully
|
||||
pub is_done: bool,
|
||||
/// Has the flow been cancelled
|
||||
pub is_cancelled: bool,
|
||||
/// The list of verification methods that the other side advertised as
|
||||
/// supported
|
||||
pub their_methods: Option<Vec<String>>,
|
||||
/// The list of verification methods that we advertised as supported
|
||||
pub our_methods: Option<Vec<String>>,
|
||||
/// Information about the cancellation of the flow, will be `None` if the
|
||||
/// flow hasn't been cancelled
|
||||
pub cancel_info: Option<CancelInfo>,
|
||||
}
|
||||
|
||||
impl From<InnerVerificationRequest> for VerificationRequest {
|
||||
fn from(v: InnerVerificationRequest) -> Self {
|
||||
Self {
|
||||
other_user_id: v.other_user().to_string(),
|
||||
other_device_id: v.other_device_id().map(|d| d.to_string()),
|
||||
flow_id: v.flow_id().as_str().to_owned(),
|
||||
is_cancelled: v.is_cancelled(),
|
||||
is_done: v.is_done(),
|
||||
is_ready: v.is_ready(),
|
||||
room_id: v.room_id().map(|r| r.to_string()),
|
||||
we_started: v.we_started(),
|
||||
is_passive: v.is_passive(),
|
||||
cancel_info: v.cancel_info().map(|c| c.into()),
|
||||
their_methods: v
|
||||
.their_supported_methods()
|
||||
.map(|v| v.into_iter().map(|m| m.to_string()).collect()),
|
||||
our_methods: v
|
||||
.our_supported_methods()
|
||||
.map(|v| v.into_iter().map(|m| m.to_string()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
authors = ["Ivan Enderlin <ivane@element.io>"]
|
||||
description = "Matrix encryption library, for JavaScript"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-crypto-js"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = "1.60"
|
||||
version = "0.5.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ['-Oz']
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
qrcode = ["matrix-sdk-crypto/qrcode"]
|
||||
docsrs = []
|
||||
|
||||
[dependencies]
|
||||
matrix-sdk-crypto = { version = "0.5.0", path = "../matrix-sdk-crypto" }
|
||||
ruma = { version = "0.6.2", features = ["client-api-c", "js", "rand", "unstable-msc2676", "unstable-msc2677"] }
|
||||
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "d0e744287a14319c2a9148fef3747548c740fc36", features = ["js"] }
|
||||
wasm-bindgen = "0.2.80"
|
||||
wasm-bindgen-futures = "0.4.30"
|
||||
js-sys = "0.3.49"
|
||||
serde_json = "1.0.79"
|
||||
http = "0.2.6"
|
||||
anyhow = "1.0"
|
||||
@@ -0,0 +1,5 @@
|
||||
build:
|
||||
RUSTFLAGS='-C opt-level=z' wasm-pack build --release --target nodejs --out-name matrix_sdk_crypto --out-dir ./js/pkg ../
|
||||
|
||||
test:
|
||||
node --test ../tests/js/**.js
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Types related to events.
|
||||
|
||||
use ruma::events::room::history_visibility::HistoryVisibility as RumaHistoryVisibility;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Who can see a room's history.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HistoryVisibility {
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they were invited onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *invite* or *join*.
|
||||
Invited,
|
||||
|
||||
/// Previous events are accessible to newly joined members from
|
||||
/// the point they joined the room onwards.
|
||||
///
|
||||
/// Events stop being accessible when the member's state changes
|
||||
/// to something other than *join*.
|
||||
Joined,
|
||||
|
||||
/// Previous events are always accessible to newly joined members.
|
||||
///
|
||||
/// All events in the room are accessible, even those sent when
|
||||
/// the member was not a part of the room.
|
||||
Shared,
|
||||
|
||||
/// All events while this is the `HistoryVisibility` value may be
|
||||
/// shared by any participating homeserver with anyone, regardless
|
||||
/// of whether they have ever joined the room.
|
||||
WorldReadable,
|
||||
}
|
||||
|
||||
impl From<HistoryVisibility> for RumaHistoryVisibility {
|
||||
fn from(value: HistoryVisibility) -> Self {
|
||||
use HistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaHistoryVisibility> for HistoryVisibility {
|
||||
fn from(value: RumaHistoryVisibility) -> Self {
|
||||
use RumaHistoryVisibility::*;
|
||||
|
||||
match value {
|
||||
Invited => Self::Invited,
|
||||
Joined => Self::Joined,
|
||||
Shared => Self::Shared,
|
||||
WorldReadable => Self::WorldReadable,
|
||||
_ => unreachable!("Unknown variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::future::Future;
|
||||
|
||||
use js_sys::Promise;
|
||||
use wasm_bindgen::{JsValue, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
pub(crate) fn future_to_promise<F, T>(future: F) -> Promise
|
||||
where
|
||||
F: Future<Output = Result<T, anyhow::Error>> + 'static,
|
||||
T: Into<JsValue>,
|
||||
{
|
||||
let mut future = Some(future);
|
||||
|
||||
Promise::new(&mut |resolve, reject| {
|
||||
let future = future.take().unwrap_throw();
|
||||
|
||||
spawn_local(async move {
|
||||
match future.await {
|
||||
Ok(value) => resolve.call1(&JsValue::UNDEFINED, &value.into()).unwrap_throw(),
|
||||
Err(value) => {
|
||||
reject.call1(&JsValue::UNDEFINED, &value.to_string().into()).unwrap_throw()
|
||||
}
|
||||
};
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! Types for [Matrix](https://matrix.org/) identifiers for devices,
|
||||
//! events, keys, rooms, servers, users and URIs.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// A Matrix [user ID].
|
||||
///
|
||||
/// [user ID]: https://spec.matrix.org/v1.2/appendices/#user-identifiers
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserId {
|
||||
pub(crate) inner: ruma::OwnedUserId,
|
||||
}
|
||||
|
||||
impl UserId {
|
||||
pub(crate) fn new_with(inner: ruma::OwnedUserId) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl UserId {
|
||||
/// Parse/validate and create a new `UserId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<UserId, JsError> {
|
||||
Ok(Self::new_with(ruma::UserId::parse(id)?))
|
||||
}
|
||||
|
||||
/// Returns the user's localpart.
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the user ID.
|
||||
#[wasm_bindgen(js_name = "serverName")]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Whether this user ID is a historical one.
|
||||
///
|
||||
/// A historical user ID is one that doesn't conform to the latest
|
||||
/// specification of the user ID grammar but is still accepted
|
||||
/// because it was previously allowed.
|
||||
#[wasm_bindgen(getter, js_name = "isHistorical")]
|
||||
pub fn is_historical(&self) -> bool {
|
||||
self.inner.is_historical()
|
||||
}
|
||||
|
||||
/// Return the user ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix key ID.
|
||||
///
|
||||
/// Device identifiers in Matrix are completely opaque character
|
||||
/// sequences. This type is provided simply for its semantic value.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceId {
|
||||
pub(crate) inner: ruma::OwnedDeviceId,
|
||||
}
|
||||
|
||||
impl DeviceId {
|
||||
pub(crate) fn new_with(inner: ruma::OwnedDeviceId) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeviceId {
|
||||
/// Create a new `DeviceId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> DeviceId {
|
||||
Self::new_with(id.into())
|
||||
}
|
||||
|
||||
/// Return the device ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix [room ID].
|
||||
///
|
||||
/// [room ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoomId {
|
||||
pub(crate) inner: ruma::OwnedRoomId,
|
||||
}
|
||||
|
||||
impl RoomId {
|
||||
pub(crate) fn new_with(inner: ruma::OwnedRoomId) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RoomId {
|
||||
/// Parse/validate and create a new `RoomId`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: &str) -> Result<RoomId, JsError> {
|
||||
Ok(Self::new_with(ruma::RoomId::parse(id)?))
|
||||
}
|
||||
|
||||
/// Returns the user's localpart.
|
||||
pub fn localpart(&self) -> String {
|
||||
self.inner.localpart().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the server name of the room ID.
|
||||
#[wasm_bindgen(js_name = "serverName")]
|
||||
pub fn server_name(&self) -> ServerName {
|
||||
ServerName { inner: self.inner.server_name().to_owned() }
|
||||
}
|
||||
|
||||
/// Return the room ID as a string.
|
||||
#[wasm_bindgen(js_name = "toString")]
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.inner.as_str().to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Matrix-spec compliant [server name].
|
||||
///
|
||||
/// It consists of a host and an optional port (separated by a colon if
|
||||
/// present).
|
||||
///
|
||||
/// [server name]: https://spec.matrix.org/v1.2/appendices/#server-name
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct ServerName {
|
||||
inner: ruma::OwnedServerName,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ServerName {
|
||||
/// Parse/validate and create a new `ServerName`.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(name: &str) -> Result<ServerName, JsError> {
|
||||
Ok(Self { inner: ruma::ServerName::parse(name)? })
|
||||
}
|
||||
|
||||
/// Returns the host of the server name.
|
||||
///
|
||||
/// That is: Return the part of the server before `:<port>` or the
|
||||
/// full server name if there is no port.
|
||||
pub fn host(&self) -> String {
|
||||
self.inner.host().to_owned()
|
||||
}
|
||||
|
||||
/// Returns the port of the server name if any.
|
||||
pub fn port(&self) -> Option<u16> {
|
||||
self.inner.port()
|
||||
}
|
||||
|
||||
/// Returns true if and only if the server name is an IPv4 or IPv6
|
||||
/// address.
|
||||
#[wasm_bindgen(js_name = "isIpLiteral")]
|
||||
pub fn is_ip_literal(&self) -> bool {
|
||||
self.inner.is_ip_literal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2022 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.
|
||||
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
pub mod events;
|
||||
mod future;
|
||||
pub mod identifiers;
|
||||
pub mod machine;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
pub mod sync_events;
|
||||
|
||||
use js_sys::{Object, Reflect};
|
||||
use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*};
|
||||
|
||||
/// A really hacky and dirty code to downcast a `JsValue` to `T:
|
||||
/// RefFromWasmAbi`, inspired by
|
||||
/// https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288.
|
||||
///
|
||||
/// The returned value is likely to be a `wasm_bindgen::__ref::Ref<T>`.
|
||||
fn downcast<T>(value: &JsValue, classname: &str) -> Result<T::Anchor, JsError>
|
||||
where
|
||||
T: RefFromWasmAbi<Abi = u32>,
|
||||
{
|
||||
let constructor_name = Object::get_prototype_of(value).constructor().name();
|
||||
|
||||
if constructor_name == classname {
|
||||
let pointer = Reflect::get(value, &JsValue::from_str("ptr"))
|
||||
.map_err(|_| JsError::new("Failed to read the `JsValue` pointer"))?;
|
||||
let pointer = pointer
|
||||
.as_f64()
|
||||
.ok_or_else(|| JsError::new("Failed to read the `JsValue` pointer as a `f64`"))?
|
||||
as u32;
|
||||
|
||||
Ok(unsafe { T::ref_from_abi(pointer) })
|
||||
} else {
|
||||
Err(JsError::new(&format!(
|
||||
"Expect an `{}` instance, received `{}` instead",
|
||||
classname, constructor_name,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
//! The crypto specific Olm objects.
|
||||
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use js_sys::{Array, Map, Promise, Set};
|
||||
use ruma::{DeviceKeyAlgorithm, OwnedTransactionId, UInt};
|
||||
use serde_json::Value as JsonValue;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
downcast, events,
|
||||
future::future_to_promise,
|
||||
identifiers, requests,
|
||||
requests::OutgoingRequest,
|
||||
responses::{self, response_from_string},
|
||||
sync_events,
|
||||
};
|
||||
|
||||
/// State machine implementation of the Olm/Megolm encryption protocol
|
||||
/// used for Matrix end to end encryption.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OlmMachine {
|
||||
inner: matrix_sdk_crypto::OlmMachine,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl OlmMachine {
|
||||
/// Create a new memory based `OlmMachine`.
|
||||
///
|
||||
/// The created machine will keep the encryption keys only in
|
||||
/// memory and once the objects is dropped, the keys will be lost.
|
||||
///
|
||||
/// `user_id` represents the unique ID of the user that owns this
|
||||
/// machine. `device_id` represents the unique ID of the device
|
||||
/// that owns this machine.
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(user_id: &identifiers::UserId, device_id: &identifiers::DeviceId) -> Promise {
|
||||
let user_id = user_id.inner.clone();
|
||||
let device_id = device_id.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(OlmMachine {
|
||||
inner: matrix_sdk_crypto::OlmMachine::new(user_id.as_ref(), device_id.as_ref())
|
||||
.await,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The unique user ID that owns this `OlmMachine` instance.
|
||||
#[wasm_bindgen(js_name = "userId")]
|
||||
pub fn user_id(&self) -> identifiers::UserId {
|
||||
identifiers::UserId::new_with(self.inner.user_id().to_owned())
|
||||
}
|
||||
|
||||
/// The unique device ID that identifies this `OlmMachine`.
|
||||
#[wasm_bindgen(js_name = "deviceId")]
|
||||
pub fn device_id(&self) -> identifiers::DeviceId {
|
||||
identifiers::DeviceId::new_with(self.inner.device_id().to_owned())
|
||||
}
|
||||
|
||||
/// Get the public parts of our Olm identity keys.
|
||||
#[wasm_bindgen(js_name = "identityKeys")]
|
||||
pub fn identity_keys(&self) -> IdentityKeys {
|
||||
self.inner.identity_keys().into()
|
||||
}
|
||||
|
||||
/// Get the display name of our own device.
|
||||
#[wasm_bindgen(js_name = "displayName")]
|
||||
pub fn display_name(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.display_name().await?) })
|
||||
}
|
||||
|
||||
/// Get all the tracked users of our own device.
|
||||
///
|
||||
/// Returns a `Set<UserId>`.
|
||||
#[wasm_bindgen(js_name = "trackedUsers")]
|
||||
pub fn tracked_users(&self) -> Set {
|
||||
let set = Set::new(&JsValue::UNDEFINED);
|
||||
|
||||
self.inner.tracked_users().into_iter().map(identifiers::UserId::new_with).for_each(
|
||||
|user| {
|
||||
set.add(&user.into());
|
||||
},
|
||||
);
|
||||
|
||||
set
|
||||
}
|
||||
|
||||
/// Update the tracked users.
|
||||
///
|
||||
/// `users` is an iterator over user IDs that should be marked for
|
||||
/// tracking.
|
||||
///
|
||||
/// This will mark users that weren't seen before for a key query
|
||||
/// and tracking.
|
||||
///
|
||||
/// If the user is already known to the Olm machine, it will not
|
||||
/// be considered for a key query.
|
||||
#[wasm_bindgen(js_name = "updateTrackedUsers")]
|
||||
pub fn update_tracked_users(&self, users: &Array) -> Result<Promise, JsError> {
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
me.update_tracked_users(users.iter().map(AsRef::as_ref)).await;
|
||||
Ok(JsValue::UNDEFINED)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Handle to-device events and one-time key counts from a sync
|
||||
/// response.
|
||||
///
|
||||
/// This will decrypt and handle to-device events returning the
|
||||
/// decrypted versions of them.
|
||||
///
|
||||
/// To decrypt an event from the room timeline call
|
||||
/// `decrypt_room_event`.
|
||||
#[wasm_bindgen(js_name = "receiveSyncChanges")]
|
||||
pub fn receive_sync_changes(
|
||||
&self,
|
||||
to_device_events: &str,
|
||||
changed_devices: &sync_events::DeviceLists,
|
||||
one_time_key_counts: &Map,
|
||||
unused_fallback_keys: &Set,
|
||||
) -> Result<Promise, JsError> {
|
||||
let to_device_events = serde_json::from_str(to_device_events)?;
|
||||
let changed_devices = changed_devices.inner.clone();
|
||||
let one_time_key_counts: BTreeMap<DeviceKeyAlgorithm, UInt> = one_time_key_counts
|
||||
.entries()
|
||||
.into_iter()
|
||||
.filter_map(|js_value| {
|
||||
let pair = Array::from(&js_value.ok()?);
|
||||
let (key, value) = (
|
||||
DeviceKeyAlgorithm::from(pair.at(0).as_string()?),
|
||||
UInt::new(pair.at(1).as_f64()? as u64)?,
|
||||
);
|
||||
|
||||
Some((key, value))
|
||||
})
|
||||
.collect();
|
||||
let unused_fallback_keys: Option<Vec<DeviceKeyAlgorithm>> = Some(
|
||||
unused_fallback_keys
|
||||
.values()
|
||||
.into_iter()
|
||||
.filter_map(|js_value| Some(DeviceKeyAlgorithm::from(js_value.ok()?.as_string()?)))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.receive_sync_changes(
|
||||
to_device_events,
|
||||
&changed_devices,
|
||||
&one_time_key_counts,
|
||||
unused_fallback_keys.as_deref(),
|
||||
)
|
||||
.await?,
|
||||
)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the outgoing requests that need to be sent out.
|
||||
///
|
||||
/// This returns a list of `JsValue` to represent either:
|
||||
/// * `KeysUploadRequest`,
|
||||
/// * `KeysQueryRequest`,
|
||||
/// * `KeysClaimRequest`,
|
||||
/// * `ToDeviceRequest`,
|
||||
/// * `SignatureUploadRequest`,
|
||||
/// * `RoomMessageRequest` or
|
||||
/// * `KeysBackupRequest`.
|
||||
///
|
||||
/// Those requests need to be sent out to the server and the
|
||||
/// responses need to be passed back to the state machine using
|
||||
/// `mark_request_as_sent`.
|
||||
#[wasm_bindgen(js_name = "outgoingRequests")]
|
||||
pub fn outgoing_requests(&self) -> Promise {
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
Ok(me
|
||||
.outgoing_requests()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(OutgoingRequest)
|
||||
.map(TryFrom::try_from)
|
||||
.collect::<Result<Vec<JsValue>, _>>()?
|
||||
.into_iter()
|
||||
.collect::<Array>())
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark the request with the given request ID as sent (see
|
||||
/// `outgoing_requests`).
|
||||
///
|
||||
/// Arguments are:
|
||||
///
|
||||
/// * `request_id` represents the unique ID of the request that was sent
|
||||
/// out. This is needed to couple the response with the now sent out
|
||||
/// request.
|
||||
/// * `response_type` represents the type of the request that was sent out.
|
||||
/// * `response` represents the response that was received from the server
|
||||
/// after the outgoing request was sent out.
|
||||
#[wasm_bindgen(js_name = "markRequestAsSent")]
|
||||
pub fn mark_request_as_sent(
|
||||
&self,
|
||||
request_id: &str,
|
||||
request_type: requests::RequestType,
|
||||
response: &str,
|
||||
) -> Result<Promise, JsError> {
|
||||
let transaction_id = OwnedTransactionId::from(request_id);
|
||||
let response = response_from_string(response)?;
|
||||
let incoming_response = responses::OwnedResponse::try_from((request_type, response))?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(me.mark_request_as_sent(&transaction_id, &incoming_response).await.map(|_| true)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encrypt a room message for the given room.
|
||||
///
|
||||
/// Beware that a room key needs to be shared before this
|
||||
/// method can be called using the `share_room_key` method.
|
||||
///
|
||||
/// `room_id` is the ID of the room for which the message should
|
||||
/// be encrypted. `event_type` is the type of the event. `content`
|
||||
/// is the plaintext content of the message that should be
|
||||
/// encrypted.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if a group session for the given room wasn't shared
|
||||
/// beforehand.
|
||||
#[wasm_bindgen(js_name = "encryptRoomEvent")]
|
||||
pub fn encrypt_room_event(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
event_type: String,
|
||||
content: &str,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let content: JsonValue = serde_json::from_str(content)?;
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.encrypt_room_event_raw(&room_id, content, event_type.as_ref()).await?,
|
||||
)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Invalidate the currently active outbound group session for the
|
||||
/// given room.
|
||||
///
|
||||
/// Returns true if a session was invalidated, false if there was
|
||||
/// no session to invalidate.
|
||||
#[wasm_bindgen(js_name = "invalidateGroupSession")]
|
||||
pub fn invalidate_group_session(&self, room_id: &identifiers::RoomId) -> Promise {
|
||||
let room_id = room_id.inner.clone();
|
||||
let me = self.inner.clone();
|
||||
|
||||
future_to_promise(async move { Ok(me.invalidate_group_session(&room_id).await?) })
|
||||
}
|
||||
|
||||
/// Get to-device requests to share a room key with users in a room.
|
||||
///
|
||||
/// `room_id` is the room ID. `users` is an array of `UserId`
|
||||
/// objects. `encryption_settings` are an `EncryptionSettings`
|
||||
/// object.
|
||||
#[wasm_bindgen(js_name = "shareRoomKey")]
|
||||
pub fn share_room_key(
|
||||
&self,
|
||||
room_id: &identifiers::RoomId,
|
||||
users: &Array,
|
||||
encryption_settings: &EncryptionSettings,
|
||||
) -> Result<Promise, JsError> {
|
||||
let room_id = room_id.inner.clone();
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
let encryption_settings =
|
||||
matrix_sdk_crypto::olm::EncryptionSettings::from(encryption_settings);
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
Ok(serde_json::to_string(
|
||||
&me.share_room_key(&room_id, users.iter().map(AsRef::as_ref), encryption_settings)
|
||||
.await?,
|
||||
)?)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the a key claiming request for the user/device pairs that
|
||||
/// we are missing Olm sessions for.
|
||||
///
|
||||
/// Returns `NULL` if no key claiming request needs to be sent
|
||||
/// out, otherwise it returns an `Array` where the first key is
|
||||
/// the transaction ID as a string, and the second key is the keys
|
||||
/// claim request serialized to JSON.
|
||||
///
|
||||
/// Sessions need to be established between devices so group
|
||||
/// sessions for a room can be shared with them.
|
||||
///
|
||||
/// This should be called every time a group session needs to be
|
||||
/// shared as well as between sync calls. After a sync some
|
||||
/// devices may request room keys without us having a valid Olm
|
||||
/// session with them, making it impossible to server the room key
|
||||
/// request, thus it’s necessary to check for missing sessions
|
||||
/// between sync as well.
|
||||
///
|
||||
/// Note: Care should be taken that only one such request at a
|
||||
/// time is in flight, e.g. using a lock.
|
||||
///
|
||||
/// The response of a successful key claiming requests needs to be
|
||||
/// passed to the `OlmMachine` with the `mark_request_as_sent`.
|
||||
///
|
||||
/// `users` represents the list of users that we should check if
|
||||
/// we lack a session with one of their devices. This can be an
|
||||
/// empty iterator when calling this method between sync requests.
|
||||
#[wasm_bindgen(js_name = "getMissingSessions")]
|
||||
pub fn get_missing_sessions(&self, users: &Array) -> Result<Promise, JsError> {
|
||||
let users = users
|
||||
.iter()
|
||||
.map(|user| Ok(downcast::<identifiers::UserId>(&user, "UserId")?.inner.clone()))
|
||||
.collect::<Result<Vec<ruma::OwnedUserId>, JsError>>()?;
|
||||
|
||||
let me = self.inner.clone();
|
||||
|
||||
Ok(future_to_promise(async move {
|
||||
match me.get_missing_sessions(users.iter().map(AsRef::as_ref)).await? {
|
||||
Some((transaction_id, keys_claim_request)) => {
|
||||
Ok(JsValue::from(requests::KeysClaimRequest::try_from((
|
||||
transaction_id.to_string(),
|
||||
&keys_claim_request,
|
||||
))?))
|
||||
}
|
||||
|
||||
None => Ok(JsValue::NULL),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// An Ed25519 public key, used to verify digital signatures.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ed25519PublicKey {
|
||||
inner: vodozemac::Ed25519PublicKey,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Ed25519PublicKey {
|
||||
/// The number of bytes an Ed25519 public key has.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
vodozemac::Ed25519PublicKey::LENGTH
|
||||
}
|
||||
|
||||
/// Serialize an Ed25519 public key to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Curve25519 public key.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Curve25519PublicKey {
|
||||
inner: vodozemac::Curve25519PublicKey,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Curve25519PublicKey {
|
||||
/// The number of bytes a Curve25519 public key has.
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
vodozemac::Curve25519PublicKey::LENGTH
|
||||
}
|
||||
|
||||
/// Serialize an Curve25519 public key to an unpadded base64
|
||||
/// representation.
|
||||
#[wasm_bindgen(js_name = "toBase64")]
|
||||
pub fn to_base64(&self) -> String {
|
||||
self.inner.to_base64()
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct holding the two public identity keys of an account.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct IdentityKeys {
|
||||
/// The Ed25519 public key, used for signing.
|
||||
pub ed25519: Ed25519PublicKey,
|
||||
|
||||
/// The Curve25519 public key, used for establish shared secrets.
|
||||
pub curve25519: Curve25519PublicKey,
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::olm::IdentityKeys> for IdentityKeys {
|
||||
fn from(value: matrix_sdk_crypto::olm::IdentityKeys) -> Self {
|
||||
Self {
|
||||
ed25519: Ed25519PublicKey { inner: value.ed25519 },
|
||||
curve25519: Curve25519PublicKey { inner: value.curve25519 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An encryption algorithm to be used to encrypt messages sent to a
|
||||
/// room.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EncryptionAlgorithm {
|
||||
/// Olm version 1 using Curve25519, AES-256, and SHA-256.
|
||||
OlmV1Curve25519AesSha2,
|
||||
|
||||
/// Megolm version 1 using AES-256 and SHA-256.
|
||||
MegolmV1AesSha2,
|
||||
}
|
||||
|
||||
impl From<EncryptionAlgorithm> for ruma::EventEncryptionAlgorithm {
|
||||
fn from(value: EncryptionAlgorithm) -> Self {
|
||||
use EncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
|
||||
MegolmV1AesSha2 => Self::MegolmV1AesSha2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ruma::EventEncryptionAlgorithm> for EncryptionAlgorithm {
|
||||
fn from(value: ruma::EventEncryptionAlgorithm) -> Self {
|
||||
use ruma::EventEncryptionAlgorithm::*;
|
||||
|
||||
match value {
|
||||
OlmV1Curve25519AesSha2 => Self::OlmV1Curve25519AesSha2,
|
||||
MegolmV1AesSha2 => Self::MegolmV1AesSha2,
|
||||
_ => unreachable!("Unknown variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings for an encrypted room.
|
||||
///
|
||||
/// This determines the algorithm and rotation periods of a group
|
||||
/// session.
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncryptionSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EncryptionAlgorithm,
|
||||
|
||||
/// How long the session should be used before changing it,
|
||||
/// expressed in microseconds.
|
||||
#[wasm_bindgen(js_name = "rotationPeriod")]
|
||||
pub rotation_period: u64,
|
||||
|
||||
/// How many messages should be sent before changing the session.
|
||||
#[wasm_bindgen(js_name = "rotationPeriodMessages")]
|
||||
pub rotation_period_messages: u64,
|
||||
|
||||
/// The history visibility of the room when the session was
|
||||
/// created.
|
||||
#[wasm_bindgen(js_name = "historyVisibility")]
|
||||
pub history_visibility: events::HistoryVisibility,
|
||||
}
|
||||
|
||||
impl Default for EncryptionSettings {
|
||||
fn default() -> Self {
|
||||
let default = matrix_sdk_crypto::olm::EncryptionSettings::default();
|
||||
|
||||
Self {
|
||||
algorithm: default.algorithm.into(),
|
||||
rotation_period: default.rotation_period.as_micros().try_into().unwrap(),
|
||||
rotation_period_messages: default.rotation_period_msgs,
|
||||
history_visibility: default.history_visibility.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl EncryptionSettings {
|
||||
/// Create a new `EncryptionSettings` with default values.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> EncryptionSettings {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EncryptionSettings> for matrix_sdk_crypto::olm::EncryptionSettings {
|
||||
fn from(value: &EncryptionSettings) -> Self {
|
||||
Self {
|
||||
algorithm: value.algorithm.clone().into(),
|
||||
rotation_period: Duration::from_micros(value.rotation_period),
|
||||
rotation_period_msgs: value.rotation_period_messages,
|
||||
history_visibility: value.history_visibility.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user