Compare commits
1384 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b95ebe444e | |||
| e7c7b63b6e | |||
| cba22ae3b2 | |||
| 57b2f6ad22 | |||
| 8f1d8eeca2 | |||
| 513fbd8900 | |||
| 17097f4d42 | |||
| 58369fe7d0 | |||
| 43e213fd67 | |||
| ae5be67322 | |||
| b984fcca0c | |||
| bdd35206e8 | |||
| 0e84349d05 | |||
| 092ca90403 | |||
| 0fb3dedd1c | |||
| 3cf843d24f | |||
| baee5b2d11 | |||
| 34703bc0d6 | |||
| d212e7df18 | |||
| f8b09d4537 | |||
| 5d38bc3802 | |||
| 58d3b42a60 | |||
| b7986a5153 | |||
| c547f384bc | |||
| 29bba0b2ca | |||
| 80fac4bfa4 | |||
| be53913a16 | |||
| df1fe0ebc4 | |||
| 073b91fa62 | |||
| cc0388929a | |||
| b14d754aed | |||
| 00c3921d2a | |||
| 71aba433da | |||
| 1c8081533d | |||
| 7f364fd615 | |||
| ada71586ac | |||
| 533a5b92b0 | |||
| c4b1d3bc44 | |||
| 4cdb03e64b | |||
| da4876acee | |||
| dbf8cf231d | |||
| ba0cc3d45f | |||
| 1a5cd544e7 | |||
| 97c7baab14 | |||
| df42ef68a2 | |||
| 4a83e36195 | |||
| da673f1308 | |||
| c634efbe09 | |||
| 5fa2b05622 | |||
| 66551d28e4 | |||
| 7116fc1103 | |||
| d8b23f789d | |||
| 4dacef2e2c | |||
| d6ca3a27bb | |||
| 8d061447d6 | |||
| 38512d6a54 | |||
| f3bbcf553c | |||
| e1d905fbc6 | |||
| 1168c39c20 | |||
| 54063513a3 | |||
| 26788f83f0 | |||
| c705af1048 | |||
| c964589049 | |||
| 74d0ac7c77 | |||
| 3bac536daf | |||
| e18f248dbb | |||
| b6c7b317bf | |||
| 6f11244017 | |||
| 6b685b671d | |||
| eed2b37885 | |||
| f76cb1d123 | |||
| f36fb55727 | |||
| 74a6d39b9f | |||
| 0df782e93e | |||
| 7cca358399 | |||
| 96d4566111 | |||
| 31e00eb434 | |||
| 612fa46359 | |||
| 0a7fb2cbc3 | |||
| f9fb530480 | |||
| 2ec8893273 | |||
| bd5dda370d | |||
| ac04b0c36e | |||
| a04afac963 | |||
| cf98681f19 | |||
| cbcf673d21 | |||
| 9b20b00908 | |||
| f50d0cd3a6 | |||
| 1e48b15040 | |||
| b52f3fb11f | |||
| d877c1cf8c | |||
| 327445c6a0 | |||
| 8a5a0e511e | |||
| 12619ab8b3 | |||
| 069ef3a661 | |||
| 999f0899f8 | |||
| 681f32b0a7 | |||
| 0e514b755f | |||
| c0b30cadc9 | |||
| ee40d917d1 | |||
| 3f2fecd309 | |||
| d58a190712 | |||
| 3c72304e36 | |||
| 10b38ce44e | |||
| 3c9f929598 | |||
| d7e167498d | |||
| a959116af2 | |||
| 63dc939081 | |||
| 2becb88c35 | |||
| 6367cdddbf | |||
| 5e05b37d02 | |||
| df883d3328 | |||
| b805670c8a | |||
| 0c8e870bff | |||
| c318a6e847 | |||
| bdd51a323a | |||
| f619bbb884 | |||
| 37c23f1761 | |||
| 49c72e74f7 | |||
| 7609c7445c | |||
| a889bb3aca | |||
| 20454e1666 | |||
| aaa17535ac | |||
| bd5e112a46 | |||
| cc591cce1c | |||
| e058191b99 | |||
| 300189bb37 | |||
| d928f39f68 | |||
| 98c259dc1e | |||
| c174c4fda2 | |||
| 7a5daf6ac7 | |||
| ded5830deb | |||
| 59c8652ce8 | |||
| 5670700f7f | |||
| 79025e3f40 | |||
| c90e8ab483 | |||
| 64b5298881 | |||
| 8018b43443 | |||
| f49f5f1636 | |||
| b073323089 | |||
| 305766955b | |||
| 110b8eb8dd | |||
| fe17dce813 | |||
| c122549e0d | |||
| bb69901d94 | |||
| cd77441d1b | |||
| 5059d8b2c6 | |||
| ffea84b64a | |||
| 15540e84e3 | |||
| 0bdcc0fbf9 | |||
| 3f57a2a9f2 | |||
| 09a7858702 | |||
| ec55258be9 | |||
| 6b600d7e6d | |||
| 5f09d091cb | |||
| 2ef0c2959c | |||
| c85f4d4f0c | |||
| 4f7902d6f0 | |||
| 9863bc4a1c | |||
| 77c2a4ed4f | |||
| 4c09c6272b | |||
| da57061db0 | |||
| 753302394f | |||
| a2125adeee | |||
| 14bc4eb7e0 | |||
| 325531d13f | |||
| 87099676f9 | |||
| 3b24d33822 | |||
| eece920953 | |||
| 44eff7deb7 | |||
| 68b74c5ea9 | |||
| efe5b1e538 | |||
| a2ab6a9f23 | |||
| 1bda3659ce | |||
| 80d01b23c4 | |||
| dea3d4cb68 | |||
| b8017b1fb0 | |||
| 8dbbacfbe6 | |||
| 43b7072609 | |||
| cad888e69b | |||
| 5df9ae350c | |||
| d90e112c06 | |||
| 6048a1a507 | |||
| 233c4355d8 | |||
| e71cabc8f0 | |||
| 22b333a0d9 | |||
| c720abfa87 | |||
| 5d73dd7da7 | |||
| 5cf0fd2b85 | |||
| b3cf2c5899 | |||
| 4fc21a8860 | |||
| cff90b1480 | |||
| 5ed0c7a7b3 | |||
| 0e2017e537 | |||
| 1cc4f953b3 | |||
| f8bc9f3dc9 | |||
| 24e96df7ea | |||
| c569436ba4 | |||
| f6c4fdde7d | |||
| 3f2c5d22b6 | |||
| bd02ff901f | |||
| 242d46c9a1 | |||
| 5c882f89e8 | |||
| ab180362c9 | |||
| 28ddb9b70b | |||
| 910a45b3d5 | |||
| a1c0acbd0c | |||
| a7c2a645aa | |||
| 2a5ede9e1a | |||
| 324a0aafca | |||
| bfc7434f7e | |||
| e15f7264dc | |||
| 4a7be13961 | |||
| 65d84c111b | |||
| 78b7dcac61 | |||
| 796354ce5d | |||
| 95421f1713 | |||
| 1578067498 | |||
| 401cf282a7 | |||
| 3414a59b91 | |||
| 8c007510cd | |||
| f9d290746c | |||
| d4c56cc5b3 | |||
| 9e817a623b | |||
| 02331fa325 | |||
| 5637ca3080 | |||
| 975f9a0b41 | |||
| 4713af6aac | |||
| ba81c2460c | |||
| 5132971558 | |||
| 53b1845cbe | |||
| 893a5109ce | |||
| a97b01f3ce | |||
| be72c53d3e | |||
| b4b897dd51 | |||
| cb58c499b3 | |||
| ebcb2024d1 | |||
| dadd2fa68c | |||
| b5de203499 | |||
| 98ee4a3bca | |||
| fdb1e3482e | |||
| 999c99107d | |||
| 7c34ac4e82 | |||
| e72f4cee59 | |||
| 50423786f7 | |||
| 79eb07f717 | |||
| ff683602f2 | |||
| 74274e6dcb | |||
| 02b44ca9ba | |||
| 84b187ec12 | |||
| d35e730052 | |||
| ef6e481860 | |||
| 8679e81555 | |||
| 6f59e895b6 | |||
| 8a96b2c062 | |||
| ce4b809072 | |||
| e92b97eff6 | |||
| 51d915a181 | |||
| 9d0085d4dd | |||
| 35c7ae665d | |||
| 97385255d4 | |||
| cf90a18f13 | |||
| a9c37ba2d0 | |||
| 957bca1a14 | |||
| f0f6012871 | |||
| 15d5b234ed | |||
| 50d7e09347 | |||
| 12bf0f53a8 | |||
| bbe812f1d9 | |||
| dc74bc6116 | |||
| 382ec01bc3 | |||
| e9dff24ba7 | |||
| 19cacb1f26 | |||
| 5d66ff475f | |||
| 9aad775f01 | |||
| ec88e28fd2 | |||
| 387104e6e0 | |||
| de1bf2b89f | |||
| 8c1761faed | |||
| cbc8b53da1 | |||
| b110ee27fa | |||
| 5465a7b511 | |||
| 2f769726dd | |||
| 31dd031269 | |||
| 450036cf86 | |||
| a4bac499e9 | |||
| 2d6502247b | |||
| 88e230689e | |||
| 7c04c3a041 | |||
| 75ac29540d | |||
| ea7d90de62 | |||
| 880818a588 | |||
| e09a155cfc | |||
| 42c8c42150 | |||
| 587c09e700 | |||
| e9be23f853 | |||
| 7465574bdc | |||
| 593b5e55cb | |||
| d4e847f02f | |||
| a32f9187e6 | |||
| daf313e358 | |||
| 570bd2e358 | |||
| c8d4cd0a5b | |||
| 0c5d13cb91 | |||
| aff5cddb68 | |||
| 4a8c30527d | |||
| 560aa5b0a9 | |||
| a8bc619dca | |||
| 91c326e970 | |||
| e5585b57e8 | |||
| 61167fab15 | |||
| bc2c924c88 | |||
| 9332c55c8d | |||
| c5241af675 | |||
| f6f382e28a | |||
| 31f4a58f38 | |||
| 780348f546 | |||
| 93e5c34670 | |||
| 6597948564 | |||
| 693a0337a2 | |||
| 7729e2b11f | |||
| 00df34ed59 | |||
| 8f481dd859 | |||
| 123772c524 | |||
| cb91aa76fc | |||
| bb358909ef | |||
| 56a696d1c0 | |||
| 3f7eae8633 | |||
| 2b5e1744ee | |||
| df8c489304 | |||
| aa16a7e291 | |||
| 70ecf269d0 | |||
| 83926e154b | |||
| 42fb88a7f9 | |||
| 4ccb5a1cb9 | |||
| 48903a24d2 | |||
| e6f6665fa0 | |||
| 2e659afd26 | |||
| 3a08f0c278 | |||
| 9893ddba74 | |||
| 5c0f0140e9 | |||
| 1f5cad136e | |||
| 4c3cd29224 | |||
| ffaddb22b8 | |||
| 8ebd61dd18 | |||
| c8e769860b | |||
| e1d4fe533d | |||
| 447d78567a | |||
| d07ac997f2 | |||
| fc6ff4288e | |||
| c64567ba9b | |||
| 6e168051b6 | |||
| 2a09e588f3 | |||
| 5ca40b9893 | |||
| 6cc03d1c19 | |||
| 544881f11c | |||
| ef5d7ca579 | |||
| 1db89741bc | |||
| c39fa6543f | |||
| fe11ad7e3e | |||
| b6f2c43330 | |||
| e3e48148f0 | |||
| 2811c490a0 | |||
| 2e7f862f9c | |||
| e857172170 | |||
| b7fda1deb7 | |||
| c34f69f8a3 | |||
| e3d1de8e6c | |||
| 19b78be93f | |||
| e437aea012 | |||
| 155f975262 | |||
| e7e43a8bf0 | |||
| 0289f564b4 | |||
| 1e67f338ac | |||
| 36e3039d73 | |||
| fcd1c87765 | |||
| 1799721a5f | |||
| 9e83eaf2f5 | |||
| 347f79d08c | |||
| bdaed6237e | |||
| ca7117af2b | |||
| f3d4f6aab4 | |||
| 19e9884963 | |||
| 2e2d9b33a4 | |||
| a551ae2bee | |||
| 6a4ac8f361 | |||
| 42ec456abf | |||
| 585ca9fdf7 | |||
| b66c666997 | |||
| 92f0523e37 | |||
| 58691986a9 | |||
| 10da61c567 | |||
| bf4f32eccf | |||
| bc3ba3fab0 | |||
| d6c5a4d8aa | |||
| 81667173b6 | |||
| cb26e653da | |||
| 442103a37e | |||
| 094ead9d7d | |||
| 55430dd3d2 | |||
| b3cfa48b45 | |||
| 71a087c379 | |||
| fc085a7391 | |||
| b4a916b797 | |||
| 6cb2c8b468 | |||
| ac6dad3f35 | |||
| c1f9d3bc39 | |||
| eb8138ca6a | |||
| 44974982e1 | |||
| 077050efb4 | |||
| d10b85a05d | |||
| 9c98d0227b | |||
| 8028c23f56 | |||
| cb12bc1584 | |||
| b83399ba14 | |||
| 5daa22250f | |||
| c034de470b | |||
| 9cd217fc5d | |||
| cf07fc8e8e | |||
| 7b8d2b5319 | |||
| abd62cab0d | |||
| 7d45417a17 | |||
| 66ecb4c1e6 | |||
| 1483c22171 | |||
| ae0d810fb0 | |||
| 948c811d4b | |||
| ef2f20eb97 | |||
| 303ac513e5 | |||
| de4df4e50a | |||
| 2bcc0afb91 | |||
| 0a6b0e5804 | |||
| 4a06c9e82d | |||
| 06a973a1b8 | |||
| 28cc5acc87 | |||
| 2b5ff82414 | |||
| 3472c99c27 | |||
| 17f3dbb0a0 | |||
| 6a30514d40 | |||
| 4f4ba831c1 | |||
| b8fcc003ea | |||
| ef95d9b539 | |||
| 377b8ea75a | |||
| 4af9b74776 | |||
| d07063af2b | |||
| e5ba0298d0 | |||
| 4eb504d000 | |||
| 436530e874 | |||
| 1746690eda | |||
| 629a8ee84f | |||
| 5418c88775 | |||
| 14575892bd | |||
| 43a74524c5 | |||
| aadbc14dc6 | |||
| bab8fde0ac | |||
| 40c53f09ba | |||
| 508bf3b23d | |||
| 43ea9a16a0 | |||
| 3f3ae794a4 | |||
| 9efece4f7a | |||
| 077c20ed74 | |||
| 6c4888a123 | |||
| 643526987f | |||
| b311a31c9e | |||
| b8c6c2e07c | |||
| cdc93ddd0f | |||
| ccd8a4d602 | |||
| 4f2cad8f62 | |||
| f3acf582ec | |||
| 22b13c369b | |||
| 76ce3fecb3 | |||
| 99c1f70c1a | |||
| 8924865c9c | |||
| c6a80dc921 | |||
| 60950044f2 | |||
| 4c6c1d2107 | |||
| 2e3b6fba7d | |||
| de51291166 | |||
| e9d22c95a4 | |||
| 108d4ebffe | |||
| d84a852ae9 | |||
| e66add476f | |||
| 4afc6b2567 | |||
| 83b850d8f9 | |||
| e7e1d2d3eb | |||
| 74998c8dd8 | |||
| 0edef38eb7 | |||
| 807c58649d | |||
| bafe9a0f61 | |||
| f1140fec8b | |||
| f9f176ccfd | |||
| 16f94ecc1d | |||
| b995492457 | |||
| 0c81f3d9ae | |||
| c804104293 | |||
| 0952205e1e | |||
| 4d7da05b90 | |||
| d121a856c4 | |||
| 2384069641 | |||
| a29d2e39c4 | |||
| 0d99d8cc23 | |||
| e2225b2700 | |||
| 8857335a7d | |||
| 007e452d39 | |||
| 9245b2a89a | |||
| d39e3141fc | |||
| 1313c3da3c | |||
| 1bfb2d08a6 | |||
| c5709d23a5 | |||
| e25441babc | |||
| a370eb1e37 | |||
| f9af880176 | |||
| 7abf0c8805 | |||
| b119b30939 | |||
| 55436c6514 | |||
| ec863a928d | |||
| 1fd8c2052e | |||
| 897c6abe92 | |||
| f735107caf | |||
| 48f43a4af1 | |||
| 4ad4ad1e94 | |||
| 79102b3390 | |||
| d4327d4cfc | |||
| b6e28e2280 | |||
| b05fed5a3b | |||
| b4edaffbe1 | |||
| 45db95742a | |||
| 3a76cf7692 | |||
| 05b1384d16 | |||
| e245599913 | |||
| b16e3b6bd8 | |||
| 5105629c08 | |||
| 7570cf5ac2 | |||
| 6f35a05311 | |||
| b0ac9d3320 | |||
| 1bb5b42b1d | |||
| b9ddbb11af | |||
| a4e7dc1042 | |||
| ae33904a93 | |||
| a08f857e49 | |||
| 7198b0daba | |||
| 5babd71341 | |||
| d4ebe8cc83 | |||
| d9e5a17ab0 | |||
| b5c61af472 | |||
| fd705b7d5e | |||
| 8e53982bcd | |||
| ca4e738fff | |||
| 594e9b9e2d | |||
| 15b87e9dc1 | |||
| fa3583234c | |||
| 9679db6ddc | |||
| fdf48d1f30 | |||
| 40d13d9b59 | |||
| 4ab6ae7f30 | |||
| c8dd6bfd26 | |||
| 0a76f75c22 | |||
| 795c1225dd | |||
| b982d36303 | |||
| a80aa4c2ad | |||
| 27d9cf04de | |||
| e4779163b8 | |||
| 2cc899338a | |||
| 8dc56ec332 | |||
| 1d5ee22dd2 | |||
| 59917f45e3 | |||
| 35247fac2a | |||
| e24fb8b471 | |||
| 795900cf39 | |||
| 6d2d48a35a | |||
| 5c608ed474 | |||
| e38f0762ee | |||
| ab832da03e | |||
| de61798d78 | |||
| bca7f41ca9 | |||
| 7f503eb71c | |||
| a26dc3179a | |||
| b36d907fac | |||
| 886a1c7a77 | |||
| aa1a64628f | |||
| 0e66640b9f | |||
| 3f41e5071b | |||
| 9eb17e757c | |||
| 2bcdd0163b | |||
| f483a56f81 | |||
| a5131a0a73 | |||
| a7b31c90e0 | |||
| 8a842ec0a5 | |||
| 1b6bdc3307 | |||
| 1733808221 | |||
| 8291b93356 | |||
| 804bd221b2 | |||
| 45442dfac8 | |||
| e20b1efae9 | |||
| e65915e159 | |||
| 4800e80492 | |||
| 5d0ff961b2 | |||
| 270350cd34 | |||
| ae2391791d | |||
| 24592adbba | |||
| efe659910f | |||
| 08babb6d6c | |||
| 50bd408d48 | |||
| 27b5bf3ddd | |||
| 0e563a9a81 | |||
| 7dd834a214 | |||
| b4d0179c18 | |||
| 38048a2043 | |||
| ac2d90e92a | |||
| a8d6909c56 | |||
| fcb50956bb | |||
| ce4d53a88c | |||
| 7e9baf2707 | |||
| 43ced3d279 | |||
| 3073883076 | |||
| baa5bed1c9 | |||
| 7ec5a5ad1a | |||
| 0422bae924 | |||
| 27ecab8574 | |||
| de5f5cf00a | |||
| 35069c5252 | |||
| dadcc68336 | |||
| 64fff933af | |||
| c13d04ae18 | |||
| e84d3b9950 | |||
| 5ca66a6985 | |||
| 2e387436cf | |||
| 591f031246 | |||
| c1383402ed | |||
| a98f23e2a7 | |||
| 53daf40c7c | |||
| dedb1eb745 | |||
| c40edcf2fc | |||
| 6509e72a74 | |||
| 38fec7f2b3 | |||
| 9edf8657d0 | |||
| 95243003c4 | |||
| 3da1d3cf8f | |||
| 8ed8929788 | |||
| 133b230964 | |||
| 3a1eeb6a16 | |||
| 3f57ba57d0 | |||
| 11fcf5c42f | |||
| b27f1b0e34 | |||
| b67cd4ddd2 | |||
| 44cc1cef71 | |||
| 34bec59389 | |||
| cb95f576a5 | |||
| 5c530cf9ee | |||
| 30a78bb1d6 | |||
| 2077ea0ddf | |||
| e757d605f5 | |||
| 61a5293af5 | |||
| 6e83a4bbca | |||
| 5c14910126 | |||
| dc57873687 | |||
| 962f725d63 | |||
| c1e679147d | |||
| 4cc803fe27 | |||
| 8ed1e37cef | |||
| 5fd004bae5 | |||
| 9ce7feea1a | |||
| 7de002b128 | |||
| f60dc7ed78 | |||
| bdf32eecc7 | |||
| 78d7f6c10b | |||
| fa25ca4475 | |||
| c9db63509f | |||
| ac0df5dea9 | |||
| d175c47a05 | |||
| 959e8450af | |||
| dd0642cd59 | |||
| 6a7da5a8b6 | |||
| 5323e6e270 | |||
| 045ab25fb7 | |||
| cd3d90df3f | |||
| 92bedb4571 | |||
| 7cab7cadc9 | |||
| 728d80ed06 | |||
| 0c1d33d43f | |||
| 8f99180c99 | |||
| 0682292b91 | |||
| 404cc410cc | |||
| 17cc4fcb81 | |||
| 93f49265a6 | |||
| b1c8c64205 | |||
| 425a07d670 | |||
| 4262f1d3b0 | |||
| b5560d3cb6 | |||
| fc54c63a4c | |||
| e7a24d5e68 | |||
| b5c9473424 | |||
| 59d7b53242 | |||
| 59a7199202 | |||
| d1313b8614 | |||
| 4e8ce4cb5d | |||
| c85fe6bc21 | |||
| 3338ecf62a | |||
| 1c6a67d864 | |||
| e737200fbe | |||
| 41b3b0651f | |||
| 1cabc0cac9 | |||
| e0ee03fa6f | |||
| 4ec7aff301 | |||
| f5ff91935f | |||
| 7519bec9a3 | |||
| bf7070b8f2 | |||
| 39fc33d37a | |||
| d81a6e6872 | |||
| 2afc0c7661 | |||
| f349811020 | |||
| 0ab12fe969 | |||
| c4ac0d0570 | |||
| d039a39d84 | |||
| 1c008f549c | |||
| bd0ac703a0 | |||
| 6d2e9cfc02 | |||
| 661f182382 | |||
| 9ea4835e3e | |||
| 2602c36ad0 | |||
| d858940342 | |||
| 883183324f | |||
| 7d023ebdb9 | |||
| 473e49252e | |||
| d96c9f85a1 | |||
| 279ce0bba0 | |||
| da5ef42719 | |||
| a4eae1053c | |||
| f7039d9a8d | |||
| 723fdeaa06 | |||
| 19d513e3c0 | |||
| 23ac00c8ec | |||
| 4019ebf121 | |||
| 220ccfb52b | |||
| 9a838abd67 | |||
| 17d23eb9e5 | |||
| 8ea0035cd0 | |||
| 06b9c71dbc | |||
| 6a8ac62a51 | |||
| 1e894269c8 | |||
| 27c6f30e0f | |||
| bc48674f9f | |||
| e5f0f64405 | |||
| 09f4b07fb7 | |||
| 2ffac286ed | |||
| 83b48fb53c | |||
| f4137c6bba | |||
| e16b7f9c44 | |||
| 45953a268c | |||
| 84039ad7aa | |||
| 137fa9619f | |||
| e3d24f5c31 | |||
| 02c765f903 | |||
| fc6ff2c78a | |||
| bcdcdeb259 | |||
| 1d8f01ef11 | |||
| b58d88e0c3 | |||
| c8ca93c924 | |||
| d644af7be9 | |||
| ff2079da91 | |||
| 646f18ae18 | |||
| 2b8d4a21a4 | |||
| 78badd9af8 | |||
| 58aef51770 | |||
| 8fe1eda169 | |||
| 84066d4a76 | |||
| e1c220e2f7 | |||
| 798656dac5 | |||
| 721c459577 | |||
| 23173c4a1e | |||
| 4a8c5ebab0 | |||
| e29508938b | |||
| a357536ade | |||
| f3be27921c | |||
| 42c4cf2a30 | |||
| c5bece2d58 | |||
| 4662ca2e32 | |||
| 5a86b067e4 | |||
| 9a5345ec77 | |||
| 7c3e751d6e | |||
| 3070c98d26 | |||
| 95e906e0dc | |||
| 2e3d30d7b4 | |||
| 5b0457dad0 | |||
| a183584541 | |||
| 562bb5aee3 | |||
| dea3e4adf4 | |||
| 5d5d5bb141 | |||
| c58cf71be1 | |||
| af4b00195b | |||
| 41529a6bff | |||
| 300b03bd9e | |||
| a5b195efc7 | |||
| 692f9baa0e | |||
| 6b24d91ed9 | |||
| 24ce4881c7 | |||
| 849934b180 | |||
| 428b28a985 | |||
| 95145fae8f | |||
| ae894e0ff6 | |||
| 86d95518be | |||
| c8e459bc55 | |||
| 4d431b7c9e | |||
| 890e6cbc73 | |||
| e98960f30b | |||
| c500c06e4b | |||
| 3ac3be501f | |||
| 3573614640 | |||
| a60f60bd7d | |||
| a4980e8a04 | |||
| b628e6286a | |||
| fb47abcc17 | |||
| c2756a9a92 | |||
| 2d6882c495 | |||
| 51f3d90224 | |||
| 1a140ecc2f | |||
| f603696ff4 | |||
| ffd2843b0a | |||
| 618a58ba34 | |||
| 126ac3059b | |||
| 8af18a4df7 | |||
| 7790c3db8f | |||
| e3f4c1849c | |||
| 848156213b | |||
| 23e953d9cf | |||
| 464e181f66 | |||
| 7bd0e4975b | |||
| 127d4c225b | |||
| 9617d9aac9 | |||
| e828828ace | |||
| 3e9b0a8e7f | |||
| aff1e1d0a8 | |||
| 98f69aed41 | |||
| acfd0cdb07 | |||
| fc60593801 | |||
| 14226c0778 | |||
| 70ffc43ce0 | |||
| 9810a2f630 | |||
| 35f5117800 | |||
| d35cf56dc8 | |||
| 083cebe735 | |||
| faaf3f7a29 | |||
| 34cdf31cc5 | |||
| 6c7dbb814b | |||
| 217543ef38 | |||
| f57447527d | |||
| 8dbc7c38e5 | |||
| 5a069a8721 | |||
| 89efcee337 | |||
| 22daf0d81e | |||
| 53fec7a87e | |||
| adf8905d9f | |||
| 7b3dfe2f27 | |||
| 73c104cac1 | |||
| d86c05efb3 | |||
| cc236a8765 | |||
| 8b5bb7d8c5 | |||
| 2195da1cd8 | |||
| 65843f89dc | |||
| 8b56546565 | |||
| 8c4acf54e0 | |||
| c652762255 | |||
| 4bab678e46 | |||
| 81b127b6e7 | |||
| 269cfc3d34 | |||
| 987d87cd5d | |||
| 0de4a21320 | |||
| 6872cc717b | |||
| 5c4e46e908 | |||
| a2bfa08e09 | |||
| 977e29c3af | |||
| a2f7297941 | |||
| 6fa365935f | |||
| 39628a308b | |||
| 54391040a4 | |||
| 7a418ae09e | |||
| deff66ac42 | |||
| 2995cebd57 | |||
| ea4befabd9 | |||
| 6760f81498 | |||
| b3d1e8687e | |||
| 95c8708995 | |||
| 8d39821a1f | |||
| 2bcbf1eca4 | |||
| a5f06f772f | |||
| 2b389b920d | |||
| 298c260c5f | |||
| 72614e4252 | |||
| 8a71cec81a | |||
| a57c6159bd | |||
| 5f10f4301c | |||
| 176181bdcf | |||
| edea5e1c51 | |||
| b3941ca254 | |||
| c3c6428717 | |||
| de90da4adc | |||
| 002531349e | |||
| e38bfc64f4 | |||
| 93e1967119 | |||
| aee40977a3 | |||
| 9fe23227af | |||
| ce93869915 | |||
| 202c20feda | |||
| c307690c2e | |||
| 552a12eeed | |||
| c2ad298963 | |||
| d908d0f817 | |||
| 9edc876160 | |||
| 398edbbe0c | |||
| 89b56b5af8 | |||
| a57f63d614 | |||
| 74dd0a00d3 | |||
| b97e3d7bae | |||
| c3eb4d8106 | |||
| ea49a35b43 | |||
| a99e47c310 | |||
| 69fbe65ac4 | |||
| aaa15c768c | |||
| 58185e08e8 | |||
| 89c9e31140 | |||
| 1bd15b9fdd | |||
| 23126c4e48 | |||
| 6f5352b9a9 | |||
| eb16737d3b | |||
| 56309ae12c | |||
| 9fe0717cee | |||
| 7f23cbbeb5 | |||
| 3153a81cd2 | |||
| c3e593d998 | |||
| c2a386b889 | |||
| 317a141e07 | |||
| 3990e50ca6 | |||
| 90ea0229f2 | |||
| a42af5da69 | |||
| f63a01a85b | |||
| 27e1fb9a35 | |||
| c21517c61e | |||
| f626f2b24e | |||
| 37a7f69e03 | |||
| 38cf771f1f | |||
| 6d0b73cb3d | |||
| f96437a242 | |||
| 150862ec0c | |||
| 6db7eb0694 | |||
| 84c0311d80 | |||
| de097d3ca0 | |||
| 8aedc3077d | |||
| 0f26e7e3bc | |||
| 91db502cfe | |||
| 43aea6e482 | |||
| e778f7d72d | |||
| 94248523b3 | |||
| fd8377bce2 | |||
| 9e609a0fdf | |||
| 16a115d27e | |||
| 8167f5e9de | |||
| 5876c89858 | |||
| 5040be042f | |||
| ad2d3d2037 | |||
| 09f009ebd7 | |||
| 664d8c239c | |||
| 97ad060d4b | |||
| f4de3580b6 | |||
| 0fc5134563 | |||
| b0de9d1809 | |||
| 75fa7e97f9 | |||
| d21e8213b5 | |||
| 181c2a92de | |||
| 08d76f2ff4 | |||
| 5b758b8344 | |||
| 499f2796ba | |||
| df0444faa5 | |||
| b4c1b26f96 | |||
| 0245782cf4 | |||
| 87d0102663 | |||
| 6ee8b07cfe | |||
| 344631b4ee | |||
| f3e03c66a5 | |||
| d4e31f07a1 | |||
| d4de877e09 | |||
| 9b8e11aab9 | |||
| a0abffd026 | |||
| 4e99278eac | |||
| cdb8b5c1e9 | |||
| bf42e1a39f | |||
| 5883396106 | |||
| c6b0a19171 | |||
| 7ee0430054 | |||
| 36ca784690 | |||
| 2449bd27c1 | |||
| 29bd38734f | |||
| 6c07620a26 | |||
| 3e3894b573 | |||
| 0a26195472 | |||
| 0dc232b268 | |||
| c4465e7979 | |||
| 41f04d4f5d | |||
| 15d7deddb8 | |||
| 18e597aa79 | |||
| 407f9a3da8 | |||
| 82c3a795ff | |||
| ccda5c7260 | |||
| d706140a8f | |||
| 8351858be7 | |||
| 7cb25361b2 | |||
| 42a4ad60e8 | |||
| 9a325a4505 | |||
| fe572017b1 | |||
| c4ed5b6cda | |||
| 0d2f8c6d0f | |||
| fa1a40543c | |||
| 7637e79f2c | |||
| d0a5b86ff3 | |||
| 707b4c1185 | |||
| 9234ac96e1 | |||
| 2437a92998 | |||
| 947fa08dae | |||
| 8f4ac3da7f | |||
| 01bcbaf063 | |||
| 4770dc636a | |||
| 9294280dc1 | |||
| fba3298162 | |||
| ac2469d270 | |||
| db553b2040 | |||
| eeb6a811c0 | |||
| 528483ef0e | |||
| 72168ce084 | |||
| 6c85d3e28f | |||
| d5a853f3da | |||
| 8a2d6a4450 | |||
| c15ffb989a | |||
| 2b78f05aad | |||
| 1f0a96e31d | |||
| 6593cce778 | |||
| d7bcf42a2b | |||
| 18b655f829 | |||
| e2e70d6583 | |||
| c305b5052b | |||
| 6f4d2022fd | |||
| ef5201cf35 | |||
| 7bcdc2a3b6 | |||
| 7eeff64059 | |||
| 9c4229dc57 | |||
| 6c4e2fa508 | |||
| d5cd608045 | |||
| d83fc971ce | |||
| d96142b8cb | |||
| cd5d5da06a | |||
| 87bcba3561 | |||
| 81e9a7cefc | |||
| 3ddb2199d2 | |||
| 4abab73462 | |||
| 17fd85d687 | |||
| 279e88d9f9 | |||
| d016ce1848 | |||
| 591388d13e | |||
| a3b4cab22e | |||
| ffdb9c4a79 | |||
| cb8d5ce8fb | |||
| c10120602a | |||
| 47690bd268 | |||
| 807432b31f | |||
| 69d2a00759 | |||
| be01ee2de0 | |||
| 408fe5da4b | |||
| 28a7831ffd | |||
| 2bf8c99dfe | |||
| 77f0676a58 | |||
| e7b2a54e46 | |||
| 33e1601004 | |||
| 26ec0c6368 | |||
| 9f0fbcccf6 | |||
| 01ba94c670 | |||
| e431ba0bf5 | |||
| a3bb8a0d74 | |||
| 3245fbb1c9 | |||
| f4517c150c | |||
| e37229554b | |||
| df9da7539a | |||
| 1787d2ebe6 | |||
| faadb4953b | |||
| 021193087d | |||
| 0ac2b84c02 | |||
| 230b2a229f | |||
| ed1f12ce37 | |||
| 3f83941d57 | |||
| 91d7a8329e | |||
| 1a40491c0b | |||
| 79e661d1d9 | |||
| dce06d31aa | |||
| 3472614649 | |||
| 7ecd4a035f | |||
| 2ce0765206 | |||
| 108f6d90c9 | |||
| 7ceda2f39c | |||
| e00e94c6c3 | |||
| a71c7b2964 | |||
| 30c07b4e08 | |||
| d9fbc18777 | |||
| a58ace70a7 | |||
| 359c5280d7 | |||
| a07767d417 | |||
| 5a58fdff98 | |||
| 5058f09111 | |||
| 21b0afe72c | |||
| a726ebab39 | |||
| 2b124d98bc | |||
| 4634efc092 | |||
| 117ebeaf4b | |||
| 27f918e52d | |||
| 9facd86d81 | |||
| 7f2df68d62 | |||
| a6fa9f99fd | |||
| 7e95d85f17 | |||
| 57b65ec8c4 | |||
| 2d6fff7927 | |||
| 792623f53d | |||
| 6e67585bf6 | |||
| 5471c07244 | |||
| 0b04f7960b | |||
| 623f91733e | |||
| da3734ffc7 | |||
| 7128505768 | |||
| 6a96368048 | |||
| 8c9c843bfc | |||
| 094b2f90d6 | |||
| 2cbdca1f58 | |||
| d4fe2fe0a2 | |||
| 14db34beee | |||
| 7aea6160c3 | |||
| ca88539ec4 | |||
| 670755bfce | |||
| 46c1657643 | |||
| 9ac1417292 | |||
| de94b903d6 | |||
| 2f28976694 | |||
| 8ff8ea1342 | |||
| a1edef0ed5 | |||
| ee51ed78be | |||
| 2729f01e0f | |||
| e6730a7007 | |||
| 6fd852d573 | |||
| 7f2b268a59 | |||
| bb9adea5de | |||
| b1ae5534a1 | |||
| 9214f01185 | |||
| c35f73473e | |||
| bf54b17a2f | |||
| 4ce26f4fa0 | |||
| cdcbcdfab3 | |||
| 7a2d5c30db | |||
| a7bc1a95d3 | |||
| 4fa58bfaac | |||
| 7c92d91c04 | |||
| e612326714 | |||
| 9ef784d665 | |||
| 2481fbbd27 | |||
| a9d645cbcd | |||
| 578c927e58 | |||
| 24baf1fe0f | |||
| 861c07d5ce | |||
| 451d902604 | |||
| c3f00c96f8 | |||
| e50cf39a17 | |||
| 3f1439fe28 | |||
| 3d6872607e | |||
| fe33430e9b | |||
| b22324b305 | |||
| 037d62b165 | |||
| 8c39db002b | |||
| e27b6fb51e | |||
| e4f94cbfec | |||
| 807435c043 | |||
| 71f2a042c2 | |||
| 2e8fc3e232 | |||
| d273786d83 | |||
| 7ddc785a9a | |||
| 3e23affc9e | |||
| f2163164bf | |||
| 44dfbd2fa6 | |||
| 2f99d0de59 | |||
| 7a72949613 | |||
| b0241e51a3 | |||
| c5ea4fde35 | |||
| cc4ae3db1e | |||
| a5c5f5a7b1 | |||
| 7c46953805 | |||
| 2d83c40626 | |||
| d75101d042 | |||
| 04bb65f43e | |||
| c1ffed4fc9 | |||
| 0542e3d83d | |||
| 2d955027e1 | |||
| 38166135dc | |||
| 5bebe1d434 | |||
| a2a87b9fff | |||
| 497b973eb5 | |||
| de1988265d | |||
| 6cced25ae1 | |||
| 4e40c13b81 | |||
| bf152df322 | |||
| 3315cf5bc6 | |||
| 204279c575 | |||
| 83806b42e9 | |||
| fa0a22b090 | |||
| bce7fe0217 | |||
| 1fd21ee206 | |||
| 8a4a4140b3 | |||
| 62943f055d | |||
| ea149ebd8e | |||
| 32737a5517 | |||
| 1691a26163 | |||
| 51012e632e | |||
| 5d76fd9aac | |||
| c25f4c0642 | |||
| 9e48b7172b | |||
| 68125f5de6 | |||
| b602d3007d | |||
| 41cfbaf520 | |||
| 8206394918 | |||
| ca85564a9f | |||
| 7d9a699d62 | |||
| a38efc0f29 | |||
| 048a2000e7 | |||
| 18b444aac5 | |||
| a7a9ac24ed | |||
| b2ccb61864 | |||
| ac264918b8 | |||
| 8e19c583c6 | |||
| 740a5af068 | |||
| 8c3855221c | |||
| c2f1e4de64 | |||
| 8a7c53c00d | |||
| c1ae183795 | |||
| eea00301ff | |||
| 85522ac35a | |||
| 9b5f95672b | |||
| ffc5204109 | |||
| a607d70371 | |||
| 1fcb68c59f | |||
| 9bceb2f539 | |||
| 7003ea2d23 | |||
| 18ccd30c8c | |||
| eb19c19e36 | |||
| df2bcf6f1f | |||
| 3ee06be87b | |||
| 3a07a17e9d | |||
| 27eeeb8db6 | |||
| 6ded76a5a7 | |||
| 05a41d3b4d | |||
| 58d79ca9c6 | |||
| 4ee245dcce | |||
| 8daa12ac56 | |||
| 4134ba969a | |||
| a8f24da3ba | |||
| 390a1aa12c | |||
| b16724841d | |||
| ec81a5e539 | |||
| 949305da72 | |||
| 559306a33c | |||
| 24d2aa8078 | |||
| e70929317a | |||
| 62eeb3707f | |||
| 3fa06eeb99 | |||
| c0e6279837 | |||
| e7c70854ab | |||
| dcc3d6e755 | |||
| 2338d3e8fd | |||
| b83b9dc59d | |||
| 68822861d5 | |||
| b1e7bc77a4 | |||
| eb5949dbc2 | |||
| 7943baee49 | |||
| 7abdeed449 | |||
| eeebb43e32 | |||
| 5f49dab1fa | |||
| 6cacf83661 | |||
| 599c1ba98f | |||
| c2ec69cf44 | |||
| 32bdcede0c | |||
| 9af48920f6 | |||
| a3441429da | |||
| 9f957655d0 | |||
| 2663a17065 | |||
| 7124235d1b | |||
| babbdb4b90 | |||
| 18f6cbc23a | |||
| 583dbb07a5 | |||
| 25207a1586 | |||
| 283cf0d782 | |||
| 98d36d0ef0 | |||
| f33298b1a6 | |||
| 11aa306de2 | |||
| 669a3f22d2 | |||
| ff5f638b60 | |||
| 2a0c6c6474 | |||
| f447c55fcb | |||
| 84fc662614 | |||
| c57f076375 | |||
| 4561b94f33 | |||
| 9bd8699e18 | |||
| 5ef9a7b924 | |||
| bd56c52b37 | |||
| 1f25c4cf4b | |||
| 3f1a40a7d1 | |||
| b092ed0a82 | |||
| cd9252cc3d | |||
| 8b13602b3b | |||
| 92a43e7685 | |||
| 262a61afc9 | |||
| 1016519bb6 | |||
| 4dbe785bd7 | |||
| 676d547161 | |||
| 6a670163d3 | |||
| b8c4d1d5fa | |||
| 9e738f45ef | |||
| 8e8ac8c5ac | |||
| 4a7b3a103c | |||
| fc077bcd6b | |||
| c0c02baffc | |||
| 1174ccfc89 | |||
| 733689870e | |||
| 255451b8c7 | |||
| d4087a1aae | |||
| f07ac5d679 | |||
| 8b77b4171a | |||
| ea427cf366 | |||
| 15191d0230 | |||
| 5bd3c49afc | |||
| 765487dd9f | |||
| 03e53e991b | |||
| c3373f796b | |||
| 311e41ee0d | |||
| f8b5fceeb1 | |||
| 97b1bb6004 | |||
| 331cb02266 | |||
| 7751605e37 | |||
| a0eaa9c364 | |||
| 241d456a81 | |||
| 3e5b6bb460 | |||
| 5868c72662 | |||
| 4c184a30a2 | |||
| e4977d1d2a | |||
| ac069152b9 | |||
| 82827542b7 | |||
| 20a8e8e49b | |||
| 098cc1f9f8 | |||
| a3c46c6144 | |||
| f35fbdf8b0 | |||
| 442464add6 | |||
| abe40dff11 | |||
| b93eb0e318 | |||
| e6b67e5fa7 | |||
| 22ba253103 | |||
| a9fd63fd4b | |||
| 60a43439e5 | |||
| b6d7939685 | |||
| 4df0a839aa | |||
| e3cb3566bf | |||
| 9f34615869 | |||
| 05503b28b7 | |||
| 49e913865d | |||
| 4675a72e6b | |||
| d5f66631c1 | |||
| 81baca2f92 | |||
| 6e5870bd2b | |||
| 6df1f12b45 | |||
| 5abac19b72 | |||
| 62e959a94d | |||
| 54871f2af9 | |||
| 6a323525b5 | |||
| 1d00f79675 | |||
| 7201749280 | |||
| 5175cd8ddb | |||
| 21b33f4e61 | |||
| 9f34b371be | |||
| 587614cdd7 | |||
| db38bf1276 | |||
| 761071dac5 | |||
| 8f017e7b27 | |||
| b1864887aa | |||
| 9cb86596d8 | |||
| 8ee6c3bdc8 | |||
| 16f4021800 | |||
| 53876ea6e8 |
@@ -0,0 +1,258 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
style:
|
||||
name: Check style
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- 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 }}
|
||||
|
||||
test:
|
||||
name: ${{ matrix.name }}
|
||||
needs: [clippy]
|
||||
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
matrix:
|
||||
name:
|
||||
- linux / stable
|
||||
- linux / beta
|
||||
- macOS / stable
|
||||
|
||||
include:
|
||||
- name: linux / stable
|
||||
|
||||
- name: linux / beta
|
||||
rust: beta
|
||||
|
||||
- name: macOS / stable
|
||||
os: macOS-latest
|
||||
|
||||
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: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Code coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
code_coverage:
|
||||
name: Code Coverage
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install tarpaulin
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-tarpaulin -f
|
||||
|
||||
- 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
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
@@ -0,0 +1,37 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
|
||||
- name: Build docs
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options"
|
||||
with:
|
||||
command: doc
|
||||
args: --no-deps --workspace --features docs
|
||||
|
||||
- name: Deploy docs
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc/
|
||||
@@ -1,2 +1,4 @@
|
||||
Cargo.lock
|
||||
target
|
||||
master.zip
|
||||
emsdk-*
|
||||
@@ -0,0 +1,6 @@
|
||||
max_width = 100
|
||||
comment_width = 80
|
||||
wrap_comments = true
|
||||
imports_granularity = "Crate"
|
||||
use_small_heuristics = "Max"
|
||||
group_imports = "StdExternalCrate"
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
language: rust
|
||||
rust: stable
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libssl-dev
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- os: linux
|
||||
dist: bionic
|
||||
|
||||
- os: osx
|
||||
- os: linux
|
||||
name: Coverage
|
||||
before_script:
|
||||
- cargo install cargo-tarpaulin
|
||||
script:
|
||||
- cargo tarpaulin --out Xml
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
- os: linux
|
||||
name: wasm32-unknown-unknown
|
||||
before_script:
|
||||
- |
|
||||
set -e
|
||||
cargo install wasm-bindgen-cli
|
||||
rustup target add wasm32-unknown-unknown
|
||||
wget https://github.com/emscripten-core/emsdk/archive/master.zip
|
||||
unzip master.zip
|
||||
./emsdk-master/emsdk install latest
|
||||
./emsdk-master/emsdk activate latest
|
||||
script:
|
||||
- |
|
||||
set -e
|
||||
source emsdk-master/emsdk_env.sh
|
||||
cd matrix_sdk/examples/wasm_command_bot
|
||||
cargo build --target wasm32-unknown-unknown
|
||||
cd -
|
||||
|
||||
cd matrix_sdk_base
|
||||
cargo test --target wasm32-unknown-unknown --no-default-features
|
||||
|
||||
|
||||
|
||||
before_script:
|
||||
- rustup component add rustfmt
|
||||
|
||||
script:
|
||||
- cargo fmt --all -- --check
|
||||
- cargo build
|
||||
- cargo test
|
||||
@@ -1,9 +1,11 @@
|
||||
[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"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[](https://travis-ci.org/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://docs.rs/matrix-sdk)
|
||||
|
||||
# matrix-rust-sdk
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
A mitmproxy script that introduces certain request failures in a deterministic
|
||||
way.
|
||||
|
||||
Used mainly for Matrix style requests.
|
||||
|
||||
To run execute it with mitmproxy:
|
||||
|
||||
>>> mitmproxy -s failures.py`
|
||||
|
||||
"""
|
||||
import time
|
||||
import json
|
||||
import random
|
||||
|
||||
from mitmproxy import http
|
||||
from mitmproxy.script import concurrent
|
||||
|
||||
REQUEST_COUNT = 0
|
||||
|
||||
|
||||
def timeout(flow):
|
||||
timeout = 60 if "sync" in flow.request.pretty_url else 30
|
||||
time.sleep(timeout)
|
||||
return None
|
||||
|
||||
|
||||
# A map holding our failure modes.
|
||||
# The keys are just descriptive names for the failure mode while the values
|
||||
# hold a tuple containing a function that may or may not create a failure and
|
||||
# the probability weight at which rate this failure should be triggered.
|
||||
#
|
||||
# The method should return an http.HTTPResponse if it should modify the
|
||||
# response or None if the response should be passed as is.
|
||||
FAILURES = {
|
||||
"Success": (lambda x: None, 50),
|
||||
"Gateway error":
|
||||
(lambda _: http.HTTPResponse.make(500, b"Gateway error"), 20),
|
||||
"Limit exeeded": (lambda _: http.HTTPResponse.make(
|
||||
429,
|
||||
json.dumps({
|
||||
"errcode": "M_LIMIT_EXCEEDED",
|
||||
"error": "Too many requests",
|
||||
"retry_after_ms": 2000
|
||||
})), 20),
|
||||
"Timeout error": (timeout, 10)
|
||||
}
|
||||
|
||||
|
||||
@concurrent
|
||||
def request(flow):
|
||||
global FAILURES
|
||||
|
||||
weights = [weight for (_, weight) in FAILURES.values()]
|
||||
failure = random.choices(list(FAILURES), weights=weights)[0]
|
||||
failure_func, _ = FAILURES[failure]
|
||||
|
||||
response = failure_func(flow)
|
||||
|
||||
if response:
|
||||
flow.response = response
|
||||
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
A mitmproxy script that blocks and removes well known Matrix server
|
||||
information.
|
||||
|
||||
There are two ways a Matrix server can trigger the client to reconfigure the
|
||||
homeserver URL:
|
||||
|
||||
1. By responding to a `./well-known/matrix/client` request with a new
|
||||
homeserver URL.
|
||||
|
||||
2. By including a new homeserver URL inside the `/login` response.
|
||||
|
||||
To run execute it with mitmproxy:
|
||||
|
||||
>>> mitmproxy -s well-known-block.py`
|
||||
|
||||
"""
|
||||
import json
|
||||
|
||||
from mitmproxy import http
|
||||
|
||||
|
||||
def request(flow):
|
||||
if flow.request.path == "/.well-known/matrix/client":
|
||||
flow.response = http.HTTPResponse.make(
|
||||
404, # (optional) status code
|
||||
b"Not found", # (optional) content
|
||||
{"Content-Type": "text/html"} # (optional) headers
|
||||
)
|
||||
|
||||
|
||||
def response(flow: http.HTTPFlow):
|
||||
if flow.request.path == "/_matrix/client/r0/login":
|
||||
if flow.response.status_code == 200:
|
||||
body = json.loads(flow.response.content)
|
||||
body.pop("well_known", None)
|
||||
flow.response.text = json.dumps(body)
|
||||
@@ -1,100 +0,0 @@
|
||||
# Matrix Rust SDK
|
||||
|
||||
## Design and Layout
|
||||
|
||||
#### Async Client
|
||||
The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to
|
||||
- login
|
||||
- send messages
|
||||
- encryption ...
|
||||
- sync client state with the server
|
||||
- make raw Http requests
|
||||
|
||||
#### Base Client/Client State Machine
|
||||
In addition to Http, the `AsyncClient` passes along methods from the `BaseClient` that deal with `Room`s and `RoomMember`s. This allows the client to keep track of more complicated information that needs to be calculated in some way.
|
||||
- human-readable room names
|
||||
- power level?
|
||||
- ignored list?
|
||||
- push rulesset?
|
||||
- more?
|
||||
|
||||
#### Crypto State Machine
|
||||
Given a Matrix response the crypto machine will update its own internal state, along with encryption information. `BaseClient` and the crypto machine together keep track of when to encrypt. It knows when encryption needs to happen based on signals from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption-related requests. Basically it tells the `BaseClient` to send to-device messages out, and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state.
|
||||
|
||||
#### Client State/Room and RoomMember
|
||||
The `BaseClient` is responsible for keeping state in sync through the `IncomingResponse`s of `AsyncClient` or querying the `StateStore`. By processing and then delegating incoming `RoomEvent`s, `StateEvent`s, `PresenceEvent`, `IncomingAccountData` and `EphemeralEvent`s to the correct `Room` in the base clients `HashMap<RoomId, Room>` or further to `Room`'s `RoomMember` via the members `HashMap<UserId, RoomMember>`. The `BaseClient` is also responsible for emitting the incoming events to the `EventEmitter` trait.
|
||||
|
||||
```rust
|
||||
/// A Matrix room.
|
||||
pub struct Room {
|
||||
/// The unique id of the room.
|
||||
pub room_id: RoomId,
|
||||
/// The name of the room, clients use this to represent a room.
|
||||
pub room_name: RoomName,
|
||||
/// The mxid of our own user.
|
||||
pub own_user_id: UserId,
|
||||
/// The mxid of the room creator.
|
||||
pub creator: Option<UserId>,
|
||||
/// The map of room members.
|
||||
pub members: HashMap<UserId, RoomMember>,
|
||||
/// A list of users that are currently typing.
|
||||
pub typing_users: Vec<UserId>,
|
||||
/// The power level requirements for specific actions in this room
|
||||
pub power_levels: Option<PowerLevels>,
|
||||
// TODO when encryption events are handled we store algorithm used and rotation time.
|
||||
/// A flag indicating if the room is encrypted.
|
||||
pub encrypted: bool,
|
||||
/// Number of unread notifications with highlight flag set.
|
||||
pub unread_highlight: Option<UInt>,
|
||||
/// Number of unread notifications.
|
||||
pub unread_notifications: Option<UInt>,
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
pub struct RoomMember {
|
||||
/// The unique mxid of the user.
|
||||
pub user_id: UserId,
|
||||
/// The human readable name of the user.
|
||||
pub display_name: Option<String>,
|
||||
/// The matrix url of the users avatar.
|
||||
pub avatar_url: Option<String>,
|
||||
/// The time, in ms, since the user interacted with the server.
|
||||
pub last_active_ago: Option<UInt>,
|
||||
/// If the user should be considered active.
|
||||
pub currently_active: Option<bool>,
|
||||
/// The unique id of the room.
|
||||
pub room_id: Option<String>,
|
||||
/// If the member is typing.
|
||||
pub typing: Option<bool>,
|
||||
/// The presence of the user, if found.
|
||||
pub presence: Option<PresenceState>,
|
||||
/// The presence status message, if found.
|
||||
pub status_msg: Option<String>,
|
||||
/// The users power level.
|
||||
pub power_level: Option<Int>,
|
||||
/// The normalized power level of this `RoomMember` (0-100).
|
||||
pub power_level_norm: Option<Int>,
|
||||
/// The `MembershipState` of this `RoomMember`.
|
||||
pub membership: MembershipState,
|
||||
/// The human readable name of this room member.
|
||||
pub name: String,
|
||||
/// The events that created the state of this room member.
|
||||
pub events: Vec<Event>,
|
||||
/// The `PresenceEvent`s connected to this user.
|
||||
pub presence_events: Vec<PresenceEvent>,
|
||||
}
|
||||
```
|
||||
|
||||
#### State Store
|
||||
The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. The base client handles the storage automatically. There "may be/are TODO" ways for the user to interact directly. The room event handling methods signal if the state was modified; if so, we check if some room state file needs to be overwritten.
|
||||
- open
|
||||
- load client/rooms
|
||||
- store client/room
|
||||
- update ??
|
||||
|
||||
The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s main purpose is to provide load/store functionality and, internally to the crate, update the `BaseClient`.
|
||||
|
||||
#### Event Emitter
|
||||
The consumer of this crate can implement the `EventEmitter` trait for full control over how incoming events are handled by their client. If that isn't enough, it is possible to receive every incoming response with the `AsyncClient::sync_forever` callback.
|
||||
- list the methods for `EventEmitter`?
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "matrix-qrcode"
|
||||
description = "Library to encode and decode QR codes for interactive verifications in Matrix land"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docs"]
|
||||
rustdoc-args = ["--cfg", "feature=\"docs\""]
|
||||
|
||||
[features]
|
||||
default = ["decode_image"]
|
||||
decode_image = ["image", "rqrr", "qrcode/image", "qrcode/svg"]
|
||||
|
||||
docs = ["decode_image"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.13.0"
|
||||
byteorder = "1.4.3"
|
||||
image = { version = "0.23.14", optional = true }
|
||||
qrcode = { version = "0.12.0", default-features = false }
|
||||
rqrr = { version = "0.3.2", optional = true }
|
||||
ruma-identifiers = "0.19.3"
|
||||
thiserror = "1.0.25"
|
||||
@@ -0,0 +1,62 @@
|
||||
[](https://travis-ci.org/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)
|
||||
|
||||
# matrix-qrcode
|
||||
|
||||
**matrix-qrcode** is a crate to easily generate and parse QR codes for
|
||||
interactive verification using [QR codes] in Matrix.
|
||||
|
||||
[Matrix]: https://matrix.org/
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[QR codes]: https://spec.matrix.org/unstable/client-server-api/#qr-codes
|
||||
|
||||
## Usage
|
||||
|
||||
This is probably not the crate you are looking for, it's used internally in the
|
||||
matrix-rust-sdk.
|
||||
|
||||
If you still want to play with QR codes, here are a couple of helpful examples.
|
||||
|
||||
|
||||
### Decode an image
|
||||
|
||||
```rust
|
||||
use image;
|
||||
use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
|
||||
fn main() -> Result<(), DecodingError> {
|
||||
let image = image::open("/path/to/my/image.png").unwrap();
|
||||
let result = QrVerificationData::from_image(image)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Encode into a QR code
|
||||
|
||||
```rust
|
||||
use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
use image::Luma;
|
||||
|
||||
fn main() -> Result<(), DecodingError> {
|
||||
let data = b"MATRIX\
|
||||
\x02\x02\x00\x07\
|
||||
FLOW_ID\
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
SHARED_SECRET";
|
||||
|
||||
let data = QrVerificationData::from_bytes(data)?;
|
||||
let encoded = data.to_qr_code().unwrap();
|
||||
let image = encoded.render::<Luma<u8>>().build();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error type describing errors that happen while QR data is being decoded.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecodingError {
|
||||
/// Error decoding the QR code.
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
#[error(transparent)]
|
||||
Qr(#[from] rqrr::DeQRError),
|
||||
/// The QR code data is missing the mandatory Matrix header.
|
||||
#[error("the decoded QR code is missing the Matrix header")]
|
||||
Header,
|
||||
/// The QR code data is containing an invalid, non UTF-8, flow id.
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
/// The QR code data is using an unsupported or invalid verification mode.
|
||||
#[error("the QR code contains an invalid verification mode: {0}")]
|
||||
Mode(u8),
|
||||
/// The flow id is not a valid event ID.
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] ruma_identifiers::Error),
|
||||
#[error(transparent)]
|
||||
/// The QR code data does not contain all the necessary fields.
|
||||
Read(#[from] std::io::Error),
|
||||
/// The QR code data uses an invalid shared secret.
|
||||
#[error("the QR code contains a too short shared secret, length: {0}")]
|
||||
SharedSecret(usize),
|
||||
/// The QR code data uses an invalid or unsupported version.
|
||||
#[error("the QR code contains an invalid or unsupported version: {0}")]
|
||||
Version(u8),
|
||||
}
|
||||
|
||||
/// Error type describing errors that happen while QR data is being encoded.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EncodingError {
|
||||
/// Error generating a QR code from the data, likely because the data
|
||||
/// doesn't fit into a QR code.
|
||||
#[error(transparent)]
|
||||
Qr(#[from] qrcode::types::QrError),
|
||||
/// Error decoding the identity keys as base64.
|
||||
#[error(transparent)]
|
||||
Base64(#[from] base64::DecodeError),
|
||||
/// Error encoding the given flow id, the flow id is too large.
|
||||
#[error("The verification flow id length can't be converted into a u16: {0}")]
|
||||
FlowId(#[from] std::num::TryFromIntError),
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// 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.
|
||||
|
||||
//! This crate implements methods to parse and generate QR codes that are used
|
||||
//! for interactive verification in [Matrix](https://matrix.org/).
|
||||
//!
|
||||
//! It implements the QR format defined in the Matrix [spec].
|
||||
//!
|
||||
//! [spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
//! # fn main() -> Result<(), DecodingError> {
|
||||
//! use image;
|
||||
//!
|
||||
//! let image = image::open("/path/to/my/image.png").unwrap();
|
||||
//! let result = QrVerificationData::from_image(image)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(feature = "docs", feature(doc_cfg))]
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
dead_code,
|
||||
trivial_casts,
|
||||
missing_docs,
|
||||
trivial_numeric_casts,
|
||||
unused_extern_crates,
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
|
||||
mod error;
|
||||
mod types;
|
||||
mod utils;
|
||||
|
||||
pub use error::{DecodingError, EncodingError};
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
pub use image;
|
||||
pub use qrcode;
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
pub use rqrr;
|
||||
pub use types::{
|
||||
QrVerificationData, SelfVerificationData, SelfVerificationNoMasterKey, VerificationData,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[cfg(feature = "decode_image")]
|
||||
use std::{convert::TryFrom, io::Cursor};
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
use image::{ImageFormat, Luma};
|
||||
#[cfg(feature = "decode_image")]
|
||||
use qrcode::QrCode;
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
use crate::utils::decode_qr;
|
||||
use crate::{DecodingError, QrVerificationData};
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
static VERIFICATION: &[u8; 4277] = include_bytes!("../data/verification.png");
|
||||
#[cfg(feature = "decode_image")]
|
||||
static SELF_VERIFICATION: &[u8; 1467] = include_bytes!("../data/self-verification.png");
|
||||
#[cfg(feature = "decode_image")]
|
||||
static SELF_NO_MASTER: &[u8; 1775] = include_bytes!("../data/self-no-master.png");
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "decode_image")]
|
||||
fn decode_qr_test() {
|
||||
let image = Cursor::new(VERIFICATION);
|
||||
let image = image::load(image, ImageFormat::Png).unwrap().to_luma8();
|
||||
decode_qr(image).expect("Couldn't decode the QR code");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "decode_image")]
|
||||
fn decode_test() {
|
||||
let image = Cursor::new(VERIFICATION);
|
||||
let image = image::load(image, ImageFormat::Png).unwrap().to_luma8();
|
||||
let result = QrVerificationData::try_from(image).unwrap();
|
||||
|
||||
assert!(matches!(result, QrVerificationData::Verification(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "decode_image")]
|
||||
fn decode_encode_cycle() {
|
||||
let image = Cursor::new(VERIFICATION);
|
||||
let image = image::load(image, ImageFormat::Png).unwrap();
|
||||
let result = QrVerificationData::from_image(image).unwrap();
|
||||
|
||||
assert!(matches!(result, QrVerificationData::Verification(_)));
|
||||
|
||||
let encoded = result.to_qr_code().unwrap();
|
||||
let image = encoded.render::<Luma<u8>>().build();
|
||||
let second_result = QrVerificationData::try_from(image).unwrap();
|
||||
|
||||
assert_eq!(result, second_result);
|
||||
|
||||
let bytes = result.to_bytes().unwrap();
|
||||
let third_result = QrVerificationData::from_bytes(bytes).unwrap();
|
||||
|
||||
assert_eq!(result, third_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "decode_image")]
|
||||
fn decode_encode_cycle_self() {
|
||||
let image = Cursor::new(SELF_VERIFICATION);
|
||||
let image = image::load(image, ImageFormat::Png).unwrap();
|
||||
let result = QrVerificationData::try_from(image).unwrap();
|
||||
|
||||
assert!(matches!(result, QrVerificationData::SelfVerification(_)));
|
||||
|
||||
let encoded = result.to_qr_code().unwrap();
|
||||
let image = encoded.render::<Luma<u8>>().build();
|
||||
let second_result = QrVerificationData::from_luma(image).unwrap();
|
||||
|
||||
assert_eq!(result, second_result);
|
||||
|
||||
let bytes = result.to_bytes().unwrap();
|
||||
let third_result = QrVerificationData::from_bytes(bytes).unwrap();
|
||||
|
||||
assert_eq!(result, third_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "decode_image")]
|
||||
fn decode_encode_cycle_self_no_master() {
|
||||
let image = Cursor::new(SELF_NO_MASTER);
|
||||
let image = image::load(image, ImageFormat::Png).unwrap();
|
||||
let result = QrVerificationData::from_image(image).unwrap();
|
||||
|
||||
assert!(matches!(result, QrVerificationData::SelfVerificationNoMasterKey(_)));
|
||||
|
||||
let encoded = result.to_qr_code().unwrap();
|
||||
let image = encoded.render::<Luma<u8>>().build();
|
||||
let second_result = QrVerificationData::try_from(image).unwrap();
|
||||
|
||||
assert_eq!(result, second_result);
|
||||
|
||||
let bytes = result.to_bytes().unwrap();
|
||||
let third_result = QrVerificationData::try_from(bytes).unwrap();
|
||||
|
||||
assert_eq!(result, third_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "decode_image")]
|
||||
fn decode_invalid_qr() {
|
||||
let qr = QrCode::new(b"NonMatrixCode").expect("Can't build a simple QR code");
|
||||
let image = qr.render::<Luma<u8>>().build();
|
||||
let result = QrVerificationData::try_from(image);
|
||||
assert!(matches!(result, Err(DecodingError::Header)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_header() {
|
||||
let data = b"NonMatrixCode";
|
||||
let result = QrVerificationData::from_bytes(data);
|
||||
assert!(matches!(result, Err(DecodingError::Header)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_mode() {
|
||||
let data = b"MATRIX\x02\x03";
|
||||
let result = QrVerificationData::from_bytes(data);
|
||||
assert!(matches!(result, Err(DecodingError::Mode(3))))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_version() {
|
||||
let data = b"MATRIX\x01\x03";
|
||||
let result = QrVerificationData::from_bytes(data);
|
||||
assert!(matches!(result, Err(DecodingError::Version(1))))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_missing_data() {
|
||||
let data = b"MATRIX\x02\x02";
|
||||
let result = QrVerificationData::from_bytes(data);
|
||||
assert!(matches!(result, Err(DecodingError::Read(_))))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_short_secret() {
|
||||
let data = b"MATRIX\
|
||||
\x02\x02\x00\x07\
|
||||
FLOW_ID\
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
SECRET";
|
||||
|
||||
let result = QrVerificationData::from_bytes(data);
|
||||
assert!(matches!(result, Err(DecodingError::SharedSecret(_))))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_room_id() {
|
||||
let data = b"MATRIX\
|
||||
\x02\x00\x00\x0f\
|
||||
test:localhost\
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
SECRETISLONGENOUGH";
|
||||
|
||||
let result = QrVerificationData::from_bytes(data);
|
||||
assert!(matches!(result, Err(DecodingError::Identifier(_))))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
#[cfg(feature = "decode_image")]
|
||||
use image::{DynamicImage, ImageBuffer, Luma};
|
||||
use qrcode::QrCode;
|
||||
use ruma_identifiers::EventId;
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
use crate::utils::decode_qr;
|
||||
use crate::{
|
||||
error::{DecodingError, EncodingError},
|
||||
utils::{base_64_encode, to_bytes, to_qr_code, HEADER, MAX_MODE, MIN_SECRET_LEN, VERSION},
|
||||
};
|
||||
|
||||
/// An enum representing the different modes a QR verification can be in.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum QrVerificationData {
|
||||
/// The QR verification is verifying another user
|
||||
Verification(VerificationData),
|
||||
/// The QR verification is self-verifying and the current device trusts or
|
||||
/// owns the master key
|
||||
SelfVerification(SelfVerificationData),
|
||||
/// The QR verification is self-verifying in which the current device does
|
||||
/// not yet trust the master key
|
||||
SelfVerificationNoMasterKey(SelfVerificationNoMasterKey),
|
||||
}
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
impl TryFrom<DynamicImage> for QrVerificationData {
|
||||
type Error = DecodingError;
|
||||
|
||||
fn try_from(image: DynamicImage) -> Result<Self, Self::Error> {
|
||||
Self::from_image(image)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
impl TryFrom<ImageBuffer<Luma<u8>, Vec<u8>>> for QrVerificationData {
|
||||
type Error = DecodingError;
|
||||
|
||||
fn try_from(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<Self, Self::Error> {
|
||||
Self::from_luma(image)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for QrVerificationData {
|
||||
type Error = DecodingError;
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for QrVerificationData {
|
||||
type Error = DecodingError;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl QrVerificationData {
|
||||
/// Decode and parse an image of a QR code into a `QrVerificationData`
|
||||
///
|
||||
/// The image will be converted into a grey scale image before decoding is
|
||||
/// attempted
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - The image containing the QR code.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// use image;
|
||||
///
|
||||
/// let image = image::open("/path/to/my/image.png").unwrap();
|
||||
/// let result = QrVerificationData::from_image(image)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
pub fn from_image(image: DynamicImage) -> Result<Self, DecodingError> {
|
||||
let image = image.to_luma8();
|
||||
Self::decode(image)
|
||||
}
|
||||
|
||||
/// Decode and parse an grey scale image of a QR code into a
|
||||
/// `QrVerificationData`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - The grey scale image containing the QR code.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// use image;
|
||||
///
|
||||
/// let image = image::open("/path/to/my/image.png").unwrap();
|
||||
/// let image = image.to_luma8();
|
||||
/// let result = QrVerificationData::from_luma(image)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "decode_image")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
|
||||
pub fn from_luma(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<Self, DecodingError> {
|
||||
Self::decode(image)
|
||||
}
|
||||
|
||||
/// Parse the decoded payload of a QR code in byte slice form as a
|
||||
/// `QrVerificationData`
|
||||
///
|
||||
/// This method is useful if you would like to do your own custom QR code
|
||||
/// decoding.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `bytes` - The raw bytes of a decoded QR code.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// let data = b"MATRIX\
|
||||
/// \x02\x02\x00\x07\
|
||||
/// FLOW_ID\
|
||||
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
/// SHARED_SECRET";
|
||||
///
|
||||
/// let result = QrVerificationData::from_bytes(data)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, DecodingError> {
|
||||
Self::decode_bytes(bytes)
|
||||
}
|
||||
|
||||
/// Encode the `QrVerificationData` into a `QrCode`.
|
||||
///
|
||||
/// This method turns the `QrVerificationData` into a QR code that can be
|
||||
/// rendered and presented to be scanned.
|
||||
///
|
||||
/// The encoding can fail if the data doesn't fit into a QR code or if the
|
||||
/// identity keys that should be encoded into the QR code are not valid
|
||||
/// base64.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// let data = b"MATRIX\
|
||||
/// \x02\x02\x00\x07\
|
||||
/// FLOW_ID\
|
||||
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
/// SHARED_SECRET";
|
||||
///
|
||||
/// let result = QrVerificationData::from_bytes(data)?;
|
||||
/// let encoded = result.to_qr_code().unwrap();
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
|
||||
match self {
|
||||
QrVerificationData::Verification(v) => v.to_qr_code(),
|
||||
QrVerificationData::SelfVerification(v) => v.to_qr_code(),
|
||||
QrVerificationData::SelfVerificationNoMasterKey(v) => v.to_qr_code(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the `QrVerificationData` into a vector of bytes that can be
|
||||
/// encoded as a QR code.
|
||||
///
|
||||
/// The encoding can fail if the identity keys that should be encoded are
|
||||
/// not valid base64.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// let data = b"MATRIX\
|
||||
/// \x02\x02\x00\x07\
|
||||
/// FLOW_ID\
|
||||
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
/// SHARED_SECRET";
|
||||
///
|
||||
/// let result = QrVerificationData::from_bytes(data)?;
|
||||
/// let encoded = result.to_bytes().unwrap();
|
||||
///
|
||||
/// assert_eq!(data.as_ref(), encoded.as_slice());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
match self {
|
||||
QrVerificationData::Verification(v) => v.to_bytes(),
|
||||
QrVerificationData::SelfVerification(v) => v.to_bytes(),
|
||||
QrVerificationData::SelfVerificationNoMasterKey(v) => v.to_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the byte slice containing the decoded QR code data.
|
||||
///
|
||||
/// The format is defined in the [spec].
|
||||
///
|
||||
/// The byte slice consists of the following parts:
|
||||
///
|
||||
/// * the ASCII string MATRIX
|
||||
/// * one byte indicating the QR code version (must be 0x02)
|
||||
/// * one byte indicating the QR code verification mode. one of the
|
||||
/// following
|
||||
/// values:
|
||||
/// * 0x00 verifying another user with cross-signing
|
||||
/// * 0x01 self-verifying in which the current device does trust the
|
||||
/// master key
|
||||
/// * 0x02 self-verifying in which the current device does not yet trust
|
||||
/// the master key
|
||||
/// * the event ID or transaction_id of the associated verification request
|
||||
/// event, encoded as:
|
||||
/// * two bytes in network byte order (big-endian) indicating the length
|
||||
/// in bytes of the ID as a UTF-8 string
|
||||
/// * the ID as a UTF-8 string
|
||||
/// * the first key, as 32 bytes
|
||||
/// * the second key, as 32 bytes
|
||||
/// * a random shared secret, as a byte string. as we do not share the
|
||||
/// length of the secret, and it is not a fixed size, clients will just
|
||||
/// use the remainder of binary string as the shared secret.
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format
|
||||
fn decode_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, DecodingError> {
|
||||
let mut decoded = Cursor::new(bytes);
|
||||
|
||||
let mut header = [0u8; 6];
|
||||
let mut first_key = [0u8; 32];
|
||||
let mut second_key = [0u8; 32];
|
||||
|
||||
decoded.read_exact(&mut header)?;
|
||||
let version = decoded.read_u8()?;
|
||||
let mode = decoded.read_u8()?;
|
||||
|
||||
if header != HEADER {
|
||||
return Err(DecodingError::Header);
|
||||
} else if version != VERSION {
|
||||
return Err(DecodingError::Version(version));
|
||||
} else if mode > MAX_MODE {
|
||||
return Err(DecodingError::Mode(mode));
|
||||
}
|
||||
|
||||
let flow_id_len = decoded.read_u16::<BigEndian>()?;
|
||||
let mut flow_id = vec![0; flow_id_len.into()];
|
||||
|
||||
decoded.read_exact(&mut flow_id)?;
|
||||
decoded.read_exact(&mut first_key)?;
|
||||
decoded.read_exact(&mut second_key)?;
|
||||
|
||||
let mut shared_secret = Vec::new();
|
||||
|
||||
decoded.read_to_end(&mut shared_secret)?;
|
||||
|
||||
if shared_secret.len() < MIN_SECRET_LEN {
|
||||
return Err(DecodingError::SharedSecret(shared_secret.len()));
|
||||
}
|
||||
|
||||
QrVerificationData::new(mode, flow_id, first_key, second_key, shared_secret)
|
||||
}
|
||||
|
||||
/// Decode the given image of an QR code and if we find a valid code, try to
|
||||
/// decode it as a `QrVerification`.
|
||||
#[cfg(feature = "decode_image")]
|
||||
fn decode(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<QrVerificationData, DecodingError> {
|
||||
let decoded = decode_qr(image)?;
|
||||
Self::decode_bytes(decoded)
|
||||
}
|
||||
|
||||
fn new(
|
||||
mode: u8,
|
||||
flow_id: Vec<u8>,
|
||||
first_key: [u8; 32],
|
||||
second_key: [u8; 32],
|
||||
shared_secret: Vec<u8>,
|
||||
) -> Result<Self, DecodingError> {
|
||||
let first_key = base_64_encode(&first_key);
|
||||
let second_key = base_64_encode(&second_key);
|
||||
let flow_id = String::from_utf8(flow_id)?;
|
||||
let shared_secret = base_64_encode(&shared_secret);
|
||||
|
||||
match mode {
|
||||
VerificationData::QR_MODE => {
|
||||
let event_id = EventId::try_from(flow_id)?;
|
||||
Ok(VerificationData::new(event_id, first_key, second_key, shared_secret).into())
|
||||
}
|
||||
SelfVerificationData::QR_MODE => {
|
||||
Ok(SelfVerificationData::new(flow_id, first_key, second_key, shared_secret).into())
|
||||
}
|
||||
SelfVerificationNoMasterKey::QR_MODE => {
|
||||
Ok(SelfVerificationNoMasterKey::new(flow_id, first_key, second_key, shared_secret)
|
||||
.into())
|
||||
}
|
||||
m => Err(DecodingError::Mode(m)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the flow id for this `QrVerificationData`.
|
||||
///
|
||||
/// This represents the ID as a string even if it is a `EventId`.
|
||||
pub fn flow_id(&self) -> &str {
|
||||
match self {
|
||||
QrVerificationData::Verification(v) => v.event_id.as_str(),
|
||||
QrVerificationData::SelfVerification(v) => &v.transaction_id,
|
||||
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.transaction_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the first key of this `QrVerificationData`.
|
||||
pub fn first_key(&self) -> &str {
|
||||
match self {
|
||||
QrVerificationData::Verification(v) => &v.first_master_key,
|
||||
QrVerificationData::SelfVerification(v) => &v.master_key,
|
||||
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.device_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the second key of this `QrVerificationData`.
|
||||
pub fn second_key(&self) -> &str {
|
||||
match self {
|
||||
QrVerificationData::Verification(v) => &v.second_master_key,
|
||||
QrVerificationData::SelfVerification(v) => &v.device_key,
|
||||
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.master_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the secret of this `QrVerificationData`.
|
||||
pub fn secret(&self) -> &str {
|
||||
match self {
|
||||
QrVerificationData::Verification(v) => &v.shared_secret,
|
||||
QrVerificationData::SelfVerification(v) => &v.shared_secret,
|
||||
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.shared_secret,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The non-encoded data for the first mode of QR code verification.
|
||||
///
|
||||
/// This mode is used for verification between two users using their master
|
||||
/// cross signing keys.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VerificationData {
|
||||
event_id: EventId,
|
||||
first_master_key: String,
|
||||
second_master_key: String,
|
||||
shared_secret: String,
|
||||
}
|
||||
|
||||
impl VerificationData {
|
||||
const QR_MODE: u8 = 0x00;
|
||||
|
||||
/// Create a new `VerificationData` struct that can be encoded as a QR code.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `event_id` - The event id of the `m.key.verification.request` event
|
||||
/// that initiated the verification flow this QR code should be part of.
|
||||
///
|
||||
/// * `first_key` - Our own cross signing master key. Needs to be encoded as
|
||||
/// unpadded base64
|
||||
///
|
||||
/// * `second_key` - The cross signing master key of the other user.
|
||||
///
|
||||
/// * ` shared_secret` - A random bytestring encoded as unpadded base64,
|
||||
/// needs to be at least 8 bytes long.
|
||||
pub fn new(
|
||||
event_id: EventId,
|
||||
first_key: String,
|
||||
second_key: String,
|
||||
shared_secret: String,
|
||||
) -> Self {
|
||||
Self { event_id, first_master_key: first_key, second_master_key: second_key, shared_secret }
|
||||
}
|
||||
|
||||
/// Encode the `VerificationData` into a vector of bytes that can be
|
||||
/// encoded as a QR code.
|
||||
///
|
||||
/// The encoding can fail if the master keys that should be encoded are not
|
||||
/// valid base64.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// let data = b"MATRIX\
|
||||
/// \x02\x00\x00\x0f\
|
||||
/// $test:localhost\
|
||||
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
/// SHARED_SECRET";
|
||||
///
|
||||
/// let result = QrVerificationData::from_bytes(data)?;
|
||||
/// if let QrVerificationData::Verification(decoded) = result {
|
||||
/// let encoded = decoded.to_bytes().unwrap();
|
||||
/// assert_eq!(data.as_ref(), encoded.as_slice());
|
||||
/// } else {
|
||||
/// panic!("Data was encoded as an incorrect mode");
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
to_bytes(
|
||||
Self::QR_MODE,
|
||||
self.event_id.as_str(),
|
||||
&self.first_master_key,
|
||||
&self.second_master_key,
|
||||
&self.shared_secret,
|
||||
)
|
||||
}
|
||||
|
||||
/// Encode the `VerificationData` into a `QrCode`.
|
||||
///
|
||||
/// This method turns the `VerificationData` into a QR code that can be
|
||||
/// rendered and presented to be scanned.
|
||||
///
|
||||
/// The encoding can fail if the data doesn't fit into a QR code or if the
|
||||
/// keys that should be encoded into the QR code are not valid base64.
|
||||
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
|
||||
to_qr_code(
|
||||
Self::QR_MODE,
|
||||
self.event_id.as_str(),
|
||||
&self.first_master_key,
|
||||
&self.second_master_key,
|
||||
&self.shared_secret,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VerificationData> for QrVerificationData {
|
||||
fn from(data: VerificationData) -> Self {
|
||||
Self::Verification(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// The non-encoded data for the second mode of QR code verification.
|
||||
///
|
||||
/// This mode is used for verification between two devices of the same user
|
||||
/// where this device, that is creating this QR code, is trusting or owning
|
||||
/// the cross signing master key.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SelfVerificationData {
|
||||
transaction_id: String,
|
||||
master_key: String,
|
||||
device_key: String,
|
||||
shared_secret: String,
|
||||
}
|
||||
|
||||
impl SelfVerificationData {
|
||||
const QR_MODE: u8 = 0x01;
|
||||
|
||||
/// Create a new `SelfVerificationData` struct that can be encoded as a QR
|
||||
/// code.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `transaction_id` - The transaction id of this verification flow, the
|
||||
/// transaction id was sent by the `m.key.verification.request` event
|
||||
/// that initiated the verification flow this QR code should be part of.
|
||||
///
|
||||
/// * `master_key` - Our own cross signing master key. Needs to be encoded
|
||||
/// as
|
||||
/// unpadded base64
|
||||
///
|
||||
/// * `device_key` - The ed25519 key of the other device, encoded as
|
||||
/// unpadded base64.
|
||||
///
|
||||
/// * ` shared_secret` - A random bytestring encoded as unpadded base64,
|
||||
/// needs to be at least 8 bytes long.
|
||||
pub fn new(
|
||||
transaction_id: String,
|
||||
master_key: String,
|
||||
device_key: String,
|
||||
shared_secret: String,
|
||||
) -> Self {
|
||||
Self { transaction_id, master_key, device_key, shared_secret }
|
||||
}
|
||||
|
||||
/// Encode the `SelfVerificationData` into a vector of bytes that can be
|
||||
/// encoded as a QR code.
|
||||
///
|
||||
/// The encoding can fail if the keys that should be encoded are not valid
|
||||
/// base64.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// let data = b"MATRIX\
|
||||
/// \x02\x01\x00\x06\
|
||||
/// FLOWID\
|
||||
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
/// SHARED_SECRET";
|
||||
///
|
||||
/// let result = QrVerificationData::from_bytes(data)?;
|
||||
/// if let QrVerificationData::SelfVerification(decoded) = result {
|
||||
/// let encoded = decoded.to_bytes().unwrap();
|
||||
/// assert_eq!(data.as_ref(), encoded.as_slice());
|
||||
/// } else {
|
||||
/// panic!("Data was encoded as an incorrect mode");
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
to_bytes(
|
||||
Self::QR_MODE,
|
||||
&self.transaction_id,
|
||||
&self.master_key,
|
||||
&self.device_key,
|
||||
&self.shared_secret,
|
||||
)
|
||||
}
|
||||
|
||||
/// Encode the `SelfVerificationData` into a `QrCode`.
|
||||
///
|
||||
/// This method turns the `SelfVerificationData` into a QR code that can be
|
||||
/// rendered and presented to be scanned.
|
||||
///
|
||||
/// The encoding can fail if the data doesn't fit into a QR code or if the
|
||||
/// keys that should be encoded into the QR code are not valid base64.
|
||||
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
|
||||
to_qr_code(
|
||||
Self::QR_MODE,
|
||||
&self.transaction_id,
|
||||
&self.master_key,
|
||||
&self.device_key,
|
||||
&self.shared_secret,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SelfVerificationData> for QrVerificationData {
|
||||
fn from(data: SelfVerificationData) -> Self {
|
||||
Self::SelfVerification(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// The non-encoded data for the third mode of QR code verification.
|
||||
///
|
||||
/// This mode is used for verification between two devices of the same user
|
||||
/// where this device, that is creating this QR code, is not trusting the
|
||||
/// cross signing master key.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SelfVerificationNoMasterKey {
|
||||
transaction_id: String,
|
||||
device_key: String,
|
||||
master_key: String,
|
||||
shared_secret: String,
|
||||
}
|
||||
|
||||
impl SelfVerificationNoMasterKey {
|
||||
const QR_MODE: u8 = 0x02;
|
||||
|
||||
/// Create a new `SelfVerificationData` struct that can be encoded as a QR
|
||||
/// code.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `transaction_id` - The transaction id of this verification flow, the
|
||||
/// transaction id was sent by the `m.key.verification.request` event
|
||||
/// that initiated the verification flow this QR code should be part of.
|
||||
///
|
||||
/// * `device_key` - The ed25519 key of our own device, encoded as unpadded
|
||||
/// base64.
|
||||
///
|
||||
/// * `master_key` - Our own cross signing master key. Needs to be encoded
|
||||
/// as
|
||||
/// unpadded base64
|
||||
///
|
||||
/// * ` shared_secret` - A random bytestring encoded as unpadded base64,
|
||||
/// needs to be at least 8 bytes long.
|
||||
pub fn new(
|
||||
transaction_id: String,
|
||||
device_key: String,
|
||||
master_key: String,
|
||||
shared_secret: String,
|
||||
) -> Self {
|
||||
Self { transaction_id, device_key, master_key, shared_secret }
|
||||
}
|
||||
|
||||
/// Encode the `SelfVerificationNoMasterKey` into a vector of bytes that can
|
||||
/// be encoded as a QR code.
|
||||
///
|
||||
/// The encoding can fail if the keys that should be encoded are not valid
|
||||
/// base64.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
|
||||
/// # fn main() -> Result<(), DecodingError> {
|
||||
/// let data = b"MATRIX\
|
||||
/// \x02\x02\x00\x06\
|
||||
/// FLOWID\
|
||||
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
|
||||
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
|
||||
/// SHARED_SECRET";
|
||||
///
|
||||
/// let result = QrVerificationData::from_bytes(data)?;
|
||||
/// if let QrVerificationData::SelfVerificationNoMasterKey(decoded) = result {
|
||||
/// let encoded = decoded.to_bytes().unwrap();
|
||||
/// assert_eq!(data.as_ref(), encoded.as_slice());
|
||||
/// } else {
|
||||
/// panic!("Data was encoded as an incorrect mode");
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
|
||||
to_bytes(
|
||||
Self::QR_MODE,
|
||||
&self.transaction_id,
|
||||
&self.device_key,
|
||||
&self.master_key,
|
||||
&self.shared_secret,
|
||||
)
|
||||
}
|
||||
|
||||
/// Encode the `SelfVerificationNoMasterKey` into a `QrCode`.
|
||||
///
|
||||
/// This method turns the `SelfVerificationNoMasterKey` into a QR code that
|
||||
/// can be rendered and presented to be scanned.
|
||||
///
|
||||
/// The encoding can fail if the data doesn't fit into a QR code or if the
|
||||
/// keys that should be encoded into the QR code are not valid base64.
|
||||
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
|
||||
to_qr_code(
|
||||
Self::QR_MODE,
|
||||
&self.transaction_id,
|
||||
&self.device_key,
|
||||
&self.master_key,
|
||||
&self.shared_secret,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SelfVerificationNoMasterKey> for QrVerificationData {
|
||||
fn from(data: SelfVerificationNoMasterKey) -> Self {
|
||||
Self::SelfVerificationNoMasterKey(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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.
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use base64::{decode_config, encode_config, STANDARD_NO_PAD};
|
||||
#[cfg(feature = "decode_image")]
|
||||
use image::{ImageBuffer, Luma};
|
||||
use qrcode::{bits::Bits, EcLevel, QrCode, Version};
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
use crate::error::DecodingError;
|
||||
use crate::error::EncodingError;
|
||||
|
||||
pub(crate) const HEADER: &[u8] = b"MATRIX";
|
||||
pub(crate) const VERSION: u8 = 0x2;
|
||||
pub(crate) const MAX_MODE: u8 = 0x2;
|
||||
pub(crate) const MIN_SECRET_LEN: usize = 8;
|
||||
|
||||
pub(crate) fn base_64_encode(data: &[u8]) -> String {
|
||||
encode_config(data, STANDARD_NO_PAD)
|
||||
}
|
||||
|
||||
pub(crate) fn base64_decode(data: &str) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
decode_config(data, STANDARD_NO_PAD)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(
|
||||
mode: u8,
|
||||
flow_id: &str,
|
||||
first_key: &str,
|
||||
second_key: &str,
|
||||
shared_secret: &str,
|
||||
) -> Result<Vec<u8>, EncodingError> {
|
||||
let flow_id_len: u16 = flow_id.len().try_into()?;
|
||||
let flow_id_len = flow_id_len.to_be_bytes();
|
||||
|
||||
let first_key = base64_decode(first_key)?;
|
||||
let second_key = base64_decode(second_key)?;
|
||||
let shared_secret = base64_decode(shared_secret)?;
|
||||
|
||||
let data = [
|
||||
HEADER,
|
||||
&[VERSION],
|
||||
&[mode],
|
||||
flow_id_len.as_ref(),
|
||||
flow_id.as_bytes(),
|
||||
&first_key,
|
||||
&second_key,
|
||||
&shared_secret,
|
||||
]
|
||||
.concat();
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub(crate) fn to_qr_code(
|
||||
mode: u8,
|
||||
flow_id: &str,
|
||||
first_key: &str,
|
||||
second_key: &str,
|
||||
shared_secret: &str,
|
||||
) -> Result<QrCode, EncodingError> {
|
||||
let data = to_bytes(mode, flow_id, first_key, second_key, shared_secret)?;
|
||||
|
||||
// Mobile clients seem to have trouble decoding the QR code that gets
|
||||
// generated by `QrCode::new()` it seems to add a couple of data segments
|
||||
// with different data modes/types. The parsers seem to assume a single
|
||||
// data type and since we start with an ASCII `MATRIX` header the rest of
|
||||
// the data gets treated as a string as well.
|
||||
//
|
||||
// We make sure that there isn't an ECI bit set and we just push the bytes,
|
||||
// this seems to help since the decoder doesn't assume an encoding and
|
||||
// treats everything as raw bytes.
|
||||
let mut bits = Bits::new(Version::Normal(7));
|
||||
bits.push_byte_data(&data)?;
|
||||
bits.push_terminator(EcLevel::L)?;
|
||||
|
||||
Ok(QrCode::with_bits(bits, EcLevel::L)?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "decode_image")]
|
||||
pub(crate) fn decode_qr(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<Vec<u8>, DecodingError> {
|
||||
let mut image = rqrr::PreparedImage::prepare(image);
|
||||
let grids = image.detect_grids();
|
||||
|
||||
let mut error = None;
|
||||
|
||||
for grid in grids {
|
||||
let mut decoded = Vec::new();
|
||||
|
||||
match grid.decode_to(&mut decoded) {
|
||||
Ok(_) => {
|
||||
if decoded.starts_with(HEADER) {
|
||||
return Ok(decoded);
|
||||
}
|
||||
}
|
||||
Err(e) => error = Some(e),
|
||||
}
|
||||
}
|
||||
|
||||
Err(error.map(|e| e.into()).unwrap_or_else(|| DecodingError::Header))
|
||||
}
|
||||
+78
-26
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "A high level Matrix client-server library."
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
@@ -8,45 +8,97 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docs"]
|
||||
rustdoc-args = ["--cfg", "feature=\"docs\""]
|
||||
|
||||
[features]
|
||||
default = ["encryption", "sqlite-cryptostore"]
|
||||
messages = ["matrix-sdk-base/messages"]
|
||||
default = ["encryption", "sled_cryptostore", "sled_state_store", "require_auth_for_profile_requests", "native-tls"]
|
||||
|
||||
encryption = ["matrix-sdk-base/encryption"]
|
||||
sqlite-cryptostore = ["matrix-sdk-base/sqlite-cryptostore"]
|
||||
sled_state_store = ["matrix-sdk-base/sled_state_store"]
|
||||
sled_cryptostore = ["matrix-sdk-base/sled_cryptostore"]
|
||||
markdown = ["matrix-sdk-base/markdown"]
|
||||
native-tls = ["reqwest/native-tls"]
|
||||
rustls-tls = ["reqwest/rustls-tls"]
|
||||
socks = ["reqwest/socks"]
|
||||
sso_login = ["warp", "rand", "tokio-stream"]
|
||||
require_auth_for_profile_requests = []
|
||||
appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper", "ruma/rand"]
|
||||
|
||||
docs = ["encryption", "sled_cryptostore", "sled_state_store", "sso_login"]
|
||||
|
||||
[dependencies]
|
||||
http = "0.2.1"
|
||||
reqwest = "0.10.4"
|
||||
serde_json = "1.0.53"
|
||||
thiserror = "1.0.19"
|
||||
tracing = "0.1.14"
|
||||
url = "2.1.1"
|
||||
futures-timer = { version = "3.0.2", features = ["wasm-bindgen"] }
|
||||
dashmap = "4.0.2"
|
||||
futures = "0.3.15"
|
||||
http = "0.2.4"
|
||||
serde_json = "1.0.64"
|
||||
thiserror = "1.0.25"
|
||||
tracing = "0.1.26"
|
||||
url = "2.2.2"
|
||||
zeroize = "1.3.0"
|
||||
mime = "0.3.16"
|
||||
rand = { version = "0.8.4", optional = true }
|
||||
bytes = "1.0.1"
|
||||
|
||||
|
||||
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
|
||||
matrix-sdk-common = { version = "0.3.0", path = "../matrix_sdk_common" }
|
||||
|
||||
[dependencies.matrix-sdk-base]
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
path = "../matrix_sdk_base"
|
||||
default_features = false
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11.3"
|
||||
default_features = false
|
||||
|
||||
[dependencies.ruma]
|
||||
version = "0.2.0"
|
||||
features = ["client-api-c", "compat", "unstable-pre-spec"]
|
||||
|
||||
[dependencies.tokio-stream]
|
||||
version = "0.1.6"
|
||||
features = ["net"]
|
||||
optional = true
|
||||
|
||||
[dependencies.warp]
|
||||
version = "0.3.1"
|
||||
default-features = false
|
||||
optional = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.backoff]
|
||||
version = "0.3.0"
|
||||
features = ["tokio"]
|
||||
|
||||
[dependencies.tracing-futures]
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
default-features = false
|
||||
features = ["std", "std-future"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
futures-timer = "3.0.2"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||
version = "1.7.1"
|
||||
default-features = false
|
||||
features = ["fs", "rt"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.futures-timer]
|
||||
version = "3.0.2"
|
||||
features = ["wasm-bindgen"]
|
||||
|
||||
[dev-dependencies]
|
||||
async-trait = "0.1.31"
|
||||
dirs = "2.0.2"
|
||||
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
|
||||
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
|
||||
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
|
||||
serde_json = "1.0.53"
|
||||
tracing-subscriber = "0.2.5"
|
||||
tempfile = "3.1.0"
|
||||
mockito = "0.25.1"
|
||||
dirs = "3.0.2"
|
||||
matrix-sdk-test = { version = "0.3.0", path = "../matrix_sdk_test" }
|
||||
tokio = { version = "1.7.1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
serde_json = "1.0.64"
|
||||
tracing-subscriber = "0.2.18"
|
||||
tempfile = "3.2.0"
|
||||
mockito = "0.30.0"
|
||||
lazy_static = "1.4.0"
|
||||
futures = "0.3.5"
|
||||
|
||||
[[example]]
|
||||
name = "emoji_verification"
|
||||
required-features = ["encryption"]
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::{env, process::exit};
|
||||
|
||||
use matrix_sdk::{
|
||||
self, async_trait,
|
||||
events::{room::member::MemberEventContent, StrippedStateEvent},
|
||||
room::Room,
|
||||
Client, ClientConfig, EventHandler, SyncSettings,
|
||||
};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use url::Url;
|
||||
|
||||
struct AutoJoinBot {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl AutoJoinBot {
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for AutoJoinBot {
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
room: Room,
|
||||
room_member: &StrippedStateEvent<MemberEventContent>,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
if room_member.state_key != self.client.user_id().await.unwrap() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Room::Invited(room) = room {
|
||||
println!("Autojoining room {}", room.room_id());
|
||||
let mut delay = 2;
|
||||
|
||||
while let Err(err) = room.accept_invitation().await {
|
||||
// retry autojoin due to synapse sending invites, before the
|
||||
// invited user can join for more information see
|
||||
// https://github.com/matrix-org/synapse/issues/4345
|
||||
eprintln!(
|
||||
"Failed to join room {} ({:?}), retrying in {}s",
|
||||
room.room_id(),
|
||||
err,
|
||||
delay
|
||||
);
|
||||
|
||||
sleep(Duration::from_secs(delay)).await;
|
||||
delay *= 2;
|
||||
|
||||
if delay > 3600 {
|
||||
eprintln!("Can't join room {} ({:?})", room.room_id(), err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("Successfully joined room {}", room.room_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_and_sync(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let mut home = dirs::home_dir().expect("no home directory found");
|
||||
home.push("autojoin_bot");
|
||||
|
||||
let client_config = ClientConfig::new().store_path(home);
|
||||
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
client.login(username, password, None, Some("autojoin bot")).await?;
|
||||
|
||||
println!("logged in as {}", username);
|
||||
|
||||
client.set_event_handler(Box::new(AutoJoinBot::new(client.clone()))).await;
|
||||
|
||||
client.sync(SyncSettings::default()).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
login_and_sync(homeserver_url, &username, &password).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,56 +1,52 @@
|
||||
use std::{env, process::exit};
|
||||
|
||||
use matrix_sdk::{
|
||||
self,
|
||||
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings,
|
||||
self, async_trait,
|
||||
events::{
|
||||
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
|
||||
AnyMessageEventContent, SyncMessageEvent,
|
||||
},
|
||||
room::Room,
|
||||
Client, ClientConfig, EventHandler, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
struct CommandBot {
|
||||
/// This clone of the `Client` will send requests to the server,
|
||||
/// while the other keeps us in sync with the server using `sync_forever`.
|
||||
client: Client,
|
||||
}
|
||||
struct CommandBot;
|
||||
|
||||
impl CommandBot {
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventEmitter for CommandBot {
|
||||
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
|
||||
if let SyncRoom::Joined(room) = room {
|
||||
let msg_body = if let MessageEvent {
|
||||
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
#[async_trait]
|
||||
impl EventHandler for CommandBot {
|
||||
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
if let Room::Joined(room) = room {
|
||||
let msg_body = if let SyncMessageEvent {
|
||||
content:
|
||||
MessageEventContent {
|
||||
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
..
|
||||
},
|
||||
..
|
||||
} = event
|
||||
{
|
||||
msg_body.clone()
|
||||
msg_body
|
||||
} else {
|
||||
String::new()
|
||||
return;
|
||||
};
|
||||
|
||||
if msg_body.contains("!party") {
|
||||
let content = MessageEventContent::Text(TextMessageEventContent {
|
||||
body: "🎉🎊🥳 let's PARTY!! 🥳🎊🎉".to_string(),
|
||||
format: None,
|
||||
formatted_body: None,
|
||||
relates_to: None,
|
||||
});
|
||||
// we clone here to hold the lock for as little time as possible.
|
||||
let room_id = room.read().await.room_id.clone();
|
||||
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::text_plain(
|
||||
"🎉🎊🥳 let's PARTY!! 🥳🎊🎉",
|
||||
));
|
||||
|
||||
println!("sending");
|
||||
|
||||
self.client
|
||||
// send our message to the room we found the "!party" command in
|
||||
// the last parameter is an optional Uuid which we don't care about.
|
||||
.room_send(&room_id, content, None)
|
||||
.await
|
||||
.unwrap();
|
||||
// send our message to the room we found the "!party" command in
|
||||
// the last parameter is an optional Uuid which we don't care about.
|
||||
room.send(content, None).await.unwrap();
|
||||
|
||||
println!("message sent");
|
||||
}
|
||||
@@ -67,42 +63,30 @@ async fn login_and_sync(
|
||||
let mut home = dirs::home_dir().expect("no home directory found");
|
||||
home.push("party_bot");
|
||||
|
||||
let store = JsonStore::open(&home)?;
|
||||
let client_config = ClientConfig::new()
|
||||
.proxy("http://localhost:8080")?
|
||||
.disable_ssl_verification()
|
||||
.state_store(Box::new(store));
|
||||
let client_config = ClientConfig::new().store_path(home);
|
||||
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
// create a new Client with the given homeserver url and config
|
||||
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
client
|
||||
.login(
|
||||
username.clone(),
|
||||
password,
|
||||
None,
|
||||
Some("command bot".to_string()),
|
||||
)
|
||||
.await?;
|
||||
client.login(&username, &password, None, Some("command bot")).await?;
|
||||
|
||||
println!("logged in as {}", username);
|
||||
|
||||
// An initial sync to set up state and so our bot doesn't respond to old messages.
|
||||
// If the `StateStore` finds saved state in the location given the initial sync will
|
||||
// be skipped in favor of loading state from the store
|
||||
client.sync(SyncSettings::default()).await.unwrap();
|
||||
// add our CommandBot to be notified of incoming messages, we do this after the initial
|
||||
// sync to avoid responding to messages before the bot was running.
|
||||
client
|
||||
.add_event_emitter(Box::new(CommandBot::new(client.clone())))
|
||||
.await;
|
||||
// An initial sync to set up state and so our bot doesn't respond to old
|
||||
// messages. If the `StateStore` finds saved state in the location given the
|
||||
// initial sync will be skipped in favor of loading state from the store
|
||||
client.sync_once(SyncSettings::default()).await.unwrap();
|
||||
// add our CommandBot to be notified of incoming messages, we do this after the
|
||||
// initial sync to avoid responding to messages before the bot was running.
|
||||
client.set_event_handler(Box::new(CommandBot::new())).await;
|
||||
|
||||
// since we called sync before we `sync_forever` we must pass that sync token to
|
||||
// `sync_forever`
|
||||
// since we called `sync_once` before we entered our sync loop we must pass
|
||||
// that sync token to `sync`
|
||||
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
|
||||
// this keeps state from the server streaming in to CommandBot via the EventEmitter trait
|
||||
client.sync_forever(settings, |_| async {}).await;
|
||||
// this keeps state from the server streaming in to CommandBot via the
|
||||
// EventHandler trait
|
||||
client.sync(settings).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env, io,
|
||||
process::exit,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
self, api::r0::uiaa::AuthData, identifiers::UserId, Client, LoopCtrl, SyncSettings,
|
||||
};
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> {
|
||||
let mut auth_parameters = BTreeMap::new();
|
||||
let identifier = json!({
|
||||
"type": "m.id.user",
|
||||
"user": user,
|
||||
});
|
||||
|
||||
auth_parameters.insert("identifier".to_owned(), identifier);
|
||||
auth_parameters.insert("password".to_owned(), password.to_owned().into());
|
||||
|
||||
AuthData::DirectRequest { kind: "m.login.password", auth_parameters, session }
|
||||
}
|
||||
|
||||
async fn bootstrap(client: Client, user_id: UserId, password: String) {
|
||||
println!("Bootstrapping a new cross signing identity, press enter to continue.");
|
||||
|
||||
let mut input = String::new();
|
||||
|
||||
io::stdin().read_line(&mut input).expect("error: unable to read user input");
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
if let Err(e) = client.bootstrap_cross_signing(None).await {
|
||||
if let Some(response) = e.uiaa_response() {
|
||||
let auth_data = auth_data(&user_id, &password, response.session.as_deref());
|
||||
client
|
||||
.bootstrap_cross_signing(Some(auth_data))
|
||||
.await
|
||||
.expect("Couldn't bootstrap cross signing")
|
||||
} else {
|
||||
panic!("Error during cross-signing bootstrap {:#?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
panic!("Cross signing requires the encryption feature to be enabled");
|
||||
}
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new(homeserver_url).unwrap();
|
||||
|
||||
let response = client.login(username, password, None, Some("rust-sdk")).await?;
|
||||
|
||||
let user_id = &response.user_id;
|
||||
let client_ref = &client;
|
||||
let asked = AtomicBool::new(false);
|
||||
let asked_ref = &asked;
|
||||
|
||||
client
|
||||
.sync_with_callback(SyncSettings::new(), |_| async move {
|
||||
let asked = asked_ref;
|
||||
let client = &client_ref;
|
||||
let user_id = &user_id;
|
||||
let password = &password;
|
||||
|
||||
// Wait for sync to be done then ask the user to bootstrap.
|
||||
if !asked.load(Ordering::SeqCst) {
|
||||
tokio::spawn(bootstrap(
|
||||
(*client).clone(),
|
||||
(*user_id).clone(),
|
||||
password.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
asked.store(true, Ordering::SeqCst);
|
||||
LoopCtrl::Continue
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
login(homeserver_url, &username, &password).await
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
use std::{
|
||||
env, io,
|
||||
process::exit,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
self,
|
||||
events::{room::message::MessageType, AnySyncMessageEvent, AnySyncRoomEvent, AnyToDeviceEvent},
|
||||
identifiers::UserId,
|
||||
verification::{SasVerification, Verification},
|
||||
Client, LoopCtrl, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
async fn wait_for_confirmation(client: Client, sas: SasVerification) {
|
||||
println!("Does the emoji match: {:?}", sas.emoji());
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input).expect("error: unable to read user input");
|
||||
|
||||
match input.trim().to_lowercase().as_ref() {
|
||||
"yes" | "true" | "ok" => {
|
||||
sas.confirm().await.unwrap();
|
||||
|
||||
if sas.is_done() {
|
||||
print_result(&sas);
|
||||
print_devices(sas.other_device().user_id(), &client).await;
|
||||
}
|
||||
}
|
||||
_ => sas.cancel().await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_result(sas: &SasVerification) {
|
||||
let device = sas.other_device();
|
||||
|
||||
println!(
|
||||
"Successfully verified device {} {} {:?}",
|
||||
device.user_id(),
|
||||
device.device_id(),
|
||||
device.local_trust_state()
|
||||
);
|
||||
}
|
||||
|
||||
async fn print_devices(user_id: &UserId, client: &Client) {
|
||||
println!("Devices of user {}", user_id);
|
||||
|
||||
for device in client.get_user_devices(user_id).await.unwrap().devices() {
|
||||
println!(
|
||||
" {:<10} {:<30} {:<}",
|
||||
device.device_id(),
|
||||
device.display_name().as_deref().unwrap_or_default(),
|
||||
device.is_trusted()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new(homeserver_url).unwrap();
|
||||
|
||||
client.login(username, password, None, Some("rust-sdk")).await?;
|
||||
|
||||
let client_ref = &client;
|
||||
let initial_sync = Arc::new(AtomicBool::from(true));
|
||||
let initial_ref = &initial_sync;
|
||||
|
||||
client
|
||||
.sync_with_callback(SyncSettings::new(), |response| async move {
|
||||
let client = &client_ref;
|
||||
let initial = &initial_ref;
|
||||
|
||||
for event in response.to_device.events.iter().filter_map(|e| e.deserialize().ok()) {
|
||||
match event {
|
||||
AnyToDeviceEvent::KeyVerificationStart(e) => {
|
||||
if let Some(Verification::SasV1(sas)) =
|
||||
client.get_verification(&e.sender, &e.content.transaction_id).await
|
||||
{
|
||||
println!(
|
||||
"Starting verification with {} {}",
|
||||
&sas.other_device().user_id(),
|
||||
&sas.other_device().device_id()
|
||||
);
|
||||
print_devices(&e.sender, client).await;
|
||||
sas.accept().await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
AnyToDeviceEvent::KeyVerificationKey(e) => {
|
||||
if let Some(Verification::SasV1(sas)) =
|
||||
client.get_verification(&e.sender, &e.content.transaction_id).await
|
||||
{
|
||||
tokio::spawn(wait_for_confirmation((*client).clone(), sas));
|
||||
}
|
||||
}
|
||||
|
||||
AnyToDeviceEvent::KeyVerificationMac(e) => {
|
||||
if let Some(Verification::SasV1(sas)) =
|
||||
client.get_verification(&e.sender, &e.content.transaction_id).await
|
||||
{
|
||||
if sas.is_done() {
|
||||
print_result(&sas);
|
||||
print_devices(&e.sender, client).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if !initial.load(Ordering::SeqCst) {
|
||||
for (_room_id, room_info) in response.rooms.join {
|
||||
for event in
|
||||
room_info.timeline.events.iter().filter_map(|e| e.event.deserialize().ok())
|
||||
{
|
||||
if let AnySyncRoomEvent::Message(event) = event {
|
||||
match event {
|
||||
AnySyncMessageEvent::RoomMessage(m) => {
|
||||
if let MessageType::VerificationRequest(_) = &m.content.msgtype
|
||||
{
|
||||
let request = client
|
||||
.get_verification_request(&m.sender, &m.event_id)
|
||||
.await
|
||||
.expect("Request object wasn't created");
|
||||
|
||||
request
|
||||
.accept()
|
||||
.await
|
||||
.expect("Can't accept verification request");
|
||||
}
|
||||
}
|
||||
AnySyncMessageEvent::KeyVerificationKey(e) => {
|
||||
if let Some(Verification::SasV1(sas)) = client
|
||||
.get_verification(
|
||||
&e.sender,
|
||||
e.content.relates_to.event_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tokio::spawn(wait_for_confirmation((*client).clone(), sas));
|
||||
}
|
||||
}
|
||||
AnySyncMessageEvent::KeyVerificationMac(e) => {
|
||||
if let Some(Verification::SasV1(sas)) = client
|
||||
.get_verification(
|
||||
&e.sender,
|
||||
e.content.relates_to.event_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if sas.is_done() {
|
||||
print_result(&sas);
|
||||
print_devices(&e.sender, client).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initial.store(false, Ordering::SeqCst);
|
||||
|
||||
LoopCtrl::Continue
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
login(homeserver_url, &username, &password).await
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::{convert::TryFrom, env, process::exit};
|
||||
|
||||
use matrix_sdk::{
|
||||
self,
|
||||
api::r0::profile,
|
||||
identifiers::{MxcUri, UserId},
|
||||
Client, Result as MatrixResult,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UserProfile {
|
||||
avatar_url: Option<MxcUri>,
|
||||
displayname: Option<String>,
|
||||
}
|
||||
|
||||
/// This function calls the GET profile endpoint
|
||||
/// Spec: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-profile-userid
|
||||
/// Ruma: https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/index.html
|
||||
async fn get_profile(client: Client, mxid: &UserId) -> MatrixResult<UserProfile> {
|
||||
// First construct the request you want to make
|
||||
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/index.html for all available Endpoints
|
||||
let request = profile::get_profile::Request::new(mxid);
|
||||
|
||||
// Start the request using matrix_sdk::Client::send
|
||||
let resp = client.send(request, None).await?;
|
||||
|
||||
// Use the response and construct a UserProfile struct.
|
||||
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/struct.Response.html
|
||||
// for details on the Response for this Request
|
||||
let user_profile = UserProfile { avatar_url: resp.avatar_url, displayname: resp.displayname };
|
||||
Ok(user_profile)
|
||||
}
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Client, matrix_sdk::Error> {
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new(homeserver_url).unwrap();
|
||||
|
||||
client.login(username, password, None, Some("rust-sdk")).await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <mxid> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
let client = login(homeserver_url, &username, &password).await?;
|
||||
|
||||
let user_id = UserId::try_from(username).expect("Couldn't parse the MXID");
|
||||
let profile = get_profile(client, &user_id).await?;
|
||||
println!("{:#?}", profile);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
self, async_trait,
|
||||
events::{
|
||||
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
|
||||
SyncMessageEvent,
|
||||
},
|
||||
room::Room,
|
||||
Client, EventHandler, SyncSettings,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use url::Url;
|
||||
|
||||
struct ImageBot {
|
||||
image: Arc<Mutex<File>>,
|
||||
}
|
||||
|
||||
impl ImageBot {
|
||||
pub fn new(image: File) -> Self {
|
||||
let image = Arc::new(Mutex::new(image));
|
||||
Self { image }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for ImageBot {
|
||||
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
if let Room::Joined(room) = room {
|
||||
let msg_body = if let SyncMessageEvent {
|
||||
content:
|
||||
MessageEventContent {
|
||||
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
..
|
||||
},
|
||||
..
|
||||
} = event
|
||||
{
|
||||
msg_body
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if msg_body.contains("!image") {
|
||||
println!("sending image");
|
||||
let mut image = self.image.lock().await;
|
||||
|
||||
room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None).await.unwrap();
|
||||
|
||||
image.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
println!("message sent");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_and_sync(
|
||||
homeserver_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
image: File,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new(homeserver_url).unwrap();
|
||||
|
||||
client.login(&username, &password, None, Some("command bot")).await?;
|
||||
|
||||
client.sync_once(SyncSettings::default()).await.unwrap();
|
||||
client.set_event_handler(Box::new(ImageBot::new(image))).await;
|
||||
|
||||
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
|
||||
client.sync(settings).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let (homeserver_url, username, password, image_path) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3), env::args().nth(4)) {
|
||||
(Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password> <image>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
println!("helloooo {} {} {} {:#?}", homeserver_url, username, password, image_path);
|
||||
let path = PathBuf::from(image_path);
|
||||
let image = File::open(path).expect("Can't open image file.");
|
||||
|
||||
login_and_sync(homeserver_url, username, password, image).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,35 +1,34 @@
|
||||
use std::{env, process::exit};
|
||||
use url::Url;
|
||||
|
||||
use matrix_sdk::{
|
||||
self,
|
||||
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
|
||||
self, async_trait,
|
||||
events::{
|
||||
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
|
||||
SyncMessageEvent,
|
||||
},
|
||||
room::Room,
|
||||
Client, EventHandler, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
struct EventCallback;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventEmitter for EventCallback {
|
||||
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
|
||||
if let SyncRoom::Joined(room) = room {
|
||||
if let MessageEvent {
|
||||
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
#[async_trait]
|
||||
impl EventHandler for EventCallback {
|
||||
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
if let Room::Joined(room) = room {
|
||||
if let SyncMessageEvent {
|
||||
content:
|
||||
MessageEventContent {
|
||||
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
..
|
||||
},
|
||||
sender,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let name = {
|
||||
// any reads should be held for the shortest time possible to
|
||||
// avoid dead locks
|
||||
let room = room.read().await;
|
||||
let member = room.members.get(&sender).unwrap();
|
||||
member
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or(sender.to_string())
|
||||
};
|
||||
let member = room.get_member(sender).await.unwrap().unwrap();
|
||||
let name = member.display_name().unwrap_or_else(|| member.user_id().as_str());
|
||||
println!("{}: {}", name, msg_body);
|
||||
}
|
||||
}
|
||||
@@ -38,21 +37,16 @@ impl EventEmitter for EventCallback {
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let client_config = ClientConfig::new()
|
||||
.proxy("http://localhost:8080")?
|
||||
.disable_ssl_verification();
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
let client = Client::new(homeserver_url).unwrap();
|
||||
|
||||
client.add_event_emitter(Box::new(EventCallback)).await;
|
||||
client.set_event_handler(Box::new(EventCallback)).await;
|
||||
|
||||
client
|
||||
.login(username, password, None, Some("rust-sdk".to_string()))
|
||||
.await?;
|
||||
client.sync_forever(SyncSettings::new(), |_| async {}).await;
|
||||
client.login(username, password, None, Some("rust-sdk")).await?;
|
||||
client.sync(SyncSettings::new()).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -73,5 +67,5 @@ async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
}
|
||||
};
|
||||
|
||||
login(homeserver_url, username, password).await
|
||||
login(homeserver_url, &username, &password).await
|
||||
}
|
||||
|
||||
@@ -10,10 +10,15 @@ edition = "2018"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
matrix-sdk = { path = "../..", default-features = false }
|
||||
url = "2.1.1"
|
||||
wasm-bindgen = { version = "0.2.62", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4.12"
|
||||
web-sys = { version = "0.3.39", features = ["console"] }
|
||||
url = "2.2.2"
|
||||
wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4.24"
|
||||
console_error_panic_hook = "0.1.6"
|
||||
web-sys = { version = "0.3.51", features = ["console"] }
|
||||
|
||||
[dependencies.matrix-sdk]
|
||||
path = "../.."
|
||||
default-features = false
|
||||
features = ["native-tls", "encryption"]
|
||||
|
||||
[workspace]
|
||||
|
||||
@@ -7,6 +7,4 @@ You can build the example locally with:
|
||||
|
||||
and then visiting http://localhost:8080 in a browser should run the example!
|
||||
|
||||
Note: Encryption isn't supported yet
|
||||
|
||||
This example is loosely based off of [this example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch), an example usage of `fetch` from `wasm-bindgen`.
|
||||
This example is loosely based off of [this example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch), an example usage of `fetch` from `wasm-bindgen`.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wasm-tool/wasm-pack-plugin": "1.0.1",
|
||||
"text-encoding": "^0.7.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"text-encoding": "^0.7.0",
|
||||
"webpack": "^4.29.4",
|
||||
"webpack-cli": "^3.1.1",
|
||||
"webpack-dev-server": "^3.1.0"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use matrix_sdk::{
|
||||
api::r0::sync::sync_events::Response as SyncResponse,
|
||||
events::collections::all::RoomEvent,
|
||||
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
deserialized_responses::SyncResponse,
|
||||
events::{
|
||||
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
|
||||
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent,
|
||||
},
|
||||
identifiers::RoomId,
|
||||
Client, ClientConfig, SyncSettings,
|
||||
Client, LoopCtrl, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -12,49 +14,70 @@ use web_sys::console;
|
||||
struct WasmBot(Client);
|
||||
|
||||
impl WasmBot {
|
||||
async fn on_room_message(&self, room_id: &RoomId, event: RoomEvent) {
|
||||
let msg_body = if let RoomEvent::RoomMessage(MessageEvent {
|
||||
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
async fn on_room_message(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event: &SyncMessageEvent<MessageEventContent>,
|
||||
) {
|
||||
let msg_body = if let SyncMessageEvent {
|
||||
content:
|
||||
MessageEventContent {
|
||||
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = event
|
||||
} = event
|
||||
{
|
||||
msg_body.clone()
|
||||
msg_body
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
console::log_1(&format!("Received message event {:?}", &msg_body).into());
|
||||
|
||||
if msg_body.starts_with("!party") {
|
||||
let content = MessageEventContent::Text(TextMessageEventContent::new_plain(
|
||||
"🎉🎊🥳 let's PARTY with wasm!! 🥳🎊🎉".to_string(),
|
||||
if msg_body.contains("!party") {
|
||||
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::text_plain(
|
||||
"🎉🎊🥳 let's PARTY!! 🥳🎊🎉",
|
||||
));
|
||||
|
||||
self.0.room_send(&room_id, content, None).await.unwrap();
|
||||
println!("sending");
|
||||
|
||||
self.0
|
||||
// send our message to the room we found the "!party" command in
|
||||
// the last parameter is an optional Uuid which we don't care about.
|
||||
.room_send(room_id, content, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("message sent");
|
||||
}
|
||||
}
|
||||
async fn on_sync_response(&self, response: SyncResponse) {
|
||||
console::log_1(&format!("Synced").into());
|
||||
|
||||
async fn on_sync_response(&self, response: SyncResponse) -> LoopCtrl {
|
||||
console::log_1(&"Synced".to_string().into());
|
||||
|
||||
for (room_id, room) in response.rooms.join {
|
||||
for event in room.timeline.events {
|
||||
if let Ok(event) = event.deserialize() {
|
||||
self.on_room_message(&room_id, event).await
|
||||
if let Ok(AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev))) = event.event.deserialize() {
|
||||
self.on_room_message(&room_id, &ev).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoopCtrl::Continue
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn run() -> Result<JsValue, JsValue> {
|
||||
let homeserver_url = "http://localhost:8008";
|
||||
let username = "user";
|
||||
let password = "password";
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let homeserver_url = "http://localhost:8008";
|
||||
let username = "example";
|
||||
let password = "wordpass";
|
||||
|
||||
let client_config = ClientConfig::new();
|
||||
let homeserver_url = Url::parse(&homeserver_url).unwrap();
|
||||
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
let client = Client::new(homeserver_url).unwrap();
|
||||
|
||||
client
|
||||
.login(username, password, None, Some("rust-sdk-wasm"))
|
||||
@@ -63,11 +86,11 @@ pub async fn run() -> Result<JsValue, JsValue> {
|
||||
|
||||
let bot = WasmBot(client.clone());
|
||||
|
||||
client.sync(SyncSettings::default()).await.unwrap();
|
||||
client.sync_once(SyncSettings::default()).await.unwrap();
|
||||
|
||||
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
|
||||
client
|
||||
.sync_forever(settings, |response| bot.on_sync_response(response))
|
||||
.sync_with_callback(settings, |response| bot.on_sync_response(response))
|
||||
.await;
|
||||
|
||||
Ok(JsValue::NULL)
|
||||
|
||||
+3157
-1383
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
// 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.
|
||||
|
||||
use std::{ops::Deref, result::Result as StdResult};
|
||||
|
||||
use matrix_sdk_base::crypto::{
|
||||
store::CryptoStoreError, Device as BaseDevice, LocalTrust, ReadOnlyDevice,
|
||||
UserDevices as BaseUserDevices,
|
||||
};
|
||||
use ruma::{DeviceId, DeviceIdBox};
|
||||
|
||||
use crate::{error::Result, verification::SasVerification, Client};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A device represents a E2EE capable client of an user.
|
||||
pub struct Device {
|
||||
pub(crate) inner: BaseDevice,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl Deref for Device {
|
||||
type Target = ReadOnlyDevice;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Start a interactive verification with this `Device`
|
||||
///
|
||||
/// Returns a `Sas` object that represents the interactive verification
|
||||
/// flow.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::convert::TryFrom;
|
||||
/// # use matrix_sdk::{Client, identifiers::UserId};
|
||||
/// # use url::Url;
|
||||
/// # use futures::executor::block_on;
|
||||
/// # let alice = UserId::try_from("@alice:example.org").unwrap();
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # let client = Client::new(homeserver).unwrap();
|
||||
/// # block_on(async {
|
||||
/// let device = client.get_device(&alice, "DEVICEID".into())
|
||||
/// .await
|
||||
/// .unwrap()
|
||||
/// .unwrap();
|
||||
///
|
||||
/// let verification = device.start_verification().await.unwrap();
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn start_verification(&self) -> Result<SasVerification> {
|
||||
let (sas, request) = self.inner.start_verification().await?;
|
||||
self.client.send_to_device(&request).await?;
|
||||
|
||||
Ok(SasVerification { inner: sas, client: self.client.clone() })
|
||||
}
|
||||
|
||||
/// Is the device trusted.
|
||||
pub fn is_trusted(&self) -> bool {
|
||||
self.inner.trust_state()
|
||||
}
|
||||
|
||||
/// Set the local trust state of the device to the given state.
|
||||
///
|
||||
/// This won't affect any cross signing trust state, this only sets a flag
|
||||
/// marking to have the given trust state.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `trust_state` - The new trust state that should be set for the device.
|
||||
pub async fn set_local_trust(
|
||||
&self,
|
||||
trust_state: LocalTrust,
|
||||
) -> StdResult<(), CryptoStoreError> {
|
||||
self.inner.set_local_trust(trust_state).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A read only view over all devices belonging to a user.
|
||||
#[derive(Debug)]
|
||||
pub struct UserDevices {
|
||||
pub(crate) inner: BaseUserDevices,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl UserDevices {
|
||||
/// Get the specific device with the given device id.
|
||||
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
|
||||
self.inner.get(device_id).map(|d| Device { inner: d, client: self.client.clone() })
|
||||
}
|
||||
|
||||
/// Iterator over all the device ids of the user devices.
|
||||
pub fn keys(&self) -> impl Iterator<Item = &DeviceIdBox> {
|
||||
self.inner.keys()
|
||||
}
|
||||
|
||||
/// Iterator over all the devices of the user devices.
|
||||
pub fn devices(&self) -> impl Iterator<Item = Device> + '_ {
|
||||
let client = self.client.clone();
|
||||
|
||||
self.inner.devices().map(move |d| Device { inner: d, client: client.clone() })
|
||||
}
|
||||
}
|
||||
+130
-26
@@ -14,55 +14,159 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use std::io::Error as IoError;
|
||||
|
||||
use http::StatusCode;
|
||||
#[cfg(feature = "encryption")]
|
||||
use matrix_sdk_base::crypto::{store::CryptoStoreError, DecryptorError};
|
||||
use matrix_sdk_base::{Error as MatrixError, StoreError};
|
||||
use reqwest::Error as ReqwestError;
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
r0::uiaa::{UiaaInfo, UiaaResponse as UiaaError},
|
||||
Error as RumaClientApiError,
|
||||
},
|
||||
error::{FromHttpResponseError, IntoHttpError, MatrixError as RumaApiError, ServerError},
|
||||
},
|
||||
identifiers::Error as IdentifierError,
|
||||
};
|
||||
use serde_json::Error as JsonError;
|
||||
use thiserror::Error;
|
||||
|
||||
use matrix_sdk_base::Error as MatrixError;
|
||||
|
||||
use crate::api::Error as RumaClientError;
|
||||
use crate::FromHttpResponseError as RumaResponseError;
|
||||
use crate::IntoHttpError as RumaIntoHttpError;
|
||||
use url::ParseError as UrlParseError;
|
||||
|
||||
/// Result type of the rust-sdk.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Internal representation of errors.
|
||||
/// An HTTP error, representing either a connection error or an error while
|
||||
/// converting the raw HTTP response into a Matrix response.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// Queried endpoint requires authentication but was called on an anonymous client.
|
||||
#[error("the queried endpoint requires authentication but was called before logging in")]
|
||||
AuthenticationRequired,
|
||||
|
||||
pub enum HttpError {
|
||||
/// An error at the HTTP layer.
|
||||
#[error(transparent)]
|
||||
Reqwest(#[from] ReqwestError),
|
||||
|
||||
/// Queried endpoint requires authentication but was called on an anonymous
|
||||
/// client.
|
||||
#[error("the queried endpoint requires authentication but was called before logging in")]
|
||||
AuthenticationRequired,
|
||||
|
||||
/// Client tried to force authentication but did not provide an access
|
||||
/// token.
|
||||
#[error("tried to force authentication but no access token was provided")]
|
||||
ForcedAuthenticationWithoutAccessToken,
|
||||
|
||||
/// Queried endpoint is not meant for clients.
|
||||
#[error("the queried endpoint is not meant for clients")]
|
||||
NotClientRequest,
|
||||
|
||||
/// An error converting between ruma_*_api types and Hyper types.
|
||||
#[error(transparent)]
|
||||
Api(#[from] FromHttpResponseError<RumaApiError>),
|
||||
|
||||
/// An error converting between ruma_client_api types and Hyper types.
|
||||
#[error(transparent)]
|
||||
ClientApi(#[from] FromHttpResponseError<RumaClientApiError>),
|
||||
|
||||
/// An error converting between ruma_client_api types and Hyper types.
|
||||
#[error(transparent)]
|
||||
IntoHttp(#[from] IntoHttpError),
|
||||
|
||||
/// An error occurred while authenticating.
|
||||
///
|
||||
/// When registering or authenticating the Matrix server can send a
|
||||
/// `UiaaResponse` as the error type, this is a User-Interactive
|
||||
/// Authentication API response. This represents an error with
|
||||
/// information about how to authenticate the user.
|
||||
#[error(transparent)]
|
||||
UiaaError(#[from] FromHttpResponseError<UiaaError>),
|
||||
|
||||
/// The server returned a status code that should be retried.
|
||||
#[error("Server returned an error {0}")]
|
||||
Server(StatusCode),
|
||||
|
||||
/// The given request can't be cloned and thus can't be retried.
|
||||
#[error("The request cannot be cloned")]
|
||||
UnableToCloneRequest,
|
||||
|
||||
/// Tried to send a request without `user_id` in the `Session`
|
||||
#[error("missing user_id in session")]
|
||||
UserIdRequired,
|
||||
}
|
||||
|
||||
/// Internal representation of errors.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// Error doing an HTTP request.
|
||||
#[error(transparent)]
|
||||
Http(#[from] HttpError),
|
||||
|
||||
/// Queried endpoint requires authentication but was called on an anonymous
|
||||
/// client.
|
||||
#[error("the queried endpoint requires authentication but was called before logging in")]
|
||||
AuthenticationRequired,
|
||||
|
||||
/// An error de/serializing type for the `StateStore`
|
||||
#[error(transparent)]
|
||||
SerdeJson(#[from] JsonError),
|
||||
|
||||
/// An error converting between ruma_client_api types and Hyper types.
|
||||
#[error("can't parse the JSON response as a Matrix response")]
|
||||
RumaResponse(RumaResponseError<RumaClientError>),
|
||||
/// An IO error happened.
|
||||
#[error(transparent)]
|
||||
Io(#[from] IoError),
|
||||
|
||||
/// An error converting between ruma_client_api types and Hyper types.
|
||||
#[error("can't convert between ruma_client_api and hyper types.")]
|
||||
IntoHttp(RumaIntoHttpError),
|
||||
|
||||
/// An error occured in the Matrix client library.
|
||||
/// An error occurred in the Matrix client library.
|
||||
#[error(transparent)]
|
||||
MatrixError(#[from] MatrixError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[error(transparent)]
|
||||
CryptoStoreError(#[from] CryptoStoreError),
|
||||
|
||||
/// An error occurred during decryption.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[error(transparent)]
|
||||
DecryptorError(#[from] DecryptorError),
|
||||
|
||||
/// An error occurred in the state store.
|
||||
#[error(transparent)]
|
||||
StateStore(#[from] StoreError),
|
||||
|
||||
/// An error encountered when trying to parse an identifier.
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] IdentifierError),
|
||||
|
||||
/// An error encountered when trying to parse a url.
|
||||
#[error(transparent)]
|
||||
Url(#[from] UrlParseError),
|
||||
}
|
||||
|
||||
impl From<RumaResponseError<RumaClientError>> for Error {
|
||||
fn from(error: RumaResponseError<RumaClientError>) -> Self {
|
||||
Self::RumaResponse(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> {
|
||||
if let Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(ServerError::Known(
|
||||
UiaaError::AuthResponse(i),
|
||||
)))) = self
|
||||
{
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaIntoHttpError> for Error {
|
||||
fn from(error: RumaIntoHttpError) -> Self {
|
||||
Self::IntoHttp(error)
|
||||
impl From<ReqwestError> for Error {
|
||||
fn from(e: ReqwestError) -> Self {
|
||||
Error::Http(HttpError::Reqwest(e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,963 @@
|
||||
// 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.
|
||||
use std::ops::Deref;
|
||||
|
||||
use matrix_sdk_common::async_trait;
|
||||
use ruma::{
|
||||
api::client::r0::push::get_notifications::Notification,
|
||||
events::{
|
||||
call::{
|
||||
answer::AnswerEventContent, candidates::CandidatesEventContent,
|
||||
hangup::HangupEventContent, invite::InviteEventContent,
|
||||
},
|
||||
custom::CustomEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
presence::PresenceEvent,
|
||||
push_rules::PushRulesEventContent,
|
||||
reaction::ReactionEventContent,
|
||||
receipt::ReceiptEventContent,
|
||||
room::{
|
||||
aliases::AliasesEventContent,
|
||||
avatar::AvatarEventContent,
|
||||
canonical_alias::CanonicalAliasEventContent,
|
||||
join_rules::JoinRulesEventContent,
|
||||
member::MemberEventContent,
|
||||
message::{feedback::FeedbackEventContent, MessageEventContent as MsgEventContent},
|
||||
name::NameEventContent,
|
||||
power_levels::PowerLevelsEventContent,
|
||||
redaction::SyncRedactionEvent,
|
||||
tombstone::TombstoneEventContent,
|
||||
},
|
||||
typing::TypingEventContent,
|
||||
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
AnySyncEphemeralRoomEvent, AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent,
|
||||
GlobalAccountDataEvent, RoomAccountDataEvent, StrippedStateEvent, SyncEphemeralRoomEvent,
|
||||
SyncMessageEvent, SyncStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
RoomId,
|
||||
};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use crate::{deserialized_responses::SyncResponse, room::Room, Client};
|
||||
|
||||
pub(crate) struct Handler {
|
||||
pub(crate) inner: Box<dyn EventHandler>,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl Deref for Handler {
|
||||
type Target = dyn EventHandler;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
fn get_room(&self, room_id: &RoomId) -> Option<Room> {
|
||||
self.client.get_room(room_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_sync(&self, response: &SyncResponse) {
|
||||
for event in response.account_data.events.iter().filter_map(|e| e.deserialize().ok()) {
|
||||
self.handle_account_data_event(&event).await;
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &response.rooms.join {
|
||||
if let Some(room) = self.get_room(room_id) {
|
||||
for event in room_info.ephemeral.events.iter().filter_map(|e| e.deserialize().ok())
|
||||
{
|
||||
self.handle_ephemeral_event(room.clone(), &event).await;
|
||||
}
|
||||
|
||||
for event in
|
||||
room_info.account_data.events.iter().filter_map(|e| e.deserialize().ok())
|
||||
{
|
||||
self.handle_room_account_data_event(room.clone(), &event).await;
|
||||
}
|
||||
|
||||
for (raw_event, event) in room_info.state.events.iter().filter_map(|e| {
|
||||
if let Ok(d) = e.deserialize() {
|
||||
Some((e, d))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
self.handle_state_event(room.clone(), &event, raw_event).await;
|
||||
}
|
||||
|
||||
for (raw_event, event) in room_info.timeline.events.iter().filter_map(|e| {
|
||||
if let Ok(d) = e.event.deserialize() {
|
||||
Some((&e.event, d))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
self.handle_timeline_event(room.clone(), &event, raw_event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &response.rooms.leave {
|
||||
if let Some(room) = self.get_room(room_id) {
|
||||
for event in
|
||||
room_info.account_data.events.iter().filter_map(|e| e.deserialize().ok())
|
||||
{
|
||||
self.handle_room_account_data_event(room.clone(), &event).await;
|
||||
}
|
||||
|
||||
for (raw_event, event) in room_info.state.events.iter().filter_map(|e| {
|
||||
if let Ok(d) = e.deserialize() {
|
||||
Some((e, d))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
self.handle_state_event(room.clone(), &event, raw_event).await;
|
||||
}
|
||||
|
||||
for (raw_event, event) in room_info.timeline.events.iter().filter_map(|e| {
|
||||
if let Ok(d) = e.event.deserialize() {
|
||||
Some((&e.event, d))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
self.handle_timeline_event(room.clone(), &event, raw_event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, room_info) in &response.rooms.invite {
|
||||
if let Some(room) = self.get_room(room_id) {
|
||||
for event in
|
||||
room_info.invite_state.events.iter().filter_map(|e| e.deserialize().ok())
|
||||
{
|
||||
self.handle_stripped_state_event(room.clone(), &event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for event in response.presence.events.iter().filter_map(|e| e.deserialize().ok()) {
|
||||
self.on_presence_event(&event).await;
|
||||
}
|
||||
|
||||
for (room_id, notifications) in &response.notifications {
|
||||
if let Some(room) = self.get_room(room_id) {
|
||||
for notification in notifications {
|
||||
self.on_room_notification(room.clone(), notification.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_timeline_event(
|
||||
&self,
|
||||
room: Room,
|
||||
event: &AnySyncRoomEvent,
|
||||
raw_event: &Raw<AnySyncRoomEvent>,
|
||||
) {
|
||||
match event {
|
||||
AnySyncRoomEvent::State(event) => match event {
|
||||
AnySyncStateEvent::RoomMember(e) => self.on_room_member(room, e).await,
|
||||
AnySyncStateEvent::RoomName(e) => self.on_room_name(room, e).await,
|
||||
AnySyncStateEvent::RoomCanonicalAlias(e) => {
|
||||
self.on_room_canonical_alias(room, e).await
|
||||
}
|
||||
AnySyncStateEvent::RoomAliases(e) => self.on_room_aliases(room, e).await,
|
||||
AnySyncStateEvent::RoomAvatar(e) => self.on_room_avatar(room, e).await,
|
||||
AnySyncStateEvent::RoomPowerLevels(e) => self.on_room_power_levels(room, e).await,
|
||||
AnySyncStateEvent::RoomTombstone(e) => self.on_room_tombstone(room, e).await,
|
||||
AnySyncStateEvent::RoomJoinRules(e) => self.on_room_join_rules(room, e).await,
|
||||
AnySyncStateEvent::PolicyRuleRoom(_)
|
||||
| AnySyncStateEvent::PolicyRuleServer(_)
|
||||
| AnySyncStateEvent::PolicyRuleUser(_)
|
||||
| AnySyncStateEvent::RoomCreate(_)
|
||||
| AnySyncStateEvent::RoomEncryption(_)
|
||||
| AnySyncStateEvent::RoomGuestAccess(_)
|
||||
| AnySyncStateEvent::RoomHistoryVisibility(_)
|
||||
| AnySyncStateEvent::RoomPinnedEvents(_)
|
||||
| AnySyncStateEvent::RoomServerAcl(_)
|
||||
| AnySyncStateEvent::RoomThirdPartyInvite(_)
|
||||
| AnySyncStateEvent::RoomTopic(_)
|
||||
| AnySyncStateEvent::SpaceChild(_)
|
||||
| AnySyncStateEvent::SpaceParent(_) => {}
|
||||
_ => {
|
||||
if let Ok(e) = raw_event.deserialize_as::<SyncStateEvent<CustomEventContent>>()
|
||||
{
|
||||
self.on_custom_event(room, &CustomEvent::State(&e)).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
AnySyncRoomEvent::Message(event) => match event {
|
||||
AnySyncMessageEvent::RoomMessage(e) => self.on_room_message(room, e).await,
|
||||
AnySyncMessageEvent::RoomMessageFeedback(e) => {
|
||||
self.on_room_message_feedback(room, e).await
|
||||
}
|
||||
AnySyncMessageEvent::RoomRedaction(e) => self.on_room_redaction(room, e).await,
|
||||
AnySyncMessageEvent::Reaction(e) => self.on_room_reaction(room, e).await,
|
||||
AnySyncMessageEvent::CallInvite(e) => self.on_room_call_invite(room, e).await,
|
||||
AnySyncMessageEvent::CallAnswer(e) => self.on_room_call_answer(room, e).await,
|
||||
AnySyncMessageEvent::CallCandidates(e) => {
|
||||
self.on_room_call_candidates(room, e).await
|
||||
}
|
||||
AnySyncMessageEvent::CallHangup(e) => self.on_room_call_hangup(room, e).await,
|
||||
AnySyncMessageEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageEvent::KeyVerificationKey(_)
|
||||
| AnySyncMessageEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageEvent::KeyVerificationDone(_)
|
||||
| AnySyncMessageEvent::RoomEncrypted(_)
|
||||
| AnySyncMessageEvent::Sticker(_) => {}
|
||||
_ => {
|
||||
if let Ok(e) =
|
||||
raw_event.deserialize_as::<SyncMessageEvent<CustomEventContent>>()
|
||||
{
|
||||
self.on_custom_event(room, &CustomEvent::Message(&e)).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
AnySyncRoomEvent::RedactedState(_event) => {}
|
||||
AnySyncRoomEvent::RedactedMessage(_event) => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_state_event(
|
||||
&self,
|
||||
room: Room,
|
||||
event: &AnySyncStateEvent,
|
||||
raw_event: &Raw<AnySyncStateEvent>,
|
||||
) {
|
||||
match event {
|
||||
AnySyncStateEvent::RoomMember(member) => self.on_state_member(room, member).await,
|
||||
AnySyncStateEvent::RoomName(name) => self.on_state_name(room, name).await,
|
||||
AnySyncStateEvent::RoomCanonicalAlias(canonical) => {
|
||||
self.on_state_canonical_alias(room, canonical).await
|
||||
}
|
||||
AnySyncStateEvent::RoomAliases(aliases) => self.on_state_aliases(room, aliases).await,
|
||||
AnySyncStateEvent::RoomAvatar(avatar) => self.on_state_avatar(room, avatar).await,
|
||||
AnySyncStateEvent::RoomPowerLevels(power) => {
|
||||
self.on_state_power_levels(room, power).await
|
||||
}
|
||||
AnySyncStateEvent::RoomJoinRules(rules) => self.on_state_join_rules(room, rules).await,
|
||||
AnySyncStateEvent::RoomTombstone(tomb) => {
|
||||
// TODO make `on_state_tombstone` method
|
||||
self.on_room_tombstone(room, tomb).await
|
||||
}
|
||||
AnySyncStateEvent::PolicyRuleRoom(_)
|
||||
| AnySyncStateEvent::PolicyRuleServer(_)
|
||||
| AnySyncStateEvent::PolicyRuleUser(_)
|
||||
| AnySyncStateEvent::RoomCreate(_)
|
||||
| AnySyncStateEvent::RoomEncryption(_)
|
||||
| AnySyncStateEvent::RoomGuestAccess(_)
|
||||
| AnySyncStateEvent::RoomHistoryVisibility(_)
|
||||
| AnySyncStateEvent::RoomPinnedEvents(_)
|
||||
| AnySyncStateEvent::RoomServerAcl(_)
|
||||
| AnySyncStateEvent::RoomThirdPartyInvite(_)
|
||||
| AnySyncStateEvent::RoomTopic(_)
|
||||
| AnySyncStateEvent::SpaceChild(_)
|
||||
| AnySyncStateEvent::SpaceParent(_) => {}
|
||||
_ => {
|
||||
if let Ok(e) = raw_event.deserialize_as::<SyncStateEvent<CustomEventContent>>() {
|
||||
self.on_custom_event(room, &CustomEvent::State(&e)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_stripped_state_event(
|
||||
&self,
|
||||
// TODO these events are only handled in invited rooms.
|
||||
room: Room,
|
||||
event: &AnyStrippedStateEvent,
|
||||
) {
|
||||
match event {
|
||||
AnyStrippedStateEvent::RoomMember(member) => {
|
||||
self.on_stripped_state_member(room, member, None).await
|
||||
}
|
||||
AnyStrippedStateEvent::RoomName(name) => self.on_stripped_state_name(room, name).await,
|
||||
AnyStrippedStateEvent::RoomCanonicalAlias(canonical) => {
|
||||
self.on_stripped_state_canonical_alias(room, canonical).await
|
||||
}
|
||||
AnyStrippedStateEvent::RoomAliases(aliases) => {
|
||||
self.on_stripped_state_aliases(room, aliases).await
|
||||
}
|
||||
AnyStrippedStateEvent::RoomAvatar(avatar) => {
|
||||
self.on_stripped_state_avatar(room, avatar).await
|
||||
}
|
||||
AnyStrippedStateEvent::RoomPowerLevels(power) => {
|
||||
self.on_stripped_state_power_levels(room, power).await
|
||||
}
|
||||
AnyStrippedStateEvent::RoomJoinRules(rules) => {
|
||||
self.on_stripped_state_join_rules(room, rules).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_room_account_data_event(
|
||||
&self,
|
||||
room: Room,
|
||||
event: &AnyRoomAccountDataEvent,
|
||||
) {
|
||||
if let AnyRoomAccountDataEvent::FullyRead(event) = event {
|
||||
self.on_non_room_fully_read(room, event).await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_account_data_event(&self, event: &AnyGlobalAccountDataEvent) {
|
||||
match event {
|
||||
AnyGlobalAccountDataEvent::IgnoredUserList(ignored) => {
|
||||
self.on_non_room_ignored_users(ignored).await
|
||||
}
|
||||
AnyGlobalAccountDataEvent::PushRules(rules) => self.on_non_room_push_rules(rules).await,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_ephemeral_event(
|
||||
&self,
|
||||
room: Room,
|
||||
event: &AnySyncEphemeralRoomEvent,
|
||||
) {
|
||||
match event {
|
||||
AnySyncEphemeralRoomEvent::Typing(typing) => {
|
||||
self.on_non_room_typing(room, typing).await
|
||||
}
|
||||
AnySyncEphemeralRoomEvent::Receipt(receipt) => {
|
||||
self.on_non_room_receipt(room, receipt).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents the various "unrecognized" events.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CustomEvent<'c> {
|
||||
/// A custom basic event.
|
||||
Basic(&'c GlobalAccountDataEvent<CustomEventContent>),
|
||||
/// A custom basic event.
|
||||
EphemeralRoom(&'c SyncEphemeralRoomEvent<CustomEventContent>),
|
||||
/// A custom room event.
|
||||
Message(&'c SyncMessageEvent<CustomEventContent>),
|
||||
/// A custom state event.
|
||||
State(&'c SyncStateEvent<CustomEventContent>),
|
||||
/// A custom stripped state event.
|
||||
StrippedState(&'c StrippedStateEvent<CustomEventContent>),
|
||||
}
|
||||
|
||||
/// This trait allows any type implementing `EventHandler` to specify event
|
||||
/// callbacks for each event. The `Client` calls each method when the
|
||||
/// corresponding event is received.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::ops::Deref;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use std::{env, process::exit};
|
||||
/// # use matrix_sdk::{
|
||||
/// # async_trait,
|
||||
/// # EventHandler,
|
||||
/// # events::{
|
||||
/// # room::message::{MessageEventContent, MessageType, TextMessageEventContent},
|
||||
/// # SyncMessageEvent
|
||||
/// # },
|
||||
/// # locks::RwLock,
|
||||
/// # room::Room,
|
||||
/// # };
|
||||
///
|
||||
/// struct EventCallback;
|
||||
///
|
||||
/// #[async_trait]
|
||||
/// impl EventHandler for EventCallback {
|
||||
/// async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
/// if let Room::Joined(room) = room {
|
||||
/// if let SyncMessageEvent {
|
||||
/// content:
|
||||
/// MessageEventContent {
|
||||
/// msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
/// ..
|
||||
/// },
|
||||
/// sender,
|
||||
/// ..
|
||||
/// } = event
|
||||
/// {
|
||||
/// let member = room.get_member(&sender).await.unwrap().unwrap();
|
||||
/// let name = member
|
||||
/// .display_name()
|
||||
/// .unwrap_or_else(|| member.user_id().as_str());
|
||||
/// println!("{}: {}", name, msg_body);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventHandler: Send + Sync {
|
||||
// ROOM EVENTS from `IncomingTimeline`
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMember` event.
|
||||
async fn on_room_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
|
||||
async fn on_room_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
|
||||
async fn on_room_canonical_alias(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
|
||||
async fn on_room_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
|
||||
async fn on_room_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
|
||||
async fn on_room_message(&self, _: Room, _: &SyncMessageEvent<MsgEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
|
||||
async fn on_room_message_feedback(&self, _: Room, _: &SyncMessageEvent<FeedbackEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::Reaction` event.
|
||||
async fn on_room_reaction(&self, _: Room, _: &SyncMessageEvent<ReactionEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallInvite` event
|
||||
async fn on_room_call_invite(&self, _: Room, _: &SyncMessageEvent<InviteEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallAnswer` event
|
||||
async fn on_room_call_answer(&self, _: Room, _: &SyncMessageEvent<AnswerEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallCandidates` event
|
||||
async fn on_room_call_candidates(&self, _: Room, _: &SyncMessageEvent<CandidatesEventContent>) {
|
||||
}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallHangup` event
|
||||
async fn on_room_call_hangup(&self, _: Room, _: &SyncMessageEvent<HangupEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
|
||||
async fn on_room_redaction(&self, _: Room, _: &SyncRedactionEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
|
||||
async fn on_room_power_levels(&self, _: Room, _: &SyncStateEvent<PowerLevelsEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomJoinRules` event.
|
||||
async fn on_room_join_rules(&self, _: Room, _: &SyncStateEvent<JoinRulesEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
|
||||
async fn on_room_tombstone(&self, _: Room, _: &SyncStateEvent<TombstoneEventContent>) {}
|
||||
|
||||
/// Fires when `Client` receives room events that trigger notifications
|
||||
/// according to the push rules of the user.
|
||||
async fn on_room_notification(&self, _: Room, _: Notification) {}
|
||||
|
||||
// `RoomEvent`s from `IncomingState`
|
||||
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
|
||||
async fn on_state_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomName` event.
|
||||
async fn on_state_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
|
||||
async fn on_state_canonical_alias(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
|
||||
async fn on_state_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
|
||||
async fn on_state_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
|
||||
async fn on_state_power_levels(&self, _: Room, _: &SyncStateEvent<PowerLevelsEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
|
||||
async fn on_state_join_rules(&self, _: Room, _: &SyncStateEvent<JoinRulesEventContent>) {}
|
||||
|
||||
// `AnyStrippedStateEvent`s
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomMember` event.
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<MemberEventContent>,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName`
|
||||
/// event.
|
||||
async fn on_stripped_state_name(&self, _: Room, _: &StrippedStateEvent<NameEventContent>) {}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
|
||||
async fn on_stripped_state_canonical_alias(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomAliases` event.
|
||||
async fn on_stripped_state_aliases(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<AliasesEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomAvatar` event.
|
||||
async fn on_stripped_state_avatar(&self, _: Room, _: &StrippedStateEvent<AvatarEventContent>) {}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
|
||||
async fn on_stripped_state_power_levels(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
|
||||
async fn on_stripped_state_join_rules(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<JoinRulesEventContent>,
|
||||
) {
|
||||
}
|
||||
|
||||
// `NonRoomEvent` (this is a type alias from ruma_events)
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomPresence` event.
|
||||
async fn on_non_room_presence(&self, _: Room, _: &PresenceEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomName` event.
|
||||
async fn on_non_room_ignored_users(
|
||||
&self,
|
||||
_: &GlobalAccountDataEvent<IgnoredUserListEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomCanonicalAlias` event.
|
||||
async fn on_non_room_push_rules(&self, _: &GlobalAccountDataEvent<PushRulesEventContent>) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
|
||||
async fn on_non_room_fully_read(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &RoomAccountDataEvent<FullyReadEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::Typing` event.
|
||||
async fn on_non_room_typing(&self, _: Room, _: &SyncEphemeralRoomEvent<TypingEventContent>) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::Receipt` event.
|
||||
///
|
||||
/// This is always a read receipt.
|
||||
async fn on_non_room_receipt(&self, _: Room, _: &SyncEphemeralRoomEvent<ReceiptEventContent>) {}
|
||||
|
||||
// `PresenceEvent` is a struct so there is only the one method
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
|
||||
async fn on_presence_event(&self, _: &PresenceEvent) {}
|
||||
|
||||
/// Fires when `Client` receives a `Event::Custom` event or if
|
||||
/// deserialization fails because the event was unknown to ruma.
|
||||
///
|
||||
/// The only guarantee this method can give about the event is that it is
|
||||
/// valid JSON.
|
||||
async fn on_unrecognized_event(&self, _: Room, _: &RawJsonValue) {}
|
||||
|
||||
/// Fires when `Client` receives a `Event::Custom` event or if
|
||||
/// deserialization fails because the event was unknown to ruma.
|
||||
///
|
||||
/// The only guarantee this method can give about the event is that it is in
|
||||
/// the shape of a valid matrix event.
|
||||
async fn on_custom_event(&self, _: Room, _: &CustomEvent<'_>) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use matrix_sdk_common::{async_trait, locks::Mutex};
|
||||
use matrix_sdk_test::{async_test, test_json};
|
||||
use mockito::{mock, Matcher};
|
||||
use ruma::user_id;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen_test::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EvHandlerTest(Arc<Mutex<Vec<String>>>);
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventHandler for EvHandlerTest {
|
||||
async fn on_room_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {
|
||||
self.0.lock().await.push("member".to_string())
|
||||
}
|
||||
async fn on_room_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {
|
||||
self.0.lock().await.push("name".to_string())
|
||||
}
|
||||
async fn on_room_canonical_alias(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("canonical".to_string())
|
||||
}
|
||||
async fn on_room_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {
|
||||
self.0.lock().await.push("aliases".to_string())
|
||||
}
|
||||
async fn on_room_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {
|
||||
self.0.lock().await.push("avatar".to_string())
|
||||
}
|
||||
async fn on_room_message(&self, _: Room, _: &SyncMessageEvent<MsgEventContent>) {
|
||||
self.0.lock().await.push("message".to_string())
|
||||
}
|
||||
async fn on_room_message_feedback(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncMessageEvent<FeedbackEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("feedback".to_string())
|
||||
}
|
||||
async fn on_room_call_invite(&self, _: Room, _: &SyncMessageEvent<InviteEventContent>) {
|
||||
self.0.lock().await.push("call invite".to_string())
|
||||
}
|
||||
async fn on_room_call_answer(&self, _: Room, _: &SyncMessageEvent<AnswerEventContent>) {
|
||||
self.0.lock().await.push("call answer".to_string())
|
||||
}
|
||||
async fn on_room_call_candidates(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncMessageEvent<CandidatesEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("call candidates".to_string())
|
||||
}
|
||||
async fn on_room_call_hangup(&self, _: Room, _: &SyncMessageEvent<HangupEventContent>) {
|
||||
self.0.lock().await.push("call hangup".to_string())
|
||||
}
|
||||
async fn on_room_redaction(&self, _: Room, _: &SyncRedactionEvent) {
|
||||
self.0.lock().await.push("redaction".to_string())
|
||||
}
|
||||
async fn on_room_power_levels(&self, _: Room, _: &SyncStateEvent<PowerLevelsEventContent>) {
|
||||
self.0.lock().await.push("power".to_string())
|
||||
}
|
||||
async fn on_room_tombstone(&self, _: Room, _: &SyncStateEvent<TombstoneEventContent>) {
|
||||
self.0.lock().await.push("tombstone".to_string())
|
||||
}
|
||||
|
||||
async fn on_state_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {
|
||||
self.0.lock().await.push("state member".to_string())
|
||||
}
|
||||
async fn on_state_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {
|
||||
self.0.lock().await.push("state name".to_string())
|
||||
}
|
||||
async fn on_state_canonical_alias(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("state canonical".to_string())
|
||||
}
|
||||
async fn on_state_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {
|
||||
self.0.lock().await.push("state aliases".to_string())
|
||||
}
|
||||
async fn on_state_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {
|
||||
self.0.lock().await.push("state avatar".to_string())
|
||||
}
|
||||
async fn on_state_power_levels(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("state power".to_string())
|
||||
}
|
||||
async fn on_state_join_rules(&self, _: Room, _: &SyncStateEvent<JoinRulesEventContent>) {
|
||||
self.0.lock().await.push("state rules".to_string())
|
||||
}
|
||||
|
||||
// `AnyStrippedStateEvent`s
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomMember` event.
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<MemberEventContent>,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state member".to_string())
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomName` event.
|
||||
async fn on_stripped_state_name(&self, _: Room, _: &StrippedStateEvent<NameEventContent>) {
|
||||
self.0.lock().await.push("stripped state name".to_string())
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
|
||||
async fn on_stripped_state_canonical_alias(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state canonical".to_string())
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomAliases` event.
|
||||
async fn on_stripped_state_aliases(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<AliasesEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state aliases".to_string())
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomAvatar` event.
|
||||
async fn on_stripped_state_avatar(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<AvatarEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state avatar".to_string())
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
|
||||
async fn on_stripped_state_power_levels(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state power".to_string())
|
||||
}
|
||||
/// Fires when `Client` receives a
|
||||
/// `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
|
||||
async fn on_stripped_state_join_rules(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &StrippedStateEvent<JoinRulesEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state rules".to_string())
|
||||
}
|
||||
|
||||
async fn on_non_room_presence(&self, _: Room, _: &PresenceEvent) {
|
||||
self.0.lock().await.push("presence".to_string())
|
||||
}
|
||||
async fn on_non_room_ignored_users(
|
||||
&self,
|
||||
_: &GlobalAccountDataEvent<IgnoredUserListEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("account ignore".to_string())
|
||||
}
|
||||
async fn on_non_room_push_rules(&self, _: &GlobalAccountDataEvent<PushRulesEventContent>) {
|
||||
self.0.lock().await.push("account push rules".to_string())
|
||||
}
|
||||
async fn on_non_room_fully_read(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &RoomAccountDataEvent<FullyReadEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("account read".to_string())
|
||||
}
|
||||
async fn on_non_room_typing(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncEphemeralRoomEvent<TypingEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("typing event".to_string())
|
||||
}
|
||||
async fn on_non_room_receipt(
|
||||
&self,
|
||||
_: Room,
|
||||
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("receipt event".to_string())
|
||||
}
|
||||
async fn on_presence_event(&self, _: &PresenceEvent) {
|
||||
self.0.lock().await.push("presence event".to_string())
|
||||
}
|
||||
async fn on_unrecognized_event(&self, _: Room, _: &RawJsonValue) {
|
||||
self.0.lock().await.push("unrecognized event".to_string())
|
||||
}
|
||||
async fn on_custom_event(&self, _: Room, _: &CustomEvent<'_>) {
|
||||
self.0.lock().await.push("custom event".to_string())
|
||||
}
|
||||
async fn on_room_notification(&self, _: Room, _: Notification) {
|
||||
self.0.lock().await.push("notification".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::{Client, Session, SyncSettings};
|
||||
|
||||
async fn get_client() -> Client {
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: user_id!("@example:localhost"),
|
||||
device_id: "DEVICEID".into(),
|
||||
};
|
||||
let homeserver = url::Url::parse(&mockito::server_url()).unwrap();
|
||||
let client = Client::new(homeserver).unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
client
|
||||
}
|
||||
|
||||
async fn mock_sync(client: &Client, response: String) {
|
||||
let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()))
|
||||
.with_status(200)
|
||||
.match_header("authorization", "Bearer 1234")
|
||||
.with_body(response)
|
||||
.create();
|
||||
|
||||
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
|
||||
let _response = client.sync_once(sync_settings).await.unwrap();
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_handler_joined() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let handler = Box::new(EvHandlerTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.set_event_handler(handler).await;
|
||||
mock_sync(&client, test_json::SYNC.to_string()).await;
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"account ignore",
|
||||
"receipt event",
|
||||
"account read",
|
||||
"state rules",
|
||||
"state member",
|
||||
"state aliases",
|
||||
"state power",
|
||||
"state canonical",
|
||||
"state member",
|
||||
"state member",
|
||||
"message",
|
||||
"presence event",
|
||||
"notification",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_handler_invite() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let handler = Box::new(EvHandlerTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.set_event_handler(handler).await;
|
||||
mock_sync(&client, test_json::INVITE_SYNC.to_string()).await;
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(v.as_slice(), ["stripped state name", "stripped state member", "presence event"],)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_handler_leave() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let handler = Box::new(EvHandlerTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.set_event_handler(handler).await;
|
||||
mock_sync(&client, test_json::LEAVE_SYNC.to_string()).await;
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"account ignore",
|
||||
"state rules",
|
||||
"state member",
|
||||
"state aliases",
|
||||
"state power",
|
||||
"state canonical",
|
||||
"state member",
|
||||
"state member",
|
||||
"message",
|
||||
"presence event",
|
||||
"notification",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_handler_more_events() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let handler = Box::new(EvHandlerTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.set_event_handler(handler).await;
|
||||
mock_sync(&client, test_json::MORE_SYNC.to_string()).await;
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"receipt event",
|
||||
"typing event",
|
||||
"message",
|
||||
"message", // this is a message edit event
|
||||
"redaction",
|
||||
"message", // this is a notice event
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_handler_voip() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let handler = Box::new(EvHandlerTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.set_event_handler(handler).await;
|
||||
mock_sync(&client, test_json::VOIP_SYNC.to_string()).await;
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(v.as_slice(), ["call invite", "call answer", "call candidates", "call hangup",],)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_handler_two_syncs() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let handler = Box::new(EvHandlerTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.set_event_handler(handler).await;
|
||||
mock_sync(&client, test_json::SYNC.to_string()).await;
|
||||
mock_sync(&client, test_json::MORE_SYNC.to_string()).await;
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"account ignore",
|
||||
"receipt event",
|
||||
"account read",
|
||||
"state rules",
|
||||
"state member",
|
||||
"state aliases",
|
||||
"state power",
|
||||
"state canonical",
|
||||
"state member",
|
||||
"state member",
|
||||
"message",
|
||||
"presence event",
|
||||
"notification",
|
||||
"receipt event",
|
||||
"typing event",
|
||||
"message",
|
||||
"message", // this is a message edit event
|
||||
"redaction",
|
||||
"message", // this is a notice event
|
||||
"notification",
|
||||
"notification",
|
||||
"notification",
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// 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.
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32")))]
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32")))]
|
||||
use backoff::{future::retry, Error as RetryError, ExponentialBackoff};
|
||||
#[cfg(all(not(target_arch = "wasm32")))]
|
||||
use http::StatusCode;
|
||||
use http::{HeaderValue, Response as HttpResponse};
|
||||
use matrix_sdk_common::{async_trait, locks::RwLock, AsyncTraitDeps};
|
||||
use reqwest::{Client, Response};
|
||||
use ruma::api::{
|
||||
client::r0::media::create_content, error::FromHttpResponseError, AuthScheme, IncomingResponse,
|
||||
OutgoingRequest, OutgoingRequestAppserviceExt, SendAccessToken,
|
||||
};
|
||||
use tracing::trace;
|
||||
use url::Url;
|
||||
|
||||
use crate::{error::HttpError, Bytes, BytesMut, ClientConfig, RequestConfig, Session};
|
||||
|
||||
/// Abstraction around the http layer. The allows implementors to use different
|
||||
/// http libraries.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait HttpSend: AsyncTraitDeps {
|
||||
/// The method abstracting sending request types and receiving response
|
||||
/// types.
|
||||
///
|
||||
/// This is called by the client every time it wants to send anything to a
|
||||
/// homeserver.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The http request that has been converted from a ruma
|
||||
/// `Request`.
|
||||
///
|
||||
/// * `request_config` - The config used for this request.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::convert::TryFrom;
|
||||
/// use matrix_sdk::{HttpSend, async_trait, HttpError, RequestConfig, Bytes};
|
||||
///
|
||||
/// #[derive(Debug)]
|
||||
/// struct Client(reqwest::Client);
|
||||
///
|
||||
/// impl Client {
|
||||
/// async fn response_to_http_response(
|
||||
/// &self,
|
||||
/// mut response: reqwest::Response,
|
||||
/// ) -> Result<http::Response<Bytes>, HttpError> {
|
||||
/// // Convert the reqwest response to a http one.
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[async_trait]
|
||||
/// impl HttpSend for Client {
|
||||
/// async fn send_request(
|
||||
/// &self,
|
||||
/// request: http::Request<Bytes>,
|
||||
/// config: RequestConfig,
|
||||
/// ) -> Result<http::Response<Bytes>, HttpError> {
|
||||
/// Ok(self
|
||||
/// .response_to_http_response(
|
||||
/// self.0
|
||||
/// .execute(reqwest::Request::try_from(request)?)
|
||||
/// .await?,
|
||||
/// )
|
||||
/// .await?)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
async fn send_request(
|
||||
&self,
|
||||
request: http::Request<Bytes>,
|
||||
config: RequestConfig,
|
||||
) -> Result<http::Response<Bytes>, HttpError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct HttpClient {
|
||||
pub(crate) inner: Arc<dyn HttpSend>,
|
||||
pub(crate) homeserver: Arc<RwLock<Url>>,
|
||||
pub(crate) session: Arc<RwLock<Option<Session>>>,
|
||||
pub(crate) request_config: RequestConfig,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub(crate) fn new(
|
||||
inner: Arc<dyn HttpSend>,
|
||||
homeserver: Arc<RwLock<Url>>,
|
||||
session: Arc<RwLock<Option<Session>>>,
|
||||
request_config: RequestConfig,
|
||||
) -> Self {
|
||||
HttpClient { inner, homeserver, session, request_config }
|
||||
}
|
||||
|
||||
async fn send_request<Request: OutgoingRequest>(
|
||||
&self,
|
||||
request: Request,
|
||||
session: Arc<RwLock<Option<Session>>>,
|
||||
config: Option<RequestConfig>,
|
||||
) -> Result<http::Response<Bytes>, HttpError> {
|
||||
let config = match config {
|
||||
Some(config) => config,
|
||||
None => self.request_config,
|
||||
};
|
||||
|
||||
let request = if !self.request_config.assert_identity {
|
||||
self.try_into_http_request(request, session, config).await?
|
||||
} else {
|
||||
self.try_into_http_request_with_identity_assertion(request, session, config).await?
|
||||
};
|
||||
|
||||
self.inner.send_request(request, config).await
|
||||
}
|
||||
|
||||
async fn try_into_http_request<Request: OutgoingRequest>(
|
||||
&self,
|
||||
request: Request,
|
||||
session: Arc<RwLock<Option<Session>>>,
|
||||
config: RequestConfig,
|
||||
) -> Result<http::Request<Bytes>, HttpError> {
|
||||
let read_guard;
|
||||
let access_token = if config.force_auth {
|
||||
read_guard = session.read().await;
|
||||
if let Some(session) = read_guard.as_ref() {
|
||||
SendAccessToken::Always(session.access_token.as_str())
|
||||
} else {
|
||||
return Err(HttpError::ForcedAuthenticationWithoutAccessToken);
|
||||
}
|
||||
} else {
|
||||
match Request::METADATA.authentication {
|
||||
AuthScheme::AccessToken => {
|
||||
read_guard = session.read().await;
|
||||
|
||||
if let Some(session) = read_guard.as_ref() {
|
||||
SendAccessToken::IfRequired(session.access_token.as_str())
|
||||
} else {
|
||||
return Err(HttpError::AuthenticationRequired);
|
||||
}
|
||||
}
|
||||
AuthScheme::None => SendAccessToken::None,
|
||||
_ => return Err(HttpError::NotClientRequest),
|
||||
}
|
||||
};
|
||||
|
||||
let http_request = request
|
||||
.try_into_http_request::<BytesMut>(
|
||||
&self.homeserver.read().await.to_string(),
|
||||
access_token,
|
||||
)?
|
||||
.map(|body| body.freeze());
|
||||
|
||||
Ok(http_request)
|
||||
}
|
||||
|
||||
async fn try_into_http_request_with_identity_assertion<Request: OutgoingRequest>(
|
||||
&self,
|
||||
request: Request,
|
||||
session: Arc<RwLock<Option<Session>>>,
|
||||
_: RequestConfig,
|
||||
) -> Result<http::Request<Bytes>, HttpError> {
|
||||
let read_guard = session.read().await;
|
||||
let access_token = if let Some(session) = read_guard.as_ref() {
|
||||
SendAccessToken::Always(session.access_token.as_str())
|
||||
} else {
|
||||
return Err(HttpError::AuthenticationRequired);
|
||||
};
|
||||
|
||||
let user_id = if let Some(session) = read_guard.as_ref() {
|
||||
session.user_id.clone()
|
||||
} else {
|
||||
return Err(HttpError::UserIdRequired);
|
||||
};
|
||||
|
||||
let http_request = request
|
||||
.try_into_http_request_with_user_id::<BytesMut>(
|
||||
&self.homeserver.read().await.to_string(),
|
||||
access_token,
|
||||
user_id,
|
||||
)?
|
||||
.map(|body| body.freeze());
|
||||
|
||||
Ok(http_request)
|
||||
}
|
||||
|
||||
pub async fn upload(
|
||||
&self,
|
||||
request: create_content::Request<'_>,
|
||||
config: Option<RequestConfig>,
|
||||
) -> Result<create_content::Response, HttpError> {
|
||||
let response = self.send_request(request, self.session.clone(), config).await?;
|
||||
Ok(create_content::Response::try_from_http_response(response)?)
|
||||
}
|
||||
|
||||
pub async fn send<Request>(
|
||||
&self,
|
||||
request: Request,
|
||||
config: Option<RequestConfig>,
|
||||
) -> Result<Request::IncomingResponse, HttpError>
|
||||
where
|
||||
Request: OutgoingRequest + Debug,
|
||||
HttpError: From<FromHttpResponseError<Request::EndpointError>>,
|
||||
{
|
||||
let response = self.send_request(request, self.session.clone(), config).await?;
|
||||
|
||||
trace!("Got response: {:?}", response);
|
||||
|
||||
let response = Request::IncomingResponse::try_from_http_response(response)?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a client with the specified configuration.
|
||||
pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client, HttpError> {
|
||||
let http_client = reqwest::Client::builder();
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let http_client = {
|
||||
let http_client = if config.disable_ssl_verification {
|
||||
http_client.danger_accept_invalid_certs(true)
|
||||
} else {
|
||||
http_client
|
||||
};
|
||||
|
||||
let http_client = match &config.proxy {
|
||||
Some(p) => http_client.proxy(p.clone()),
|
||||
None => http_client,
|
||||
};
|
||||
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
|
||||
let user_agent = match &config.user_agent {
|
||||
Some(a) => a.clone(),
|
||||
None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", crate::VERSION))
|
||||
.expect("Can't construct the version header"),
|
||||
};
|
||||
|
||||
headers.insert(reqwest::header::USER_AGENT, user_agent);
|
||||
|
||||
http_client.default_headers(headers).timeout(config.request_config.timeout)
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[allow(unused)]
|
||||
let _ = config;
|
||||
|
||||
Ok(http_client.build()?)
|
||||
}
|
||||
|
||||
async fn response_to_http_response(
|
||||
mut response: Response,
|
||||
) -> Result<http::Response<Bytes>, reqwest::Error> {
|
||||
let status = response.status();
|
||||
|
||||
let mut http_builder = HttpResponse::builder().status(status);
|
||||
let headers = http_builder.headers_mut().expect("Can't get the response builder headers");
|
||||
|
||||
for (k, v) in response.headers_mut().drain() {
|
||||
if let Some(key) = k {
|
||||
headers.insert(key, v);
|
||||
}
|
||||
}
|
||||
|
||||
let body = response.bytes().await?;
|
||||
|
||||
Ok(http_builder.body(body).expect("Can't construct a response using the given body"))
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32"))]
|
||||
async fn send_request(
|
||||
client: &Client,
|
||||
request: http::Request<Bytes>,
|
||||
_: RequestConfig,
|
||||
) -> Result<http::Response<Bytes>, HttpError> {
|
||||
let request = reqwest::Request::try_from(request)?;
|
||||
let response = client.execute(request).await?;
|
||||
|
||||
Ok(response_to_http_response(response).await?)
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32")))]
|
||||
async fn send_request(
|
||||
client: &Client,
|
||||
request: http::Request<Bytes>,
|
||||
config: RequestConfig,
|
||||
) -> Result<http::Response<Bytes>, HttpError> {
|
||||
let mut backoff = ExponentialBackoff::default();
|
||||
let mut request = reqwest::Request::try_from(request)?;
|
||||
let retry_limit = config.retry_limit;
|
||||
let retry_count = AtomicU64::new(1);
|
||||
|
||||
*request.timeout_mut() = Some(config.timeout);
|
||||
|
||||
backoff.max_elapsed_time = config.retry_timeout;
|
||||
|
||||
let request = &request;
|
||||
let retry_count = &retry_count;
|
||||
|
||||
let request = || async move {
|
||||
let stop = if let Some(retry_limit) = retry_limit {
|
||||
retry_count.fetch_add(1, Ordering::Relaxed) >= retry_limit
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Turn errors into permanent errors when the retry limit is reached
|
||||
let error_type = if stop { RetryError::Permanent } else { RetryError::Transient };
|
||||
|
||||
let request = request.try_clone().ok_or(HttpError::UnableToCloneRequest)?;
|
||||
|
||||
let response =
|
||||
client.execute(request).await.map_err(|e| error_type(HttpError::Reqwest(e)))?;
|
||||
|
||||
let status_code = response.status();
|
||||
// TODO TOO_MANY_REQUESTS will have a retry timeout which we should
|
||||
// use.
|
||||
if !stop
|
||||
&& (status_code.is_server_error() || response.status() == StatusCode::TOO_MANY_REQUESTS)
|
||||
{
|
||||
return Err(error_type(HttpError::Server(status_code)));
|
||||
}
|
||||
|
||||
let response = response_to_http_response(response)
|
||||
.await
|
||||
.map_err(|e| RetryError::Permanent(HttpError::Reqwest(e)))?;
|
||||
|
||||
Ok(response)
|
||||
};
|
||||
|
||||
let response = retry(backoff, request).await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl HttpSend for Client {
|
||||
async fn send_request(
|
||||
&self,
|
||||
request: http::Request<Bytes>,
|
||||
config: RequestConfig,
|
||||
) -> Result<http::Response<Bytes>, HttpError> {
|
||||
send_request(self, request, config).await
|
||||
}
|
||||
}
|
||||
+85
-13
@@ -15,15 +15,44 @@
|
||||
|
||||
//! This crate implements a [Matrix](https://matrix.org/) client library.
|
||||
//!
|
||||
//! ## Crate Feature Flags
|
||||
//! # Enabling logging
|
||||
//!
|
||||
//! Users of the matrix-sdk crate can enable log output by depending on the
|
||||
//! `tracing-subscriber` crate and including the following line in their
|
||||
//! application (e.g. at the start of `main`):
|
||||
//!
|
||||
//! ```rust
|
||||
//! tracing_subscriber::fmt::init();
|
||||
//! ```
|
||||
//!
|
||||
//! The log output is controlled via the `RUST_LOG` environment variable by
|
||||
//! setting it to one of the `error`, `warn`, `info`, `debug` or `trace` levels.
|
||||
//! The output is printed to stdout.
|
||||
//!
|
||||
//! The `RUST_LOG` variable also supports a more advanced syntax for filtering
|
||||
//! log output more precisely, for instance with crate-level granularity. For
|
||||
//! more information on this, check out the [tracing_subscriber
|
||||
//! documentation](https://tracing.rs/tracing_subscriber/filter/struct.envfilter).
|
||||
//!
|
||||
//! # Crate Feature Flags
|
||||
//!
|
||||
//! The following crate feature flags are available:
|
||||
//!
|
||||
//! * `encryption`: Enables end-to-end encryption support in the library.
|
||||
//! * `sqlite-cryptostore`: Enables a SQLite based store for the encryption
|
||||
//! * `sled_cryptostore`: Enables a Sled based store for the encryption
|
||||
//! keys. If this is disabled and `encryption` support is enabled the keys will
|
||||
//! by default be stored only in memory and thus lost after the client is
|
||||
//! destroyed.
|
||||
//! * `markdown`: Support for sending markdown formatted messages.
|
||||
//! * `socks`: Enables SOCKS support in reqwest, the default HTTP client.
|
||||
//! * `sso_login`: Enables SSO login with a local http server.
|
||||
//! * `require_auth_for_profile_requests`: Whether to send the access token in
|
||||
//! the authentication
|
||||
//! header when calling endpoints that retrieve profile data. This matches the
|
||||
//! synapse configuration `require_auth_for_profile_requests`. Enabled by
|
||||
//! default.
|
||||
//! * `appservice`: Enables low-level appservice functionality. For an
|
||||
//! high-level API there's the `matrix-sdk-appservice` crate
|
||||
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
@@ -35,23 +64,66 @@
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
#![cfg_attr(feature = "docs", feature(doc_cfg))]
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use matrix_sdk_base::JsonStore;
|
||||
pub use matrix_sdk_base::{EventEmitter, Room, Session, SyncRoom};
|
||||
pub use matrix_sdk_base::{RoomState, StateStore};
|
||||
pub use matrix_sdk_common::*;
|
||||
pub use reqwest::header::InvalidHeaderValue;
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls",)))]
|
||||
compile_error!("one of 'native-tls' or 'rustls-tls' features must be enabled");
|
||||
|
||||
#[cfg(all(feature = "native-tls", feature = "rustls-tls",))]
|
||||
compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled");
|
||||
|
||||
#[cfg(all(feature = "sso_login", target_arch = "wasm32"))]
|
||||
compile_error!("'sso_login' cannot be enabled on 'wasm32' arch");
|
||||
|
||||
pub use bytes::{Bytes, BytesMut};
|
||||
#[cfg(feature = "encryption")]
|
||||
pub use matrix_sdk_base::{Device, TrustState};
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
pub use matrix_sdk_base::crypto::{EncryptionInfo, LocalTrust};
|
||||
pub use matrix_sdk_base::{
|
||||
media, Error as BaseError, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType,
|
||||
Session, StateChanges, StoreError,
|
||||
};
|
||||
pub use matrix_sdk_common::*;
|
||||
pub use reqwest;
|
||||
#[cfg(feature = "appservice")]
|
||||
pub use ruma::{
|
||||
api::{appservice as api_appservice, IncomingRequest, OutgoingRequestAppserviceExt},
|
||||
serde::{exports::serde::de::value::Error as SerdeError, urlencoded},
|
||||
};
|
||||
pub use ruma::{
|
||||
api::{
|
||||
client as api,
|
||||
error::{
|
||||
FromHttpRequestError, FromHttpResponseError, IntoHttpError, MatrixError, ServerError,
|
||||
},
|
||||
AuthScheme, EndpointError, IncomingResponse, OutgoingRequest, SendAccessToken,
|
||||
},
|
||||
assign, directory, encryption, events, identifiers, int, presence, push, receipt,
|
||||
serde::{CanonicalJsonValue, Raw},
|
||||
thirdparty, uint, Int, MilliSecondsSinceUnixEpoch, Outgoing, SecondsSinceUnixEpoch, UInt,
|
||||
};
|
||||
|
||||
mod client;
|
||||
mod error;
|
||||
mod request_builder;
|
||||
pub use client::{Client, ClientConfig, SyncSettings};
|
||||
pub use error::{Error, Result};
|
||||
pub use request_builder::{MessagesRequestBuilder, RoomBuilder};
|
||||
mod event_handler;
|
||||
mod http_client;
|
||||
/// High-level room API
|
||||
pub mod room;
|
||||
/// High-level room API
|
||||
mod room_member;
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
mod device;
|
||||
#[cfg(feature = "encryption")]
|
||||
pub mod verification;
|
||||
|
||||
pub use client::{Client, ClientConfig, LoopCtrl, RequestConfig, SyncSettings};
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
pub use device::Device;
|
||||
pub use error::{Error, HttpError, Result};
|
||||
pub use event_handler::{CustomEvent, EventHandler};
|
||||
pub use http_client::HttpSend;
|
||||
pub use room_member::RoomMember;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
use crate::api;
|
||||
use crate::events::room::power_levels::PowerLevelsEventContent;
|
||||
use crate::events::EventJson;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use api::r0::filter::RoomEventFilter;
|
||||
use api::r0::membership::Invite3pid;
|
||||
use api::r0::message::get_message_events::{self, Direction};
|
||||
use api::r0::room::{
|
||||
create_room::{self, CreationContent, InitialStateEvent, RoomPreset},
|
||||
Visibility,
|
||||
};
|
||||
|
||||
use crate::js_int::UInt;
|
||||
|
||||
/// A builder used to create rooms.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::convert::TryFrom;
|
||||
/// # use matrix_sdk::{Client, RoomBuilder};
|
||||
/// # use matrix_sdk::api::r0::room::Visibility;
|
||||
/// # use matrix_sdk::identifiers::UserId;
|
||||
/// # use url::Url;
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// # rt.block_on(async {
|
||||
/// let mut builder = RoomBuilder::default();
|
||||
/// builder.creation_content(false)
|
||||
/// .initial_state(vec![])
|
||||
/// .visibility(Visibility::Public)
|
||||
/// .name("name")
|
||||
/// .room_version("v1.0");
|
||||
/// let mut client = Client::new(homeserver).unwrap();
|
||||
/// client.create_room(builder).await;
|
||||
/// # })
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RoomBuilder {
|
||||
/// Extra keys to be added to the content of the `m.room.create`.
|
||||
creation_content: Option<CreationContent>,
|
||||
/// List of state events to send to the new room.
|
||||
///
|
||||
/// Takes precedence over events set by preset, but gets overriden by
|
||||
/// name and topic keys.
|
||||
initial_state: Vec<InitialStateEvent>,
|
||||
/// A list of user IDs to invite to the room.
|
||||
///
|
||||
/// This will tell the server to invite everyone in the list to the newly created room.
|
||||
invite: Vec<UserId>,
|
||||
/// List of third party IDs of users to invite.
|
||||
invite_3pid: Vec<Invite3pid>,
|
||||
/// If set, this sets the `is_direct` flag on room invites.
|
||||
is_direct: Option<bool>,
|
||||
/// If this is included, an `m.room.name` event will be sent into the room to indicate
|
||||
/// the name of the room.
|
||||
name: Option<String>,
|
||||
/// Power level content to override in the default power level event.
|
||||
power_level_content_override: Option<PowerLevelsEventContent>,
|
||||
/// Convenience parameter for setting various default state events based on a preset.
|
||||
preset: Option<RoomPreset>,
|
||||
/// The desired room alias local part.
|
||||
room_alias_name: Option<String>,
|
||||
/// Room version to set for the room. Defaults to homeserver's default if not specified.
|
||||
room_version: Option<String>,
|
||||
/// If this is included, an `m.room.topic` event will be sent into the room to indicate
|
||||
/// the topic for the room.
|
||||
topic: Option<String>,
|
||||
/// A public visibility indicates that the room will be shown in the published room
|
||||
/// list. A private visibility will hide the room from the published room list. Rooms
|
||||
/// default to private visibility if this key is not included.
|
||||
visibility: Option<Visibility>,
|
||||
}
|
||||
|
||||
impl RoomBuilder {
|
||||
/// Returns an empty `RoomBuilder` for creating rooms.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the `CreationContent`.
|
||||
///
|
||||
/// Weather users on other servers can join this room.
|
||||
pub fn creation_content(&mut self, federate: bool) -> &mut Self {
|
||||
let federate = Some(federate);
|
||||
self.creation_content = Some(CreationContent { federate });
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `InitialStateEvent` vector.
|
||||
pub fn initial_state(&mut self, state: Vec<InitialStateEvent>) -> &mut Self {
|
||||
self.initial_state = state;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the vec of `UserId`s.
|
||||
pub fn invite(&mut self, invite: Vec<UserId>) -> &mut Self {
|
||||
self.invite = invite;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the vec of `Invite3pid`s.
|
||||
pub fn invite_3pid(&mut self, invite: Vec<Invite3pid>) -> &mut Self {
|
||||
self.invite_3pid = invite;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the vec of `Invite3pid`s.
|
||||
pub fn is_direct(&mut self, direct: bool) -> &mut Self {
|
||||
self.is_direct = Some(direct);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the room name. A `m.room.name` event will be sent to the room.
|
||||
pub fn name<S: Into<String>>(&mut self, name: S) -> &mut Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the room's power levels.
|
||||
pub fn power_level_override(&mut self, power: PowerLevelsEventContent) -> &mut Self {
|
||||
self.power_level_content_override = Some(power);
|
||||
self
|
||||
}
|
||||
|
||||
/// Convenience for setting various default state events based on a preset.
|
||||
pub fn preset(&mut self, preset: RoomPreset) -> &mut Self {
|
||||
self.preset = Some(preset);
|
||||
self
|
||||
}
|
||||
|
||||
/// The local part of a room alias.
|
||||
pub fn room_alias_name<S: Into<String>>(&mut self, alias: S) -> &mut Self {
|
||||
self.room_alias_name = Some(alias.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Room version, defaults to homeserver's version if left unspecified.
|
||||
pub fn room_version<S: Into<String>>(&mut self, version: S) -> &mut Self {
|
||||
self.room_version = Some(version.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// If included, a `m.room.topic` event will be sent to the room.
|
||||
pub fn topic<S: Into<String>>(&mut self, topic: S) -> &mut Self {
|
||||
self.topic = Some(topic.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// A public visibility indicates that the room will be shown in the published
|
||||
/// room list. A private visibility will hide the room from the published room list.
|
||||
/// Rooms default to private visibility if this key is not included.
|
||||
pub fn visibility(&mut self, vis: Visibility) -> &mut Self {
|
||||
self.visibility = Some(vis);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<create_room::Request> for RoomBuilder {
|
||||
fn into(self) -> create_room::Request {
|
||||
create_room::Request {
|
||||
creation_content: self.creation_content,
|
||||
initial_state: self.initial_state,
|
||||
invite: self.invite,
|
||||
invite_3pid: self.invite_3pid,
|
||||
is_direct: self.is_direct,
|
||||
name: self.name,
|
||||
power_level_content_override: self.power_level_content_override.map(EventJson::from),
|
||||
preset: self.preset,
|
||||
room_alias_name: self.room_alias_name,
|
||||
room_version: self.room_version,
|
||||
topic: self.topic,
|
||||
visibility: self.visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a builder for making get_message_event requests.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::convert::TryFrom;
|
||||
/// # use matrix_sdk::{Client, MessagesRequestBuilder};
|
||||
/// # use matrix_sdk::api::r0::message::get_message_events::{self, Direction};
|
||||
/// # use matrix_sdk::identifiers::RoomId;
|
||||
/// # use url::Url;
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// # rt.block_on(async {
|
||||
/// # let room_id = RoomId::try_from("!test:localhost").unwrap();
|
||||
/// # let last_sync_token = "".to_string();
|
||||
/// let mut client = Client::new(homeserver).unwrap();
|
||||
///
|
||||
/// let mut builder = MessagesRequestBuilder::new();
|
||||
/// builder.room_id(room_id)
|
||||
/// .from(last_sync_token)
|
||||
/// .direction(Direction::Forward);
|
||||
///
|
||||
/// client.room_messages(builder).await.is_err();
|
||||
/// # })
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MessagesRequestBuilder {
|
||||
/// The room to get events from.
|
||||
room_id: Option<RoomId>,
|
||||
/// The token to start returning events from.
|
||||
///
|
||||
/// This token can be obtained from a
|
||||
/// prev_batch token returned for each room by the sync API, or from a start or end token
|
||||
/// returned by a previous request to this endpoint.
|
||||
from: Option<String>,
|
||||
/// The token to stop returning events at.
|
||||
///
|
||||
/// This token can be obtained from a prev_batch
|
||||
/// token returned for each room by the sync endpoint, or from a start or end token returned
|
||||
/// by a previous request to this endpoint.
|
||||
to: Option<String>,
|
||||
/// The direction to return events from.
|
||||
direction: Option<Direction>,
|
||||
/// The maximum number of events to return.
|
||||
///
|
||||
/// Default: 10.
|
||||
limit: Option<UInt>,
|
||||
/// A filter of the returned events with.
|
||||
filter: Option<RoomEventFilter>,
|
||||
}
|
||||
|
||||
impl MessagesRequestBuilder {
|
||||
/// Create a `MessagesRequestBuilder` builder to make a `get_message_events::Request`.
|
||||
///
|
||||
/// The `room_id` and `from`` fields **need to be set** to create the request.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// RoomId is required to create a `get_message_events::Request`.
|
||||
pub fn room_id(&mut self, room_id: RoomId) -> &mut Self {
|
||||
self.room_id = Some(room_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
|
||||
///
|
||||
/// This is required to create a `get_message_events::Request`.
|
||||
pub fn from(&mut self, from: String) -> &mut Self {
|
||||
self.from = Some(from);
|
||||
self
|
||||
}
|
||||
|
||||
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
|
||||
///
|
||||
/// This token signals when to stop receiving events.
|
||||
pub fn to(&mut self, to: String) -> &mut Self {
|
||||
self.to = Some(to);
|
||||
self
|
||||
}
|
||||
|
||||
/// The direction to return events from.
|
||||
///
|
||||
/// If not specified `Direction::Backward` is used.
|
||||
pub fn direction(&mut self, direction: Direction) -> &mut Self {
|
||||
self.direction = Some(direction);
|
||||
self
|
||||
}
|
||||
|
||||
/// The maximum number of events to return.
|
||||
pub fn limit(&mut self, limit: UInt) -> &mut Self {
|
||||
self.limit = Some(limit);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter events by the given `RoomEventFilter`.
|
||||
pub fn filter(&mut self, filter: RoomEventFilter) -> &mut Self {
|
||||
self.filter = Some(filter);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<get_message_events::Request> for MessagesRequestBuilder {
|
||||
fn into(self) -> get_message_events::Request {
|
||||
get_message_events::Request {
|
||||
room_id: self.room_id.expect("`room_id` and `from` need to be set"),
|
||||
from: self.from.expect("`room_id` and `from` need to be set"),
|
||||
to: self.to,
|
||||
dir: self.direction.unwrap_or(Direction::Backward),
|
||||
limit: self.limit,
|
||||
filter: self.filter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::*;
|
||||
use crate::api::r0::filter::{LazyLoadOptions, RoomEventFilter};
|
||||
use crate::events::room::power_levels::NotificationPowerLevels;
|
||||
use crate::js_int::Int;
|
||||
use crate::{identifiers::RoomId, Client, Session};
|
||||
|
||||
use mockito::{mock, Matcher};
|
||||
use std::convert::TryFrom;
|
||||
use url::Url;
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_room_builder() {
|
||||
let homeserver = Url::parse(&mockito::server_url()).unwrap();
|
||||
|
||||
let _m = mock("POST", "/_matrix/client/r0/createRoom")
|
||||
.with_status(200)
|
||||
.with_body_from_file("../test_data/room_id.json")
|
||||
.create();
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
|
||||
let mut builder = RoomBuilder::new();
|
||||
builder
|
||||
.creation_content(false)
|
||||
.initial_state(vec![])
|
||||
.visibility(Visibility::Public)
|
||||
.name("room_name")
|
||||
.room_version("v1.0")
|
||||
.invite_3pid(vec![])
|
||||
.is_direct(true)
|
||||
.power_level_override(PowerLevelsEventContent {
|
||||
ban: Int::MAX,
|
||||
events: BTreeMap::default(),
|
||||
events_default: Int::MIN,
|
||||
invite: Int::MIN,
|
||||
kick: Int::MIN,
|
||||
redact: Int::MAX,
|
||||
state_default: Int::MIN,
|
||||
users_default: Int::MIN,
|
||||
notifications: NotificationPowerLevels { room: Int::MIN },
|
||||
users: BTreeMap::default(),
|
||||
})
|
||||
.preset(RoomPreset::PrivateChat)
|
||||
.room_alias_name("room_alias")
|
||||
.topic("room topic")
|
||||
.visibility(Visibility::Private);
|
||||
let cli = Client::new(homeserver).unwrap();
|
||||
cli.restore_login(session).await.unwrap();
|
||||
assert!(cli.create_room(builder).await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_message_events() {
|
||||
let homeserver = Url::parse(&mockito::server_url()).unwrap();
|
||||
|
||||
let _m = mock(
|
||||
"GET",
|
||||
Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/messages".to_string()),
|
||||
)
|
||||
.with_status(200)
|
||||
.with_body_from_file("../test_data/room_messages.json")
|
||||
.create();
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
|
||||
let mut builder = MessagesRequestBuilder::new();
|
||||
builder
|
||||
.room_id(RoomId::try_from("!roomid:example.com").unwrap())
|
||||
.from("t47429-4392820_219380_26003_2265".to_string())
|
||||
.to("t4357353_219380_26003_2265".to_string())
|
||||
.direction(Direction::Backward)
|
||||
.limit(UInt::new(10).unwrap())
|
||||
.filter(RoomEventFilter {
|
||||
lazy_load_options: LazyLoadOptions::Enabled {
|
||||
include_redundant_members: false,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let cli = Client::new(homeserver).unwrap();
|
||||
cli.restore_login(session).await.unwrap();
|
||||
assert!(cli.room_messages(builder).await.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use matrix_sdk_base::deserialized_responses::MembersResponse;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use ruma::{
|
||||
api::client::r0::{
|
||||
membership::{get_member_events, join_room_by_id, leave_room},
|
||||
message::get_message_events,
|
||||
},
|
||||
events::room::history_visibility::HistoryVisibility,
|
||||
UserId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
media::{MediaFormat, MediaRequest, MediaType},
|
||||
room::RoomType,
|
||||
BaseRoom, Client, Result, RoomMember,
|
||||
};
|
||||
|
||||
/// A struct containing methods that are common for Joined, Invited and Left
|
||||
/// Rooms
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Common {
|
||||
inner: BaseRoom,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl Deref for Common {
|
||||
type Target = BaseRoom;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Common {
|
||||
/// Create a new `room::Common`
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - The client used to make requests.
|
||||
///
|
||||
/// * `room` - The underlying room.
|
||||
pub fn new(client: Client, room: BaseRoom) -> Self {
|
||||
// TODO: Make this private
|
||||
Self { inner: room, client }
|
||||
}
|
||||
|
||||
/// Leave this room.
|
||||
///
|
||||
/// Only invited and joined rooms can be left
|
||||
pub(crate) async fn leave(&self) -> Result<()> {
|
||||
let request = leave_room::Request::new(self.inner.room_id());
|
||||
let _response = self.client.send(request, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Join this room.
|
||||
///
|
||||
/// Only invited and left rooms can be joined via this method
|
||||
pub(crate) async fn join(&self) -> Result<()> {
|
||||
let request = join_room_by_id::Request::new(self.inner.room_id());
|
||||
let _response = self.client.send(request, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the avatar of this room, if set.
|
||||
///
|
||||
/// Returns the avatar.
|
||||
/// If a thumbnail is requested no guarantee on the size of the image is
|
||||
/// given.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `format` - The desired format of the avatar.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use futures::executor::block_on;
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use matrix_sdk::identifiers::room_id;
|
||||
/// # use matrix_sdk::media::MediaFormat;
|
||||
/// # use url::Url;
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # block_on(async {
|
||||
/// # let user = "example";
|
||||
/// let client = Client::new(homeserver).unwrap();
|
||||
/// client.login(user, "password", None, None).await.unwrap();
|
||||
/// let room_id = room_id!("!roomid:example.com");
|
||||
/// let room = client
|
||||
/// .get_joined_room(&room_id)
|
||||
/// .unwrap();
|
||||
/// if let Some(avatar) = room.avatar(MediaFormat::File).await.unwrap() {
|
||||
/// std::fs::write("avatar.png", avatar);
|
||||
/// }
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
|
||||
if let Some(url) = self.avatar_url() {
|
||||
let request = MediaRequest { media_type: MediaType::Uri(url.clone()), format };
|
||||
Ok(Some(self.client.get_media_content(&request, true).await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and
|
||||
/// returns a `get_message_events::Response` that contains a chunk of
|
||||
/// room and state events (`AnyRoomEvent` and `AnyStateEvent`).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The easiest way to create this request is using the
|
||||
/// `get_message_events::Request` itself.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// # use std::convert::TryFrom;
|
||||
/// use matrix_sdk::Client;
|
||||
/// # use matrix_sdk::identifiers::room_id;
|
||||
/// # use matrix_sdk::api::r0::filter::RoomEventFilter;
|
||||
/// # use matrix_sdk::api::r0::message::get_message_events::Request as MessagesRequest;
|
||||
/// # use url::Url;
|
||||
///
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// let room_id = room_id!("!roomid:example.com");
|
||||
/// let request = MessagesRequest::backward(&room_id, "t47429-4392820_219380_26003_2265");
|
||||
///
|
||||
/// let mut client = Client::new(homeserver).unwrap();
|
||||
/// # let room = client
|
||||
/// # .get_joined_room(&room_id)
|
||||
/// # .unwrap();
|
||||
/// # use futures::executor::block_on;
|
||||
/// # block_on(async {
|
||||
/// assert!(room.messages(request).await.is_ok());
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn messages(
|
||||
&self,
|
||||
request: impl Into<get_message_events::Request<'_>>,
|
||||
) -> Result<get_message_events::Response> {
|
||||
let request = request.into();
|
||||
self.client.send(request, None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn request_members(&self) -> Result<Option<MembersResponse>> {
|
||||
#[allow(clippy::map_clone)]
|
||||
if let Some(mutex) =
|
||||
self.client.members_request_locks.get(self.inner.room_id()).map(|m| m.clone())
|
||||
{
|
||||
mutex.lock().await;
|
||||
|
||||
Ok(None)
|
||||
} else {
|
||||
let mutex = Arc::new(Mutex::new(()));
|
||||
self.client.members_request_locks.insert(self.inner.room_id().clone(), mutex.clone());
|
||||
|
||||
let _guard = mutex.lock().await;
|
||||
|
||||
let request = get_member_events::Request::new(self.inner.room_id());
|
||||
let response = self.client.send(request, None).await?;
|
||||
|
||||
let response =
|
||||
self.client.base_client.receive_members(self.inner.room_id(), &response).await?;
|
||||
|
||||
self.client.members_request_locks.remove(self.inner.room_id());
|
||||
|
||||
Ok(Some(response))
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_members(&self) -> Result<()> {
|
||||
if !self.are_events_visible() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.are_members_synced() {
|
||||
self.request_members().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn are_events_visible(&self) -> bool {
|
||||
if let RoomType::Invited = self.inner.room_type() {
|
||||
return matches!(
|
||||
self.inner.history_visibility(),
|
||||
HistoryVisibility::WorldReadable | HistoryVisibility::Invited
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Sync the member list with the server.
|
||||
///
|
||||
/// This method will de-duplicate requests if it is called multiple times in
|
||||
/// quick succession, in that case the return value will be `None`.
|
||||
pub async fn sync_members(&self) -> Result<Option<MembersResponse>> {
|
||||
self.request_members().await
|
||||
}
|
||||
|
||||
/// Get active members for this room, includes invited, joined members.
|
||||
///
|
||||
/// *Note*: This method will fetch the members from the homeserver if the
|
||||
/// member list isn't synchronized due to member lazy loading. Because of
|
||||
/// that, it might panic if it isn't run on a tokio thread.
|
||||
///
|
||||
/// Use [active_members_no_sync()](#method.active_members_no_sync) if you
|
||||
/// want a method that doesn't do any requests.
|
||||
pub async fn active_members(&self) -> Result<Vec<RoomMember>> {
|
||||
self.ensure_members().await?;
|
||||
self.active_members_no_sync().await
|
||||
}
|
||||
|
||||
/// Get active members for this room, includes invited, joined members.
|
||||
///
|
||||
/// *Note*: This method will fetch the members from the homeserver if the
|
||||
/// member list isn't synchronized due to member lazy loading. Because of
|
||||
/// that, it might panic if it isn't run on a tokio thread.
|
||||
///
|
||||
/// Use [active_members()](#method.active_members) if you want to ensure to
|
||||
/// always get the full member list.
|
||||
pub async fn active_members_no_sync(&self) -> Result<Vec<RoomMember>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.active_members()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|member| RoomMember::new(self.client.clone(), member))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get all the joined members of this room.
|
||||
///
|
||||
/// *Note*: This method will fetch the members from the homeserver if the
|
||||
/// member list isn't synchronized due to member lazy loading. Because of
|
||||
/// that it might panic if it isn't run on a tokio thread.
|
||||
///
|
||||
/// Use [joined_members_no_sync()](#method.joined_members_no_sync) if you
|
||||
/// want a method that doesn't do any requests.
|
||||
pub async fn joined_members(&self) -> Result<Vec<RoomMember>> {
|
||||
self.ensure_members().await?;
|
||||
self.joined_members_no_sync().await
|
||||
}
|
||||
|
||||
/// Get all the joined members of this room.
|
||||
///
|
||||
/// *Note*: This method will not fetch the members from the homeserver if
|
||||
/// the member list isn't synchronized due to member lazy loading. Thus,
|
||||
/// members could be missing from the list.
|
||||
///
|
||||
/// Use [joined_members()](#method.joined_members) if you want to ensure to
|
||||
/// always get the full member list.
|
||||
pub async fn joined_members_no_sync(&self) -> Result<Vec<RoomMember>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.joined_members()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|member| RoomMember::new(self.client.clone(), member))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get a specific member of this room.
|
||||
///
|
||||
/// *Note*: This method will fetch the members from the homeserver if the
|
||||
/// member list isn't synchronized due to member lazy loading. Because of
|
||||
/// that it might panic if it isn't run on a tokio thread.
|
||||
///
|
||||
/// Use [get_member_no_sync()](#method.get_member_no_sync) if you want a
|
||||
/// method that doesn't do any requests.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user that should be fetched out of the
|
||||
/// store.
|
||||
pub async fn get_member(&self, user_id: &UserId) -> Result<Option<RoomMember>> {
|
||||
self.ensure_members().await?;
|
||||
self.get_member_no_sync(user_id).await
|
||||
}
|
||||
|
||||
/// Get a specific member of this room.
|
||||
///
|
||||
/// *Note*: This method will not fetch the members from the homeserver if
|
||||
/// the member list isn't synchronized due to member lazy loading. Thus,
|
||||
/// members could be missing.
|
||||
///
|
||||
/// Use [get_member()](#method.get_member) if you want to ensure to always
|
||||
/// have the full member list to chose from.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The ID of the user that should be fetched out of the
|
||||
/// store.
|
||||
pub async fn get_member_no_sync(&self, user_id: &UserId) -> Result<Option<RoomMember>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.get_member(user_id)
|
||||
.await?
|
||||
.map(|member| RoomMember::new(self.client.clone(), member)))
|
||||
}
|
||||
|
||||
/// Get all members for this room, includes invited, joined and left
|
||||
/// members.
|
||||
///
|
||||
/// *Note*: This method will fetch the members from the homeserver if the
|
||||
/// member list isn't synchronized due to member lazy loading. Because of
|
||||
/// that it might panic if it isn't run on a tokio thread.
|
||||
///
|
||||
/// Use [members_no_sync()](#method.members_no_sync) if you want a
|
||||
/// method that doesn't do any requests.
|
||||
pub async fn members(&self) -> Result<Vec<RoomMember>> {
|
||||
self.ensure_members().await?;
|
||||
self.members_no_sync().await
|
||||
}
|
||||
|
||||
/// Get all members for this room, includes invited, joined and left
|
||||
/// members.
|
||||
///
|
||||
/// *Note*: This method will not fetch the members from the homeserver if
|
||||
/// the member list isn't synchronized due to member lazy loading. Thus,
|
||||
/// members could be missing.
|
||||
///
|
||||
/// Use [members()](#method.members) if you want to ensure to always get
|
||||
/// the full member list.
|
||||
pub async fn members_no_sync(&self) -> Result<Vec<RoomMember>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.members()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|member| RoomMember::new(self.client.clone(), member))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{room::Common, BaseRoom, Client, Result, RoomType};
|
||||
|
||||
/// A room in the invited state.
|
||||
///
|
||||
/// This struct contains all methods specific to a `Room` with type
|
||||
/// `RoomType::Invited`. Operations may fail once the underlying `Room` changes
|
||||
/// `RoomType`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Invited {
|
||||
pub(crate) inner: Common,
|
||||
}
|
||||
|
||||
impl Invited {
|
||||
/// Create a new `room::Invited` if the underlying `Room` has type
|
||||
/// `RoomType::Invited`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - The client used to make requests.
|
||||
///
|
||||
/// * `room` - The underlying room.
|
||||
pub fn new(client: Client, room: BaseRoom) -> Option<Self> {
|
||||
// TODO: Make this private
|
||||
if room.room_type() == RoomType::Invited {
|
||||
Some(Self { inner: Common::new(client, room) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject the invitation.
|
||||
pub async fn reject_invitation(&self) -> Result<()> {
|
||||
self.inner.leave().await
|
||||
}
|
||||
|
||||
/// Accept the invitation.
|
||||
pub async fn accept_invitation(&self) -> Result<()> {
|
||||
self.inner.join().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Invited {
|
||||
type Target = Common;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
#[cfg(feature = "encryption")]
|
||||
use std::sync::Arc;
|
||||
use std::{io::Read, ops::Deref};
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
use matrix_sdk_base::crypto::AttachmentEncryptor;
|
||||
#[cfg(feature = "encryption")]
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use matrix_sdk_common::{
|
||||
instant::{Duration, Instant},
|
||||
uuid::Uuid,
|
||||
};
|
||||
use mime::{self, Mime};
|
||||
#[cfg(feature = "encryption")]
|
||||
use ruma::events::room::EncryptedFileInit;
|
||||
use ruma::{
|
||||
api::client::r0::{
|
||||
membership::{
|
||||
ban_user,
|
||||
invite_user::{self, InvitationRecipient},
|
||||
kick_user, Invite3pid,
|
||||
},
|
||||
message::send_message_event,
|
||||
read_marker::set_read_marker,
|
||||
receipt::create_receipt,
|
||||
redact::redact_event,
|
||||
state::send_state_event,
|
||||
typing::create_typing_event::{Request as TypingRequest, Typing},
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
room::{
|
||||
message::{
|
||||
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
|
||||
MessageEventContent, MessageType, VideoMessageEventContent,
|
||||
},
|
||||
EncryptedFile,
|
||||
},
|
||||
AnyMessageEventContent, AnyStateEventContent,
|
||||
},
|
||||
identifiers::{EventId, UserId},
|
||||
receipt::ReceiptType,
|
||||
};
|
||||
#[cfg(feature = "encryption")]
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{room::Common, BaseRoom, Client, Result, RoomType};
|
||||
|
||||
const TYPING_NOTICE_TIMEOUT: Duration = Duration::from_secs(4);
|
||||
const TYPING_NOTICE_RESEND_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
/// A room in the joined state.
|
||||
///
|
||||
/// The `JoinedRoom` contains all methods specific to a `Room` with type
|
||||
/// `RoomType::Joined`. Operations may fail once the underlying `Room` changes
|
||||
/// `RoomType`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Joined {
|
||||
pub(crate) inner: Common,
|
||||
}
|
||||
|
||||
impl Deref for Joined {
|
||||
type Target = Common;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Joined {
|
||||
/// Create a new `room::Joined` if the underlying `BaseRoom` has type
|
||||
/// `RoomType::Joined`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - The client used to make requests.
|
||||
///
|
||||
/// * `room` - The underlying room.
|
||||
pub fn new(client: Client, room: BaseRoom) -> Option<Self> {
|
||||
// TODO: Make this private
|
||||
if room.room_type() == RoomType::Joined {
|
||||
Some(Self { inner: Common::new(client, room) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave this room.
|
||||
pub async fn leave(&self) -> Result<()> {
|
||||
self.inner.leave().await
|
||||
}
|
||||
|
||||
/// Ban the user with `UserId` from this room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The user to ban with `UserId`.
|
||||
///
|
||||
/// * `reason` - The reason for banning this user.
|
||||
pub async fn ban_user(&self, user_id: &UserId, reason: Option<&str>) -> Result<()> {
|
||||
let request = assign!(ban_user::Request::new(self.inner.room_id(), user_id), { reason });
|
||||
self.client.send(request, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kick a user out of this room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The `UserId` of the user that should be kicked out of the
|
||||
/// room.
|
||||
///
|
||||
/// * `reason` - Optional reason why the room member is being kicked out.
|
||||
pub async fn kick_user(&self, user_id: &UserId, reason: Option<&str>) -> Result<()> {
|
||||
let request = assign!(kick_user::Request::new(self.inner.room_id(), user_id), { reason });
|
||||
self.client.send(request, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Invite the specified user by `UserId` to this room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The `UserId` of the user to invite to the room.
|
||||
pub async fn invite_user_by_id(&self, user_id: &UserId) -> Result<()> {
|
||||
let recipient = InvitationRecipient::UserId { user_id };
|
||||
|
||||
let request = invite_user::Request::new(self.inner.room_id(), recipient);
|
||||
self.client.send(request, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Invite the specified user by third party id to this room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `invite_id` - A third party id of a user to invite to the room.
|
||||
pub async fn invite_user_by_3pid(&self, invite_id: Invite3pid<'_>) -> Result<()> {
|
||||
let recipient = InvitationRecipient::ThirdPartyId(invite_id);
|
||||
let request = invite_user::Request::new(self.inner.room_id(), recipient);
|
||||
self.client.send(request, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Activate typing notice for this room.
|
||||
///
|
||||
/// The typing notice remains active for 4s. It can be deactivate at any
|
||||
/// point by setting typing to `false`. If this method is called while
|
||||
/// the typing notice is active nothing will happen. This method can be
|
||||
/// called on every key stroke, since it will do nothing while typing is
|
||||
/// active.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `typing` - Whether the user is typing or has stopped typing.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::time::Duration;
|
||||
/// use matrix_sdk::api::r0::typing::create_typing_event::Typing;
|
||||
/// # use matrix_sdk::{
|
||||
/// # Client, SyncSettings,
|
||||
/// # identifiers::room_id,
|
||||
/// # };
|
||||
/// # use futures::executor::block_on;
|
||||
/// # use url::Url;
|
||||
/// # block_on(async {
|
||||
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
|
||||
/// # let mut client = Client::new(homeserver).unwrap();
|
||||
/// # let room_id = room_id!("!test:localhost");
|
||||
/// # let room = client
|
||||
/// # .get_joined_room(&room_id!("!SVkFJHzfwvuaIEawgC:localhost"))
|
||||
/// # .unwrap();
|
||||
///
|
||||
/// room
|
||||
/// .typing_notice(true)
|
||||
/// .await
|
||||
/// .expect("Can't get devices from server");
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn typing_notice(&self, typing: bool) -> Result<()> {
|
||||
// Only send a request to the homeserver if the old timeout has elapsed
|
||||
// or the typing notice changed state within the
|
||||
// TYPING_NOTICE_TIMEOUT
|
||||
let send =
|
||||
if let Some(typing_time) = self.client.typing_notice_times.get(self.inner.room_id()) {
|
||||
if typing_time.elapsed() > TYPING_NOTICE_RESEND_TIMEOUT {
|
||||
// We always reactivate the typing notice if typing is true or
|
||||
// we may need to deactivate it if it's
|
||||
// currently active if typing is false
|
||||
typing || typing_time.elapsed() <= TYPING_NOTICE_TIMEOUT
|
||||
} else {
|
||||
// Only send a request when we need to deactivate typing
|
||||
!typing
|
||||
}
|
||||
} else {
|
||||
// Typing notice is currently deactivated, therefore, send a request
|
||||
// only when it's about to be activated
|
||||
typing
|
||||
};
|
||||
|
||||
if send {
|
||||
let typing = if typing {
|
||||
self.client
|
||||
.typing_notice_times
|
||||
.insert(self.inner.room_id().clone(), Instant::now());
|
||||
Typing::Yes(TYPING_NOTICE_TIMEOUT)
|
||||
} else {
|
||||
self.client.typing_notice_times.remove(self.inner.room_id());
|
||||
Typing::No
|
||||
};
|
||||
|
||||
let request =
|
||||
TypingRequest::new(self.inner.own_user_id(), self.inner.room_id(), typing);
|
||||
self.client.send(request, None).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a request to notify this room that the user has read specific
|
||||
/// event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_id` - The `EventId` specifies the event to set the read receipt
|
||||
/// on.
|
||||
pub async fn read_receipt(&self, event_id: &EventId) -> Result<()> {
|
||||
let request =
|
||||
create_receipt::Request::new(self.inner.room_id(), ReceiptType::Read, event_id);
|
||||
|
||||
self.client.send(request, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a request to notify this room that the user has read up to specific
|
||||
/// event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * fully_read - The `EventId` of the event the user has read to.
|
||||
///
|
||||
/// * read_receipt - An `EventId` to specify the event to set the read
|
||||
/// receipt on.
|
||||
pub async fn read_marker(
|
||||
&self,
|
||||
fully_read: &EventId,
|
||||
read_receipt: Option<&EventId>,
|
||||
) -> Result<()> {
|
||||
let request = assign!(set_read_marker::Request::new(self.inner.room_id(), fully_read), {
|
||||
read_receipt
|
||||
});
|
||||
|
||||
self.client.send(request, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Share a group session for the given room.
|
||||
///
|
||||
/// This will create Olm sessions with all the users/device pairs in the
|
||||
/// room if necessary and share a group session with them.
|
||||
///
|
||||
/// Does nothing if no group session needs to be shared.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
async fn preshare_group_session(&self) -> Result<()> {
|
||||
// TODO expose this publicly so people can pre-share a group session if
|
||||
// e.g. a user starts to type a message for a room.
|
||||
#[allow(clippy::map_clone)]
|
||||
if let Some(mutex) =
|
||||
self.client.group_session_locks.get(self.inner.room_id()).map(|m| m.clone())
|
||||
{
|
||||
// If a group session share request is already going on,
|
||||
// await the release of the lock.
|
||||
mutex.lock().await;
|
||||
} else {
|
||||
// Otherwise create a new lock and share the group
|
||||
// session.
|
||||
let mutex = Arc::new(Mutex::new(()));
|
||||
self.client.group_session_locks.insert(self.inner.room_id().clone(), mutex.clone());
|
||||
|
||||
let _guard = mutex.lock().await;
|
||||
|
||||
{
|
||||
let joined = self.client.store().get_joined_user_ids(self.inner.room_id()).await?;
|
||||
let invited =
|
||||
self.client.store().get_invited_user_ids(self.inner.room_id()).await?;
|
||||
let members = joined.iter().chain(&invited);
|
||||
self.client.claim_one_time_keys(members).await?;
|
||||
};
|
||||
|
||||
let response = self.share_group_session().await;
|
||||
|
||||
self.client.group_session_locks.remove(self.inner.room_id());
|
||||
|
||||
// If one of the responses failed invalidate the group
|
||||
// session as using it would end up in undecryptable
|
||||
// messages.
|
||||
if let Err(r) = response {
|
||||
self.client.base_client.invalidate_group_session(self.inner.room_id()).await?;
|
||||
return Err(r);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Share a group session for a room.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the client isn't logged in.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[instrument]
|
||||
async fn share_group_session(&self) -> Result<()> {
|
||||
let mut requests =
|
||||
self.client.base_client.share_group_session(self.inner.room_id()).await?;
|
||||
|
||||
for request in requests.drain(..) {
|
||||
let response = self.client.send_to_device(&request).await?;
|
||||
|
||||
self.client.base_client.mark_request_as_sent(&request.txn_id, &response).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a room message to this room.
|
||||
///
|
||||
/// Returns the parsed response from the server.
|
||||
///
|
||||
/// If the encryption feature is enabled this method will transparently
|
||||
/// encrypt the room message if this room is encrypted.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `content` - The content of the message event.
|
||||
///
|
||||
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
|
||||
/// held in its unsigned field as `transaction_id`. If not given one is
|
||||
/// created for the message.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use std::sync::{Arc, RwLock};
|
||||
/// # use matrix_sdk::{Client, SyncSettings};
|
||||
/// # use url::Url;
|
||||
/// # use futures::executor::block_on;
|
||||
/// # use matrix_sdk::identifiers::room_id;
|
||||
/// # use std::convert::TryFrom;
|
||||
/// use matrix_sdk::events::{
|
||||
/// AnyMessageEventContent,
|
||||
/// room::message::{MessageEventContent, TextMessageEventContent},
|
||||
/// };
|
||||
/// # block_on(async {
|
||||
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
|
||||
/// # let mut client = Client::new(homeserver).unwrap();
|
||||
/// # let room_id = room_id!("!test:localhost");
|
||||
/// use matrix_sdk_common::uuid::Uuid;
|
||||
///
|
||||
/// let content = AnyMessageEventContent::RoomMessage(
|
||||
/// MessageEventContent::text_plain("Hello world")
|
||||
/// );
|
||||
///
|
||||
/// let txn_id = Uuid::new_v4();
|
||||
/// # let room = client
|
||||
/// # .get_joined_room(&room_id)
|
||||
/// # .unwrap();
|
||||
/// room.send(content, Some(txn_id)).await.unwrap();
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn send(
|
||||
&self,
|
||||
content: impl Into<AnyMessageEventContent>,
|
||||
txn_id: Option<Uuid>,
|
||||
) -> Result<send_message_event::Response> {
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
let content: AnyMessageEventContent = content.into();
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
let content = if self.is_encrypted() {
|
||||
if !self.are_members_synced() {
|
||||
self.request_members().await?;
|
||||
// TODO query keys here?
|
||||
}
|
||||
|
||||
self.preshare_group_session().await?;
|
||||
AnyMessageEventContent::RoomEncrypted(
|
||||
self.client.base_client.encrypt(self.inner.room_id(), content).await?,
|
||||
)
|
||||
} else {
|
||||
content.into()
|
||||
};
|
||||
|
||||
let txn_id = txn_id.unwrap_or_else(Uuid::new_v4).to_string();
|
||||
let request = send_message_event::Request::new(self.inner.room_id(), &txn_id, &content);
|
||||
|
||||
let response = self.client.send(request, None).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Send an attachment to this room.
|
||||
///
|
||||
/// This will upload the given data that the reader produces using the
|
||||
/// [`upload()`](#method.upload) method and post an event to the given room.
|
||||
/// If the room is encrypted and the encryption feature is enabled the
|
||||
/// upload will be encrypted.
|
||||
///
|
||||
/// This is a convenience method that calls the
|
||||
/// [`Client::upload()`](#Client::method.upload) and afterwards the
|
||||
/// [`send()`](#method.send).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `body` - A textual representation of the media that is going to be
|
||||
/// uploaded. Usually the file name.
|
||||
///
|
||||
/// * `content_type` - The type of the media, this will be used as the
|
||||
/// content-type header.
|
||||
///
|
||||
/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the
|
||||
/// media.
|
||||
///
|
||||
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
|
||||
/// held in its unsigned field as `transaction_id`. If not given one is
|
||||
/// created for the message.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::{path::PathBuf, fs::File, io::Read};
|
||||
/// # use matrix_sdk::{Client, identifiers::room_id};
|
||||
/// # use url::Url;
|
||||
/// # use mime;
|
||||
/// # use futures::executor::block_on;
|
||||
/// # block_on(async {
|
||||
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
|
||||
/// # let mut client = Client::new(homeserver).unwrap();
|
||||
/// # let room_id = room_id!("!test:localhost");
|
||||
/// let path = PathBuf::from("/home/example/my-cat.jpg");
|
||||
/// let mut image = File::open(path).unwrap();
|
||||
///
|
||||
/// # let room = client
|
||||
/// # .get_joined_room(&room_id)
|
||||
/// # .unwrap();
|
||||
/// room.send_attachment("My favorite cat", &mime::IMAGE_JPEG, &mut image, None)
|
||||
/// .await
|
||||
/// .expect("Can't upload my cat.");
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn send_attachment<R: Read>(
|
||||
&self,
|
||||
body: &str,
|
||||
content_type: &Mime,
|
||||
mut reader: &mut R,
|
||||
txn_id: Option<Uuid>,
|
||||
) -> Result<send_message_event::Response> {
|
||||
let (response, encrypted_file) = if self.is_encrypted() {
|
||||
#[cfg(feature = "encryption")]
|
||||
let mut reader = AttachmentEncryptor::new(reader);
|
||||
#[cfg(feature = "encryption")]
|
||||
let content_type = mime::APPLICATION_OCTET_STREAM;
|
||||
|
||||
let response = self.client.upload(&content_type, &mut reader).await?;
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
let keys: Option<Box<EncryptedFile>> = {
|
||||
let keys = reader.finish();
|
||||
Some(Box::new(
|
||||
EncryptedFileInit {
|
||||
url: response.content_uri.clone(),
|
||||
key: keys.web_key,
|
||||
iv: keys.iv,
|
||||
hashes: keys.hashes,
|
||||
v: keys.version,
|
||||
}
|
||||
.into(),
|
||||
))
|
||||
};
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
let keys: Option<Box<EncryptedFile>> = None;
|
||||
|
||||
(response, keys)
|
||||
} else {
|
||||
let response = self.client.upload(content_type, &mut reader).await?;
|
||||
(response, None)
|
||||
};
|
||||
|
||||
let url = response.content_uri;
|
||||
|
||||
let content = match content_type.type_() {
|
||||
mime::IMAGE => {
|
||||
// TODO create a thumbnail using the image crate?.
|
||||
MessageType::Image(assign!(
|
||||
ImageMessageEventContent::plain(body.to_owned(), url, None),
|
||||
{ file: encrypted_file }
|
||||
))
|
||||
}
|
||||
mime::AUDIO => MessageType::Audio(assign!(
|
||||
AudioMessageEventContent::plain(body.to_owned(), url, None),
|
||||
{ file: encrypted_file }
|
||||
)),
|
||||
mime::VIDEO => MessageType::Video(assign!(
|
||||
VideoMessageEventContent::plain(body.to_owned(), url, None),
|
||||
{ file: encrypted_file }
|
||||
)),
|
||||
_ => MessageType::File(assign!(
|
||||
FileMessageEventContent::plain(body.to_owned(), url, None),
|
||||
{ file: encrypted_file }
|
||||
)),
|
||||
};
|
||||
|
||||
self.send(AnyMessageEventContent::RoomMessage(MessageEventContent::new(content)), txn_id)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a room state event to the homeserver.
|
||||
///
|
||||
/// Returns the parsed response from the server.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room that should receive the message.
|
||||
///
|
||||
/// * `content` - The content of the state event.
|
||||
///
|
||||
/// * `state_key` - A unique key which defines the overwriting semantics for
|
||||
/// this piece of room state. This value is often a zero-length string.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use matrix_sdk::{
|
||||
/// events::{
|
||||
/// AnyStateEventContent,
|
||||
/// room::member::{MemberEventContent, MembershipState},
|
||||
/// },
|
||||
/// identifiers::mxc_uri,
|
||||
/// assign,
|
||||
/// };
|
||||
/// # futures::executor::block_on(async {
|
||||
/// # let homeserver = url::Url::parse("http://localhost:8080").unwrap();
|
||||
/// # let mut client = matrix_sdk::Client::new(homeserver).unwrap();
|
||||
/// # let room_id = matrix_sdk::identifiers::room_id!("!test:localhost");
|
||||
///
|
||||
/// let avatar_url = mxc_uri!("mxc://example.org/avatar");
|
||||
/// let member_event = assign!(MemberEventContent::new(MembershipState::Join), {
|
||||
/// avatar_url: Some(avatar_url),
|
||||
/// });
|
||||
/// # let room = client
|
||||
/// # .get_joined_room(&room_id)
|
||||
/// # .unwrap();
|
||||
///
|
||||
/// let content = AnyStateEventContent::RoomMember(member_event);
|
||||
/// room.send_state_event(content, "").await.unwrap();
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn send_state_event(
|
||||
&self,
|
||||
content: impl Into<AnyStateEventContent>,
|
||||
state_key: &str,
|
||||
) -> Result<send_state_event::Response> {
|
||||
let content = content.into();
|
||||
let request = send_state_event::Request::new(self.inner.room_id(), state_key, &content);
|
||||
|
||||
self.client.send(request, None).await
|
||||
}
|
||||
|
||||
/// Strips all information out of an event of the room.
|
||||
///
|
||||
/// Returns the [`redact_event::Response`] from the server.
|
||||
///
|
||||
/// This cannot be undone. Users may redact their own events, and any user
|
||||
/// with a power level greater than or equal to the redact power level of
|
||||
/// the room may redact events there.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_id` - The ID of the event to redact
|
||||
///
|
||||
/// * `reason` - The reason for the event being redacted.
|
||||
///
|
||||
/// * `txn_id` - A unique [`Uuid`] that can be attached to this event as
|
||||
/// its transaction ID. If not given one is created for the message.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # futures::executor::block_on(async {
|
||||
/// # let homeserver = url::Url::parse("http://localhost:8080").unwrap();
|
||||
/// # let mut client = matrix_sdk::Client::new(homeserver).unwrap();
|
||||
/// # let room_id = matrix_sdk::identifiers::room_id!("!test:localhost");
|
||||
/// # let room = client
|
||||
/// # .get_joined_room(&room_id)
|
||||
/// # .unwrap();
|
||||
/// let event_id = matrix_sdk::identifiers::event_id!("$xxxxxx:example.org");
|
||||
/// let reason = Some("Indecent material");
|
||||
/// room.redact(&event_id, reason, None).await.unwrap();
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn redact(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
reason: Option<&str>,
|
||||
txn_id: Option<Uuid>,
|
||||
) -> Result<redact_event::Response> {
|
||||
let txn_id = txn_id.unwrap_or_else(Uuid::new_v4).to_string();
|
||||
let request =
|
||||
assign!(redact_event::Request::new(self.inner.room_id(), event_id, &txn_id), {
|
||||
reason
|
||||
});
|
||||
|
||||
self.client.send(request, None).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruma::api::client::r0::membership::forget_room;
|
||||
|
||||
use crate::{room::Common, BaseRoom, Client, Result, RoomType};
|
||||
|
||||
/// A room in the left state.
|
||||
///
|
||||
/// This struct contains all methods specific to a `Room` with type
|
||||
/// `RoomType::Left`. Operations may fail once the underlying `Room` changes
|
||||
/// `RoomType`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Left {
|
||||
pub(crate) inner: Common,
|
||||
}
|
||||
|
||||
impl Left {
|
||||
/// Create a new `room::Left` if the underlying `Room` has type
|
||||
/// `RoomType::Left`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - The client used to make requests.
|
||||
///
|
||||
/// * `room` - The underlying room.
|
||||
pub fn new(client: Client, room: BaseRoom) -> Option<Self> {
|
||||
// TODO: Make this private
|
||||
if room.room_type() == RoomType::Left {
|
||||
Some(Self { inner: Common::new(client, room) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Join this room.
|
||||
pub async fn join(&self) -> Result<()> {
|
||||
self.inner.join().await
|
||||
}
|
||||
|
||||
/// Forget this room.
|
||||
///
|
||||
/// This communicates to the homeserver that it should forget the room.
|
||||
pub async fn forget(&self) -> Result<()> {
|
||||
let request = forget_room::Request::new(self.inner.room_id());
|
||||
let _response = self.client.send(request, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Left {
|
||||
type Target = Common;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::RoomType;
|
||||
|
||||
mod common;
|
||||
mod invited;
|
||||
mod joined;
|
||||
mod left;
|
||||
|
||||
pub use self::{common::Common, invited::Invited, joined::Joined, left::Left};
|
||||
|
||||
/// An enum that abstracts over the different states a room can be in.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Room {
|
||||
/// The room in the `join` state.
|
||||
Joined(Joined),
|
||||
/// The room in the `left` state.
|
||||
Left(Left),
|
||||
/// The room in the `invited` state.
|
||||
Invited(Invited),
|
||||
}
|
||||
|
||||
impl Deref for Room {
|
||||
type Target = Common;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Joined(room) => &*room,
|
||||
Self::Left(room) => &*room,
|
||||
Self::Invited(room) => &*room,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Common> for Room {
|
||||
fn from(room: Common) -> Self {
|
||||
match room.room_type() {
|
||||
RoomType::Joined => Self::Joined(Joined { inner: room }),
|
||||
RoomType::Left => Self::Left(Left { inner: room }),
|
||||
RoomType::Invited => Self::Invited(Invited { inner: room }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Joined> for Room {
|
||||
fn from(room: Joined) -> Self {
|
||||
let room = (*room).clone();
|
||||
match room.room_type() {
|
||||
RoomType::Joined => Self::Joined(Joined { inner: room }),
|
||||
RoomType::Left => Self::Left(Left { inner: room }),
|
||||
RoomType::Invited => Self::Invited(Invited { inner: room }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Left> for Room {
|
||||
fn from(room: Left) -> Self {
|
||||
let room = (*room).clone();
|
||||
match room.room_type() {
|
||||
RoomType::Joined => Self::Joined(Joined { inner: room }),
|
||||
RoomType::Left => Self::Left(Left { inner: room }),
|
||||
RoomType::Invited => Self::Invited(Invited { inner: room }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Invited> for Room {
|
||||
fn from(room: Invited) -> Self {
|
||||
let room = (*room).clone();
|
||||
match room.room_type() {
|
||||
RoomType::Joined => Self::Joined(Joined { inner: room }),
|
||||
RoomType::Left => Self::Left(Left { inner: room }),
|
||||
RoomType::Invited => Self::Invited(Invited { inner: room }),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{
|
||||
media::{MediaFormat, MediaRequest, MediaType},
|
||||
BaseRoomMember, Client, Result,
|
||||
};
|
||||
|
||||
/// The high-level `RoomMember` representation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoomMember {
|
||||
inner: BaseRoomMember,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl Deref for RoomMember {
|
||||
type Target = BaseRoomMember;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub(crate) fn new(client: Client, member: BaseRoomMember) -> Self {
|
||||
Self { inner: member, client }
|
||||
}
|
||||
|
||||
/// Gets the avatar of this member, if set.
|
||||
///
|
||||
/// Returns the avatar.
|
||||
/// If a thumbnail is requested no guarantee on the size of the image is
|
||||
/// given.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `format` - The desired format of the avatar.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use futures::executor::block_on;
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use matrix_sdk::identifiers::room_id;
|
||||
/// # use matrix_sdk::RoomMember;
|
||||
/// # use matrix_sdk::media::MediaFormat;
|
||||
/// # use url::Url;
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # block_on(async {
|
||||
/// # let user = "example";
|
||||
/// let client = Client::new(homeserver).unwrap();
|
||||
/// client.login(user, "password", None, None).await.unwrap();
|
||||
/// let room_id = room_id!("!roomid:example.com");
|
||||
/// let room = client
|
||||
/// .get_joined_room(&room_id)
|
||||
/// .unwrap();
|
||||
/// let members = room.members().await.unwrap();
|
||||
/// let member = members.first().unwrap();
|
||||
/// if let Some(avatar) = member.avatar(MediaFormat::File).await.unwrap() {
|
||||
/// std::fs::write("avatar.png", avatar);
|
||||
/// }
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
|
||||
if let Some(url) = self.avatar_url() {
|
||||
let request = MediaRequest { media_type: MediaType::Uri(url.clone()), format };
|
||||
Ok(Some(self.client.get_media_content(&request, true).await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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.
|
||||
|
||||
//! Interactive verification for E2EE capable users and devices in Matrix.
|
||||
//!
|
||||
//! The SDK supports interactive verification of devices and users, this module
|
||||
//! contains types that model and support different verification flows.
|
||||
//!
|
||||
//! A verification flow usually starts its life as a [VerificationRequest], the
|
||||
//! request can then be accepted, or it needs to be accepted by the other side
|
||||
//! of the verification flow.
|
||||
//!
|
||||
//! Once both sides have agreed to pereform the verification, and the
|
||||
//! [VerificationRequest::is_ready()] method returns true, the verification can
|
||||
//! transition into one of the supported verification flows:
|
||||
//!
|
||||
//! * [SasVerification] - Interactive verification using a short authentication
|
||||
//! string.
|
||||
//! * [QrVerification] - Interactive verification using QR codes.
|
||||
|
||||
mod qrcode;
|
||||
mod requests;
|
||||
mod sas;
|
||||
|
||||
pub use qrcode::QrVerification;
|
||||
pub use requests::VerificationRequest;
|
||||
pub use sas::SasVerification;
|
||||
|
||||
/// An enum over the different verification types the SDK supports.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Verification {
|
||||
/// The `m.sas.v1` verification variant.
|
||||
SasV1(SasVerification),
|
||||
/// The `m.qr_code.*.v1` verification variant.
|
||||
QrV1(QrVerification),
|
||||
}
|
||||
|
||||
impl Verification {
|
||||
/// Try to deconstruct this verification enum into a SAS verification.
|
||||
pub fn sas(self) -> Option<SasVerification> {
|
||||
if let Verification::SasV1(sas) = self {
|
||||
Some(sas)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to deconstruct this verification enum into a QR code verification.
|
||||
pub fn qr(self) -> Option<QrVerification> {
|
||||
if let Verification::QrV1(qr) = self {
|
||||
Some(qr)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Has this verification finished.
|
||||
pub fn is_done(&self) -> bool {
|
||||
match self {
|
||||
Verification::SasV1(s) => s.is_done(),
|
||||
Verification::QrV1(qr) => qr.is_done(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Has the verification been cancelled.
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
match self {
|
||||
Verification::SasV1(s) => s.is_cancelled(),
|
||||
Verification::QrV1(qr) => qr.is_cancelled(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get our own user id.
|
||||
pub fn own_user_id(&self) -> &ruma::UserId {
|
||||
match self {
|
||||
Verification::SasV1(v) => v.own_user_id(),
|
||||
Verification::QrV1(v) => v.own_user_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the user id of the other user participating in this verification
|
||||
/// flow.
|
||||
pub fn other_user_id(&self) -> &ruma::UserId {
|
||||
match self {
|
||||
Verification::SasV1(v) => v.inner.other_user_id(),
|
||||
Verification::QrV1(v) => v.inner.other_user_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this a verification that is veryfying one of our own devices.
|
||||
pub fn is_self_verification(&self) -> bool {
|
||||
match self {
|
||||
Verification::SasV1(v) => v.is_self_verification(),
|
||||
Verification::QrV1(v) => v.is_self_verification(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SasVerification> for Verification {
|
||||
fn from(sas: SasVerification) -> Self {
|
||||
Self::SasV1(sas)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<QrVerification> for Verification {
|
||||
fn from(qr: QrVerification) -> Self {
|
||||
Self::QrV1(qr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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.
|
||||
|
||||
use matrix_sdk_base::crypto::{
|
||||
matrix_qrcode::{qrcode::QrCode, EncodingError},
|
||||
QrVerification as BaseQrVerification,
|
||||
};
|
||||
use ruma::UserId;
|
||||
|
||||
use crate::{Client, Result};
|
||||
|
||||
/// An object controlling QR code style key verification flows.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QrVerification {
|
||||
pub(crate) inner: BaseQrVerification,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl QrVerification {
|
||||
/// Get our own user id.
|
||||
pub fn own_user_id(&self) -> &UserId {
|
||||
self.inner.user_id()
|
||||
}
|
||||
|
||||
/// Is this a verification that is veryfying one of our own devices.
|
||||
pub fn is_self_verification(&self) -> bool {
|
||||
self.inner.is_self_verification()
|
||||
}
|
||||
|
||||
/// Has this verification finished.
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.inner.is_done()
|
||||
}
|
||||
|
||||
/// Has the verification been cancelled.
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.inner.is_cancelled()
|
||||
}
|
||||
|
||||
/// Generate a QR code object that is representing this verification flow.
|
||||
///
|
||||
/// The `QrCode` can then be rendered as an image or as an unicode string.
|
||||
///
|
||||
/// The [`to_bytes()`](#method.to_bytes) method can be used to instead
|
||||
/// output the raw bytes that should be encoded as a QR code.
|
||||
pub fn to_qr_code(&self) -> std::result::Result<QrCode, EncodingError> {
|
||||
self.inner.to_qr_code()
|
||||
}
|
||||
|
||||
/// Generate a the raw bytes that should be encoded as a QR code is
|
||||
/// representing this verification flow.
|
||||
///
|
||||
/// The [`to_qr_code()`](#method.to_qr_code) method can be used to instead
|
||||
/// output a `QrCode` object that can be rendered.
|
||||
pub fn to_bytes(&self) -> std::result::Result<Vec<u8>, EncodingError> {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
/// Confirm that the other side has scanned our QR code.
|
||||
pub async fn confirm(&self) -> Result<()> {
|
||||
if let Some(request) = self.inner.confirm_scanning() {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Abort the verification flow and notify the other side that we did so.
|
||||
pub async fn cancel(&self) -> Result<()> {
|
||||
if let Some(request) = self.inner.cancel() {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// 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.
|
||||
|
||||
use matrix_sdk_base::crypto::VerificationRequest as BaseVerificationRequest;
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
|
||||
use super::{QrVerification, SasVerification};
|
||||
use crate::{Client, Result};
|
||||
|
||||
/// An object controlling the interactive verification flow.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerificationRequest {
|
||||
pub(crate) inner: BaseVerificationRequest,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl VerificationRequest {
|
||||
/// Has this verification finished.
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.inner.is_done()
|
||||
}
|
||||
|
||||
/// Has the verification been cancelled.
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.inner.is_cancelled()
|
||||
}
|
||||
|
||||
/// Get our own user id.
|
||||
pub fn own_user_id(&self) -> &ruma::UserId {
|
||||
self.inner.own_user_id()
|
||||
}
|
||||
|
||||
/// Has the verification request been answered by another device.
|
||||
pub fn is_passive(&self) -> bool {
|
||||
self.inner.is_passive()
|
||||
}
|
||||
|
||||
/// Is the verification request ready to start a verification flow.
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
|
||||
/// Get the user id of the other user participating in this verification
|
||||
/// flow.
|
||||
pub fn other_user_id(&self) -> &ruma::UserId {
|
||||
self.inner.other_user()
|
||||
}
|
||||
|
||||
/// Is this a verification that is veryfying one of our own devices.
|
||||
pub fn is_self_verification(&self) -> bool {
|
||||
self.inner.is_self_verification()
|
||||
}
|
||||
|
||||
/// Get the supported verification methods of the other side.
|
||||
///
|
||||
/// Will be present only if the other side requested the verification or if
|
||||
/// we're in the ready state.
|
||||
pub fn their_supported_methods(&self) -> Option<Vec<VerificationMethod>> {
|
||||
self.inner.their_supported_methods()
|
||||
}
|
||||
|
||||
/// Accept the verification request.
|
||||
///
|
||||
/// This method will accept the request and signal that it supports the
|
||||
/// `m.sas.v1`, the `m.qr_code.show.v1`, and `m.reciprocate.v1` method.
|
||||
///
|
||||
/// If QR code scanning should be supported or QR code showing shouldn't be
|
||||
/// supported the [`accept_with_methods()`] method should be used instead.
|
||||
///
|
||||
/// [`accept_with_methods()`]: #method.accept_with_methods
|
||||
pub async fn accept(&self) -> Result<()> {
|
||||
if let Some(request) = self.inner.accept() {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accept the verification request signaling that our client supports the
|
||||
/// given verification methods.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `methods` - The methods that we should advertise as supported by us.
|
||||
pub async fn accept_with_methods(&self, methods: Vec<VerificationMethod>) -> Result<()> {
|
||||
if let Some(request) = self.inner.accept_with_methods(methods) {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a QR code
|
||||
pub async fn generate_qr_code(&self) -> Result<Option<QrVerification>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.generate_qr_code()
|
||||
.await?
|
||||
.map(|qr| QrVerification { inner: qr, client: self.client.clone() }))
|
||||
}
|
||||
|
||||
/// Transition from this verification request into a SAS verification flow.
|
||||
pub async fn start_sas(&self) -> Result<Option<SasVerification>> {
|
||||
if let Some((sas, request)) = self.inner.start_sas().await? {
|
||||
self.client.send_verification_request(request).await?;
|
||||
|
||||
Ok(Some(SasVerification { inner: sas, client: self.client.clone() }))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the verification request
|
||||
pub async fn cancel(&self) -> Result<()> {
|
||||
if let Some(request) = self.inner.cancel() {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// 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.
|
||||
|
||||
use matrix_sdk_base::crypto::{AcceptSettings, ReadOnlyDevice, Sas as BaseSas};
|
||||
use ruma::UserId;
|
||||
|
||||
use crate::{error::Result, Client};
|
||||
|
||||
/// An object controlling the interactive verification flow.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SasVerification {
|
||||
pub(crate) inner: BaseSas,
|
||||
pub(crate) client: Client,
|
||||
}
|
||||
|
||||
impl SasVerification {
|
||||
/// Accept the interactive verification flow.
|
||||
pub async fn accept(&self) -> Result<()> {
|
||||
self.accept_with_settings(Default::default()).await
|
||||
}
|
||||
|
||||
/// Accept the interactive verification flow with specific settings.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `settings` - specific customizations to the verification flow.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::Client;
|
||||
/// # use futures::executor::block_on;
|
||||
/// # use url::Url;
|
||||
/// # use ruma::identifiers::user_id;
|
||||
/// use matrix_sdk::verification::SasVerification;
|
||||
/// use matrix_sdk_base::crypto::AcceptSettings;
|
||||
/// use matrix_sdk::events::key::verification::ShortAuthenticationString;
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # let client = Client::new(homeserver).unwrap();
|
||||
/// # let flow_id = "someID";
|
||||
/// # let user_id = user_id!("@alice:example");
|
||||
/// # block_on(async {
|
||||
/// let sas = client
|
||||
/// .get_verification(&user_id, flow_id)
|
||||
/// .await
|
||||
/// .unwrap()
|
||||
/// .sas()
|
||||
/// .unwrap();
|
||||
///
|
||||
/// let only_decimal = AcceptSettings::with_allowed_methods(
|
||||
/// vec![ShortAuthenticationString::Decimal]
|
||||
/// );
|
||||
/// sas.accept_with_settings(only_decimal).await.unwrap();
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn accept_with_settings(&self, settings: AcceptSettings) -> Result<()> {
|
||||
if let Some(request) = self.inner.accept_with_settings(settings) {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Confirm that the short auth strings match on both sides.
|
||||
pub async fn confirm(&self) -> Result<()> {
|
||||
let (request, signature) = self.inner.confirm().await?;
|
||||
|
||||
if let Some(request) = request {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
|
||||
if let Some(s) = signature {
|
||||
self.client.send(s, None).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel the interactive verification flow.
|
||||
pub async fn cancel(&self) -> Result<()> {
|
||||
if let Some(request) = self.inner.cancel() {
|
||||
self.client.send_verification_request(request).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the emoji version of the short auth string.
|
||||
pub fn emoji(&self) -> Option<[(&'static str, &'static str); 7]> {
|
||||
self.inner.emoji()
|
||||
}
|
||||
|
||||
/// Get the decimal version of the short auth string.
|
||||
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
|
||||
self.inner.decimals()
|
||||
}
|
||||
|
||||
/// Does this verification flow support emoji for the short authentication
|
||||
/// string.
|
||||
pub fn supports_emoji(&self) -> bool {
|
||||
self.inner.supports_emoji()
|
||||
}
|
||||
|
||||
/// Is the verification process done.
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.inner.is_done()
|
||||
}
|
||||
|
||||
/// Are we in a state where we can show the short auth string.
|
||||
pub fn can_be_presented(&self) -> bool {
|
||||
self.inner.can_be_presented()
|
||||
}
|
||||
|
||||
/// Is the verification process canceled.
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.inner.is_cancelled()
|
||||
}
|
||||
|
||||
/// Get the other users device that we're verifying.
|
||||
pub fn other_device(&self) -> &ReadOnlyDevice {
|
||||
self.inner.other_device()
|
||||
}
|
||||
|
||||
/// Did this verification flow start from a verification request.
|
||||
pub fn started_from_request(&self) -> bool {
|
||||
self.inner.started_from_request()
|
||||
}
|
||||
|
||||
/// Is this a verification that is veryfying one of our own devices.
|
||||
pub fn is_self_verification(&self) -> bool {
|
||||
self.inner.is_self_verification()
|
||||
}
|
||||
|
||||
/// Get our own user id.
|
||||
pub fn own_user_id(&self) -> &UserId {
|
||||
self.inner.user_id()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
authors = ["Johannes Becker <j.becker@famedly.com>"]
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio", "appservice"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-appservice"
|
||||
version = "0.1.0"
|
||||
|
||||
[features]
|
||||
default = ["warp"]
|
||||
actix = ["actix-rt", "actix-web"]
|
||||
|
||||
docs = ["actix", "warp"]
|
||||
|
||||
[dependencies]
|
||||
actix-rt = { version = "2", optional = true }
|
||||
actix-web = { version = "4.0.0-beta.6", optional = true }
|
||||
dashmap = "4"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
http = "0.2"
|
||||
regex = "1"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.8"
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1"
|
||||
url = "2"
|
||||
warp = { git = "https://github.com/seanmonstar/warp.git", rev = "629405", optional = true, default-features = false }
|
||||
|
||||
matrix-sdk = { version = "0.3", path = "../matrix_sdk", default-features = false, features = ["appservice", "native-tls"] }
|
||||
|
||||
[dependencies.ruma]
|
||||
version = "0.2.0"
|
||||
features = ["client-api-c", "appservice-api-s", "unstable-pre-spec"]
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.8"
|
||||
mockito = "0.30"
|
||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = "0.2"
|
||||
|
||||
matrix-sdk-test = { version = "0.3", path = "../matrix_sdk_test", features = ["appservice"] }
|
||||
|
||||
[[example]]
|
||||
name = "appservice_autojoin"
|
||||
required-features = ["warp"]
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::{convert::TryFrom, env};
|
||||
|
||||
use matrix_sdk_appservice::{
|
||||
matrix_sdk::{
|
||||
async_trait,
|
||||
events::{
|
||||
room::member::{MemberEventContent, MembershipState},
|
||||
SyncStateEvent,
|
||||
},
|
||||
identifiers::UserId,
|
||||
room::Room,
|
||||
EventHandler,
|
||||
},
|
||||
AppService, AppServiceRegistration,
|
||||
};
|
||||
use tracing::{error, trace};
|
||||
|
||||
struct AppServiceEventHandler {
|
||||
appservice: AppService,
|
||||
}
|
||||
|
||||
impl AppServiceEventHandler {
|
||||
pub fn new(appservice: AppService) -> Self {
|
||||
Self { appservice }
|
||||
}
|
||||
|
||||
pub async fn handle_room_member(
|
||||
&self,
|
||||
room: Room,
|
||||
event: &SyncStateEvent<MemberEventContent>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !self.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::try_from(event.state_key.clone())?;
|
||||
|
||||
let appservice = self.appservice.clone();
|
||||
appservice.register_virtual_user(user_id.localpart()).await?;
|
||||
|
||||
let client = appservice.virtual_user_client(user_id.localpart()).await?;
|
||||
|
||||
client.join_room_by_id(room.room_id()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for AppServiceEventHandler {
|
||||
async fn on_room_member(&self, room: Room, event: &SyncStateEvent<MemberEventContent>) {
|
||||
match self.handle_room_member(room, event).await {
|
||||
Ok(_) => (),
|
||||
Err(error) => error!("{:?}", 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 mut appservice = AppService::new(homeserver_url, server_name, registration).await?;
|
||||
appservice.set_event_handler(Box::new(AppServiceEventHandler::new(appservice.clone()))).await?;
|
||||
|
||||
let (host, port) = appservice.registration().get_host_and_port()?;
|
||||
appservice.run(host, port).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("missing access token")]
|
||||
MissingAccessToken,
|
||||
|
||||
#[error("missing host on registration url")]
|
||||
MissingRegistrationHost,
|
||||
|
||||
#[error("http request builder error")]
|
||||
UnknownHttpRequestBuilder,
|
||||
|
||||
#[error("no port found")]
|
||||
MissingRegistrationPort,
|
||||
|
||||
#[error("no client for localpart found")]
|
||||
NoClientForLocalpart,
|
||||
|
||||
#[error("could not convert host:port to socket addr")]
|
||||
HostPortToSocketAddrs,
|
||||
|
||||
#[error(transparent)]
|
||||
HttpRequest(#[from] ruma::api::error::FromHttpRequestError),
|
||||
|
||||
#[error(transparent)]
|
||||
Identifier(#[from] ruma::identifiers::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Http(#[from] http::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Url(#[from] url::ParseError),
|
||||
|
||||
#[error(transparent)]
|
||||
Serde(#[from] serde::de::value::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidUri(#[from] http::uri::InvalidUri),
|
||||
|
||||
#[error(transparent)]
|
||||
Matrix(#[from] matrix_sdk::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Regex(#[from] regex::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
SerdeYaml(#[from] serde_yaml::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
#[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 {}
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
// 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.
|
||||
//!
|
||||
//! # Quickstart
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # async {
|
||||
//! #
|
||||
//! # use matrix_sdk::{async_trait, EventHandler};
|
||||
//! #
|
||||
//! # struct MyEventHandler;
|
||||
//! #
|
||||
//! # #[async_trait]
|
||||
//! # impl EventHandler for MyEventHandler {}
|
||||
//! #
|
||||
//! use matrix_sdk_appservice::{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.set_event_handler(Box::new(MyEventHandler)).await?;
|
||||
//!
|
||||
//! 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/master/matrix_sdk_appservice/examples
|
||||
|
||||
#[cfg(not(any(feature = "actix", feature = "warp")))]
|
||||
compile_error!("one webserver feature must be enabled. available ones: `actix`, `warp`");
|
||||
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fs::File,
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
pub use error::Error;
|
||||
use http::{uri::PathAndQuery, Uri};
|
||||
pub use matrix_sdk;
|
||||
use matrix_sdk::{reqwest::Url, Bytes, Client, ClientConfig, EventHandler, HttpError, Session};
|
||||
use regex::Regex;
|
||||
#[doc(inline)]
|
||||
pub use ruma::api::{appservice as api, appservice::Registration};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
r0::{account::register, uiaa::UiaaResponse},
|
||||
},
|
||||
error::{FromHttpResponseError, ServerError},
|
||||
},
|
||||
assign, identifiers, DeviceId, ServerNameBox, UserId,
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
mod error;
|
||||
mod webserver;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type Host = String;
|
||||
pub type Port = u16;
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
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: ServerNameBox,
|
||||
registration: Arc<AppServiceRegistration>,
|
||||
clients: Arc<DashMap<Localpart, Client>>,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
/// Create new AppService
|
||||
///
|
||||
/// Also creates and caches a [`Client`] for the [`MainUser`].
|
||||
/// The default [`ClientConfig`] is used, if you want to customize it
|
||||
/// use [`Self::new_with_config()`] 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<ServerNameBox, Error = identifiers::Error>,
|
||||
registration: AppServiceRegistration,
|
||||
) -> Result<Self> {
|
||||
let appservice = Self::new_with_config(
|
||||
homeserver_url,
|
||||
server_name,
|
||||
registration,
|
||||
ClientConfig::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(appservice)
|
||||
}
|
||||
|
||||
/// Same as [`Self::new()`] but lets you provide a [`ClientConfig`] for the
|
||||
/// [`Client`]
|
||||
pub async fn new_with_config(
|
||||
homeserver_url: impl TryInto<Url, Error = url::ParseError>,
|
||||
server_name: impl TryInto<ServerNameBox, Error = identifiers::Error>,
|
||||
registration: AppServiceRegistration,
|
||||
client_config: ClientConfig,
|
||||
) -> Result<Self> {
|
||||
let homeserver_url = homeserver_url.try_into()?;
|
||||
let server_name = server_name.try_into()?;
|
||||
let registration = Arc::new(registration);
|
||||
let clients = Arc::new(DashMap::new());
|
||||
let sender_localpart = registration.sender_localpart.clone();
|
||||
|
||||
let appservice = AppService { homeserver_url, server_name, registration, clients };
|
||||
|
||||
// we create and cache the [`MainUser`] by default
|
||||
appservice.create_and_cache_client(&sender_localpart, client_config).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 [`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> {
|
||||
let client =
|
||||
self.virtual_user_client_with_config(localpart, ClientConfig::default()).await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Same as [`Self::virtual_user_client()`] but with the ability to pass in
|
||||
/// a [`ClientConfig`]
|
||||
///
|
||||
/// Since this method is a singleton follow-up calls with different
|
||||
/// [`ClientConfig`]s will be ignored.
|
||||
pub async fn virtual_user_client_with_config(
|
||||
&self,
|
||||
localpart: impl AsRef<str>,
|
||||
config: ClientConfig,
|
||||
) -> Result<Client> {
|
||||
// TODO: check if localpart is covered by namespace?
|
||||
let localpart = localpart.as_ref();
|
||||
|
||||
let client = if let Some(client) = self.clients.get(localpart) {
|
||||
client.clone()
|
||||
} else {
|
||||
self.create_and_cache_client(localpart, config).await?
|
||||
};
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn create_and_cache_client(
|
||||
&self,
|
||||
localpart: &str,
|
||||
config: ClientConfig,
|
||||
) -> Result<Client> {
|
||||
let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?;
|
||||
|
||||
// The `as_token` in the `Session` maps to the [`MainUser`]
|
||||
// (`sender_localpart`) by default, so we don't need to assert identity
|
||||
// in that case
|
||||
let config = if localpart != self.registration.sender_localpart {
|
||||
let request_config = config.get_request_config().assert_identity();
|
||||
config.request_config(request_config)
|
||||
} else {
|
||||
config
|
||||
};
|
||||
|
||||
let client =
|
||||
Client::new_with_config(self.homeserver_url.clone(), config.appservice_mode())?;
|
||||
|
||||
let session = Session {
|
||||
access_token: self.registration.as_token.clone(),
|
||||
user_id: user_id.clone(),
|
||||
// TODO: expose & proper E2EE
|
||||
device_id: DeviceId::new(),
|
||||
};
|
||||
|
||||
client.restore_login(session).await?;
|
||||
self.clients.insert(localpart.to_owned(), client.clone());
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Get cached [`Client`]
|
||||
///
|
||||
/// Will return the client for the given `localpart` if previously
|
||||
/// constructed with [`Self::virtual_user_client()`] or
|
||||
/// [`Self::virtual_user_client_with_config()`].
|
||||
///
|
||||
/// 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::set_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 set_event_handler(&mut self, handler: Box<dyn EventHandler>) -> Result<()> {
|
||||
let client = self.get_cached_client(None)?;
|
||||
|
||||
client.set_event_handler(handler).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a virtual user by sending a [`register::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.
|
||||
pub async fn register_virtual_user(&self, localpart: impl AsRef<str>) -> Result<()> {
|
||||
let request = assign!(register::Request::new(), {
|
||||
username: Some(localpart.as_ref()),
|
||||
login_type: Some(®ister::LoginType::ApplicationService),
|
||||
});
|
||||
|
||||
let client = self.get_cached_client(None)?;
|
||||
match client.register(request).await {
|
||||
Ok(_) => (),
|
||||
Err(error) => match error {
|
||||
matrix_sdk::Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(
|
||||
ServerError::Known(UiaaResponse::MatrixError(ref matrix_error)),
|
||||
))) => {
|
||||
match matrix_error.kind {
|
||||
ErrorKind::UserInUse => {
|
||||
// TODO: persist the fact that we registered that user
|
||||
warn!("{}", matrix_error.message);
|
||||
}
|
||||
_ => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
_ => return Err(error.into()),
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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>) -> Result<bool> {
|
||||
for user in &self.registration.namespaces.users {
|
||||
// TODO: precompile on AppService construction
|
||||
let re = Regex::new(&user.regex)?;
|
||||
if re.is_match(user_id.as_ref()) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Returns a closure to be used with [`actix_web::App::configure()`]
|
||||
///
|
||||
/// 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
|
||||
#[cfg(feature = "actix")]
|
||||
#[cfg_attr(docs, doc(cfg(feature = "actix")))]
|
||||
pub fn actix_configure(&self) -> impl FnOnce(&mut actix_web::web::ServiceConfig) {
|
||||
let appservice = self.clone();
|
||||
|
||||
move |config| {
|
||||
config.data(appservice);
|
||||
webserver::actix::configure(config);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
#[cfg(feature = "warp")]
|
||||
#[cfg_attr(docs, doc(cfg(feature = "warp")))]
|
||||
pub fn warp_filter(&self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
|
||||
webserver::warp::warp_filter(self.clone())
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
{
|
||||
webserver::actix::run_server(self.clone(), host, port).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
{
|
||||
webserver::warp::run_server(self.clone(), host, port).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "actix", feature = "warp",)))]
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms [legacy routes] to the correct route so ruma can parse them
|
||||
/// properly
|
||||
///
|
||||
/// [legacy routes]: https://matrix.org/docs/spec/application_service/r0.1.2#legacy-routes
|
||||
pub(crate) fn transform_legacy_route(
|
||||
mut request: http::Request<Bytes>,
|
||||
) -> Result<http::Request<Bytes>> {
|
||||
let uri = request.uri().to_owned();
|
||||
|
||||
if !uri.path().starts_with("/_matrix/app/v1") {
|
||||
// rename legacy routes
|
||||
let mut parts = uri.into_parts();
|
||||
let path_and_query = match parts.path_and_query {
|
||||
Some(path_and_query) => format!("/_matrix/app/v1{}", path_and_query),
|
||||
None => "/_matrix/app/v1".to_owned(),
|
||||
};
|
||||
parts.path_and_query =
|
||||
Some(PathAndQuery::try_from(path_and_query).map_err(http::Error::from)?);
|
||||
let uri = parts.try_into().map_err(http::Error::from)?;
|
||||
|
||||
*request.uri_mut() = uri;
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// 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.
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
pub use actix_web::Scope;
|
||||
use actix_web::{
|
||||
dev::Payload,
|
||||
error::PayloadError,
|
||||
get, put,
|
||||
web::{self, BytesMut, Data},
|
||||
App, FromRequest, HttpRequest, HttpResponse, HttpServer,
|
||||
};
|
||||
use futures::Future;
|
||||
use futures_util::TryStreamExt;
|
||||
use ruma::api::appservice as api;
|
||||
|
||||
use crate::{error::Error, AppService};
|
||||
|
||||
pub async fn run_server(
|
||||
appservice: AppService,
|
||||
host: impl Into<String>,
|
||||
port: impl Into<u16>,
|
||||
) -> Result<(), Error> {
|
||||
HttpServer::new(move || App::new().configure(appservice.actix_configure()))
|
||||
.bind((host.into(), port.into()))?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut actix_web::web::ServiceConfig) {
|
||||
// also handles legacy routes
|
||||
config.service(push_transactions).service(query_user_id).service(query_room_alias).service(
|
||||
web::scope("/_matrix/app/v1")
|
||||
.service(push_transactions)
|
||||
.service(query_user_id)
|
||||
.service(query_room_alias),
|
||||
);
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[put("/transactions/{txn_id}")]
|
||||
async fn push_transactions(
|
||||
request: IncomingRequest<api::event::push_events::v1::IncomingRequest>,
|
||||
appservice: Data<AppService>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if !appservice.compare_hs_token(request.access_token) {
|
||||
return Ok(HttpResponse::Unauthorized().finish());
|
||||
}
|
||||
|
||||
appservice.get_cached_client(None)?.receive_transaction(request.incoming).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json("{}"))
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[get("/users/{user_id}")]
|
||||
async fn query_user_id(
|
||||
request: IncomingRequest<api::query::query_user_id::v1::IncomingRequest>,
|
||||
appservice: Data<AppService>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if !appservice.compare_hs_token(request.access_token) {
|
||||
return Ok(HttpResponse::Unauthorized().finish());
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json("{}"))
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[get("/rooms/{room_alias}")]
|
||||
async fn query_room_alias(
|
||||
request: IncomingRequest<api::query::query_room_alias::v1::IncomingRequest>,
|
||||
appservice: Data<AppService>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
if !appservice.compare_hs_token(request.access_token) {
|
||||
return Ok(HttpResponse::Unauthorized().finish());
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json("{}"))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IncomingRequest<T> {
|
||||
access_token: String,
|
||||
incoming: T,
|
||||
}
|
||||
|
||||
impl<T: ruma::api::IncomingRequest> FromRequest for IncomingRequest<T> {
|
||||
type Error = Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||
type Config = ();
|
||||
|
||||
fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
let request = request.to_owned();
|
||||
let payload = payload.take();
|
||||
|
||||
Box::pin(async move {
|
||||
let mut builder =
|
||||
http::request::Builder::new().method(request.method()).uri(request.uri());
|
||||
|
||||
let headers = builder.headers_mut().ok_or(Error::UnknownHttpRequestBuilder)?;
|
||||
for (key, value) in request.headers().iter() {
|
||||
headers.append(key, value.to_owned());
|
||||
}
|
||||
|
||||
let bytes = payload
|
||||
.try_fold(BytesMut::new(), |mut body, chunk| async move {
|
||||
body.extend_from_slice(&chunk);
|
||||
Ok::<_, PayloadError>(body)
|
||||
})
|
||||
.await?
|
||||
.into();
|
||||
|
||||
let access_token = match request.uri().query() {
|
||||
Some(query) => {
|
||||
let query: Vec<(String, String)> = ruma::serde::urlencoded::from_str(query)?;
|
||||
query.into_iter().find(|(key, _)| key == "access_token").map(|(_, value)| value)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let access_token = match access_token {
|
||||
Some(access_token) => access_token,
|
||||
None => return Err(Error::MissingAccessToken),
|
||||
};
|
||||
|
||||
let request = builder.body(bytes)?;
|
||||
let request = crate::transform_legacy_route(request)?;
|
||||
|
||||
Ok(IncomingRequest {
|
||||
access_token,
|
||||
incoming: ruma::api::IncomingRequest::try_from_http_request(request)?,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#[cfg(feature = "actix")]
|
||||
pub mod actix;
|
||||
#[cfg(feature = "warp")]
|
||||
pub mod warp;
|
||||
@@ -0,0 +1,210 @@
|
||||
// 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.
|
||||
|
||||
use std::{net::ToSocketAddrs, result::Result as StdResult};
|
||||
|
||||
use futures::TryFutureExt;
|
||||
use matrix_sdk::Bytes;
|
||||
use serde::Serialize;
|
||||
use warp::{filters::BoxedFilter, path::FullPath, Filter, Rejection, Reply};
|
||||
|
||||
use crate::{AppService, Error, Result};
|
||||
|
||||
pub async fn run_server(
|
||||
appservice: AppService,
|
||||
host: impl Into<String>,
|
||||
port: impl Into<u16>,
|
||||
) -> Result<()> {
|
||||
let routes = warp_filter(appservice);
|
||||
|
||||
let mut addr = format!("{}:{}", host.into(), port.into()).to_socket_addrs()?;
|
||||
if let Some(addr) = addr.next() {
|
||||
warp::serve(routes).run(addr).await;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::HostPortToSocketAddrs)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warp_filter(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
|
||||
// TODO: try to use a struct instead of needlessly cloning appservice multiple
|
||||
// times on every request
|
||||
warp::any()
|
||||
.and(filters::transactions(appservice.clone()))
|
||||
.or(filters::users(appservice.clone()))
|
||||
.or(filters::rooms(appservice))
|
||||
.recover(handle_rejection)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
mod filters {
|
||||
use super::*;
|
||||
|
||||
pub fn users(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
|
||||
warp::get()
|
||||
.and(
|
||||
warp::path!("_matrix" / "app" / "v1" / "users" / String)
|
||||
// legacy route
|
||||
.or(warp::path!("users" / String))
|
||||
.unify(),
|
||||
)
|
||||
.and(warp::path::end())
|
||||
.and(common(appservice))
|
||||
.and_then(handlers::user)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn rooms(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
|
||||
warp::get()
|
||||
.and(
|
||||
warp::path!("_matrix" / "app" / "v1" / "rooms" / String)
|
||||
// legacy route
|
||||
.or(warp::path!("rooms" / String))
|
||||
.unify(),
|
||||
)
|
||||
.and(warp::path::end())
|
||||
.and(common(appservice))
|
||||
.and_then(handlers::room)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn transactions(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
|
||||
warp::put()
|
||||
.and(
|
||||
warp::path!("_matrix" / "app" / "v1" / "transactions" / String)
|
||||
// legacy route
|
||||
.or(warp::path!("transactions" / String))
|
||||
.unify(),
|
||||
)
|
||||
.and(warp::path::end())
|
||||
.and(common(appservice))
|
||||
.and_then(handlers::transaction)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn common(appservice: AppService) -> BoxedFilter<(AppService, http::Request<Bytes>)> {
|
||||
warp::any()
|
||||
.and(filters::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)?;
|
||||
Ok::<http::Request<Bytes>, Rejection>(request)
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn valid_access_token(token: String) -> BoxedFilter<()> {
|
||||
warp::any()
|
||||
.map(move || token.clone())
|
||||
.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)?;
|
||||
|
||||
if query.into_iter().any(|(key, value)| key == "access_token" && value == token) {
|
||||
Ok::<(), Rejection>(())
|
||||
} else {
|
||||
Err(warp::reject::custom(Unauthorized))
|
||||
}
|
||||
})
|
||||
.untuple_one()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn http_request() -> impl Filter<Extract = (http::Request<Bytes>,), Error = Rejection> + Copy
|
||||
{
|
||||
// TODO: extract `hyper::Request` instead
|
||||
// blocked by https://github.com/seanmonstar/warp/issues/139
|
||||
warp::any()
|
||||
.and(warp::method())
|
||||
.and(warp::filters::path::full())
|
||||
.and(warp::filters::query::raw())
|
||||
.and(warp::header::headers_cloned())
|
||||
.and(warp::body::bytes())
|
||||
.and_then(|method, path: FullPath, query, headers, bytes| async move {
|
||||
let uri = http::uri::Builder::new()
|
||||
.path_and_query(format!("{}?{}", path.as_str(), query))
|
||||
.build()
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let mut request = http::Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.body(bytes)
|
||||
.map_err(Error::from)?;
|
||||
|
||||
*request.headers_mut() = headers;
|
||||
|
||||
Ok::<http::Request<Bytes>, Rejection>(request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mod handlers {
|
||||
use super::*;
|
||||
|
||||
pub async fn user(
|
||||
_user_id: String,
|
||||
_appservice: AppService,
|
||||
_request: http::Request<Bytes>,
|
||||
) -> StdResult<impl warp::Reply, Rejection> {
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
}
|
||||
|
||||
pub async fn room(
|
||||
_room_id: String,
|
||||
_appservice: AppService,
|
||||
_request: http::Request<Bytes>,
|
||||
) -> StdResult<impl warp::Reply, Rejection> {
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
}
|
||||
|
||||
pub async fn transaction(
|
||||
_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?;
|
||||
|
||||
Ok(warp::reply::json(&String::from("{}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Unauthorized;
|
||||
|
||||
impl warp::reject::Reject for Unauthorized {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorMessage {
|
||||
code: u16,
|
||||
message: String,
|
||||
}
|
||||
|
||||
pub async fn handle_rejection(err: Rejection) -> std::result::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";
|
||||
|
||||
let json =
|
||||
warp::reply::json(&ErrorMessage { code: code.as_u16(), message: message.into() });
|
||||
Ok(warp::reply::with_status(json, code))
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
id: appservice
|
||||
url: http://localhost:9009
|
||||
as_token: as_token
|
||||
hs_token: hs_token
|
||||
sender_localpart: _appservice
|
||||
namespaces:
|
||||
aliases: []
|
||||
rooms: []
|
||||
users:
|
||||
- exclusive: true
|
||||
regex: '@_appservice_.*'
|
||||
rate_limited: false
|
||||
protocols: []
|
||||
@@ -0,0 +1,382 @@
|
||||
use std::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,
|
||||
};
|
||||
use matrix_sdk_appservice::*;
|
||||
use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson};
|
||||
use serde_json::json;
|
||||
#[cfg(feature = "warp")]
|
||||
use warp::{Filter, Reply};
|
||||
|
||||
fn registration_string() -> String {
|
||||
include_str!("../tests/registration.yaml").to_owned()
|
||||
}
|
||||
|
||||
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",
|
||||
// );
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let registration = match registration {
|
||||
Some(registration) => registration.into(),
|
||||
None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(),
|
||||
};
|
||||
|
||||
let homeserver_url = mockito::server_url();
|
||||
let server_name = "localhost";
|
||||
|
||||
let client_config =
|
||||
ClientConfig::default().request_config(RequestConfig::default().disable_retry());
|
||||
|
||||
Ok(AppService::new_with_config(
|
||||
homeserver_url.as_ref(),
|
||||
server_name,
|
||||
registration,
|
||||
client_config,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_register_virtual_user() -> Result<()> {
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
let localpart = "someone";
|
||||
let _mock = mockito::mock("POST", "/_matrix/client/r0/register")
|
||||
.match_query(mockito::Matcher::Missing)
|
||||
.match_header(
|
||||
"authorization",
|
||||
mockito::Matcher::Exact(format!("Bearer {}", appservice.registration().as_token)),
|
||||
)
|
||||
.match_body(mockito::Matcher::Json(json!({
|
||||
"username": localpart.to_owned(),
|
||||
"type": "m.login.application_service"
|
||||
})))
|
||||
.with_body(format!(
|
||||
r#"{{
|
||||
"access_token": "abc123",
|
||||
"device_id": "GHTYAJCE",
|
||||
"user_id": "@{localpart}:localhost"
|
||||
}}"#,
|
||||
localpart = localpart
|
||||
))
|
||||
.create();
|
||||
|
||||
appservice.register_virtual_user(localpart).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_put_transaction() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(EventsJson::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.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(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_user() -> Result<()> {
|
||||
let appservice = appservice(None).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)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.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(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_get_room() -> Result<()> {
|
||||
let appservice = appservice(None).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)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.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(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_invalid_access_token() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
let transaction =
|
||||
transaction_builder.add_room_event(EventsJson::Member).build_json_transaction();
|
||||
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.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(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_no_access_token() -> Result<()> {
|
||||
let uri = "/_matrix/app/v1/transactions/1";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(EventsJson::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
let appservice = appservice(None).await?;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
{
|
||||
let status = warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.status();
|
||||
|
||||
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?;
|
||||
|
||||
#[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?;
|
||||
|
||||
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
|
||||
|
||||
let mut transaction_builder = TransactionBuilder::new();
|
||||
transaction_builder.add_room_event(EventsJson::Member);
|
||||
let transaction = transaction_builder.build_json_transaction();
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
warp::test::request()
|
||||
.method("PUT")
|
||||
.path(uri)
|
||||
.json(&transaction)
|
||||
.filter(&appservice.warp_filter())
|
||||
.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();
|
||||
assert!(on_room_member_called);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
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())
|
||||
.or(warp::get().and(warp::path("unrelated").map(warp::reply)));
|
||||
|
||||
let response = warp::test::request()
|
||||
.method("GET")
|
||||
.path("/unrelated")
|
||||
.filter(&consumer_filter)
|
||||
.await?
|
||||
.into_response();
|
||||
|
||||
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;
|
||||
|
||||
let req = actix_test::TestRequest::get().uri("/unrelated").to_request();
|
||||
|
||||
actix_test::call_service(&app, req).await.status()
|
||||
};
|
||||
|
||||
assert_eq!(status, 200);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod registration {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_registration() -> Result<()> {
|
||||
let registration: Registration = serde_yaml::from_str(®istration_string())?;
|
||||
let registration: AppServiceRegistration = registration.into();
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_from_yaml_file() -> Result<()> {
|
||||
let registration = AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?;
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_from_yaml_str() -> Result<()> {
|
||||
let registration = AppServiceRegistration::try_from_yaml_str(registration_string())?;
|
||||
|
||||
assert_eq!(registration.id, "appservice");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+42
-19
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "The base component to build a Matrix client library."
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
@@ -8,39 +8,62 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docs"]
|
||||
rustdoc-args = ["--cfg", "feature=\"docs\""]
|
||||
|
||||
[features]
|
||||
default = ["encryption", "sqlite-cryptostore", "messages"]
|
||||
messages = []
|
||||
default = []
|
||||
encryption = ["matrix-sdk-crypto"]
|
||||
sqlite-cryptostore = ["matrix-sdk-crypto/sqlite-cryptostore"]
|
||||
sled_state_store = ["sled", "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305"]
|
||||
sled_cryptostore = ["matrix-sdk-crypto/sled_cryptostore"]
|
||||
markdown = ["ruma/markdown"]
|
||||
|
||||
docs = ["encryption", "sled_cryptostore"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.31"
|
||||
serde = "1.0.110"
|
||||
serde_json = "1.0.53"
|
||||
zeroize = "1.1.0"
|
||||
dashmap = "4.0.2"
|
||||
lru = "0.6.5"
|
||||
ruma = { version = "0.2.0", features = ["client-api-c", "unstable-pre-spec"] }
|
||||
serde = { version = "1.0.126", features = ["rc"] }
|
||||
serde_json = "1.0.64"
|
||||
tracing = "0.1.26"
|
||||
|
||||
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
|
||||
matrix-sdk-crypto = { version = "0.1.0", path = "../matrix_sdk_crypto", optional = true }
|
||||
matrix-sdk-common = { version = "0.3.0", path = "../matrix_sdk_common" }
|
||||
matrix-sdk-crypto = { version = "0.3.0", path = "../matrix_sdk_crypto", optional = true }
|
||||
|
||||
# Misc dependencies
|
||||
thiserror = "1.0.19"
|
||||
thiserror = "1.0.25"
|
||||
futures = "0.3.15"
|
||||
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
|
||||
|
||||
# Deps for the sled state store
|
||||
sled = { version = "0.34.6", optional = true }
|
||||
chacha20poly1305 = { version = "0.8.0", optional = true }
|
||||
pbkdf2 = { version = "0.8.0", default-features = false, optional = true }
|
||||
hmac = { version = "0.11.0", optional = true }
|
||||
sha2 = { version = "0.9.5", optional = true }
|
||||
rand = { version = "0.8.4", optional = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||
version = "0.2.21"
|
||||
version = "1.7.1"
|
||||
default-features = false
|
||||
features = ["sync", "fs"]
|
||||
|
||||
[dev-dependencies]
|
||||
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
|
||||
http = "0.2.1"
|
||||
tracing-subscriber = "0.2.5"
|
||||
tempfile = "3.1.0"
|
||||
matrix-sdk-test = { version = "0.3.0", path = "../matrix_sdk_test" }
|
||||
http = "0.2.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
|
||||
tokio = { version = "1.7.1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
tempfile = "3.2.0"
|
||||
rustyline = "8.2.0"
|
||||
rustyline-derive = "0.4.0"
|
||||
atty = "0.2.14"
|
||||
clap = "2.33.3"
|
||||
syntect = "4.5.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.12"
|
||||
wasm-bindgen-test = "0.3.24"
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use atty::Stream;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use clap::{App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches, SubCommand};
|
||||
use futures::executor::block_on;
|
||||
use matrix_sdk_base::{RoomInfo, Store};
|
||||
use ruma::{
|
||||
events::EventType,
|
||||
identifiers::{RoomId, UserId},
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use rustyline::{
|
||||
completion::{Completer, Pair},
|
||||
error::ReadlineError,
|
||||
highlight::{Highlighter, MatchingBracketHighlighter},
|
||||
hint::{Hinter, HistoryHinter},
|
||||
validate::{MatchingBracketValidator, Validator},
|
||||
CompletionType, Config, Context, EditMode, Editor, OutputStreamType,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use rustyline_derive::Helper;
|
||||
use serde::Serialize;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use syntect::{
|
||||
dumps::from_binary,
|
||||
easy::HighlightLines,
|
||||
highlighting::{Style, ThemeSet},
|
||||
parsing::SyntaxSet,
|
||||
util::{as_24_bit_terminal_escaped, LinesWithEndings},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct Inspector {
|
||||
store: Store,
|
||||
printer: Printer,
|
||||
}
|
||||
|
||||
#[derive(Helper)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct InspectorHelper {
|
||||
store: Store,
|
||||
_highlighter: MatchingBracketHighlighter,
|
||||
_validator: MatchingBracketValidator,
|
||||
_hinter: HistoryHinter,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl InspectorHelper {
|
||||
const EVENT_TYPES: &'static [&'static str] = &[
|
||||
"m.room.aliases",
|
||||
"m.room.avatar",
|
||||
"m.room.canonical_alias",
|
||||
"m.room.create",
|
||||
"m.room.encryption",
|
||||
"m.room.guest_access",
|
||||
"m.room.history_visibility",
|
||||
"m.room.join_rules",
|
||||
"m.room.name",
|
||||
"m.room.power_levels",
|
||||
"m.room.tombstone",
|
||||
"m.room.topic",
|
||||
];
|
||||
|
||||
fn new(store: Store) -> Self {
|
||||
Self {
|
||||
store,
|
||||
_highlighter: MatchingBracketHighlighter::new(),
|
||||
_validator: MatchingBracketValidator::new(),
|
||||
_hinter: HistoryHinter {},
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_event_types(&self, arg: Option<&&str>) -> Vec<Pair> {
|
||||
Self::EVENT_TYPES
|
||||
.iter()
|
||||
.map(|t| Pair { display: t.to_string(), replacement: format!("{} ", t) })
|
||||
.filter(|r| if let Some(arg) = arg { r.replacement.starts_with(arg) } else { true })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn complete_rooms(&self, arg: Option<&&str>) -> Vec<Pair> {
|
||||
let rooms: Vec<RoomInfo> = block_on(async { self.store.get_room_infos().await.unwrap() });
|
||||
|
||||
rooms
|
||||
.into_iter()
|
||||
.map(|r| Pair {
|
||||
display: r.room_id.to_string(),
|
||||
replacement: format!("{} ", r.room_id.to_string()),
|
||||
})
|
||||
.filter(|r| if let Some(arg) = arg { r.replacement.starts_with(arg) } else { true })
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Completer for InspectorHelper {
|
||||
type Candidate = Pair;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_: &Context<'_>,
|
||||
) -> Result<(usize, Vec<Pair>), ReadlineError> {
|
||||
let args: Vec<&str> = line.split_ascii_whitespace().collect();
|
||||
|
||||
let commands = vec![
|
||||
("get-state", "get a state event in the given room"),
|
||||
("get-profiles", "get all the stored profiles in the given room"),
|
||||
("list-rooms", "list all rooms"),
|
||||
("get-members", "get all the membership events in the given room"),
|
||||
]
|
||||
.iter()
|
||||
.map(|(r, d)| Pair {
|
||||
display: format!("{} ({})", r, d),
|
||||
replacement: format!("{} ", r.to_string()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if args.is_empty() {
|
||||
Ok((pos, commands))
|
||||
} else if args.len() == 1 {
|
||||
if (args[0] == "get-state" || args[0] == "get-members" || args[0] == "get-profiles")
|
||||
&& line.ends_with(' ')
|
||||
{
|
||||
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
|
||||
} else {
|
||||
Ok((
|
||||
0,
|
||||
commands.into_iter().filter(|c| c.replacement.starts_with(args[0])).collect(),
|
||||
))
|
||||
}
|
||||
} else if args.len() == 2 {
|
||||
if args[0] == "get-state" {
|
||||
if line.ends_with(' ') {
|
||||
Ok((args[0].len() + args[1].len() + 2, self.complete_event_types(args.get(2))))
|
||||
} else {
|
||||
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
|
||||
}
|
||||
} else if args[0] == "get-members" || args[0] == "get-profiles" {
|
||||
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
|
||||
} else {
|
||||
Ok((pos, vec![]))
|
||||
}
|
||||
} else if args.len() == 3 {
|
||||
if args[0] == "get-state" {
|
||||
Ok((args[0].len() + args[1].len() + 2, self.complete_event_types(args.get(2))))
|
||||
} else {
|
||||
Ok((pos, vec![]))
|
||||
}
|
||||
} else {
|
||||
Ok((pos, vec![]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Hinter for InspectorHelper {
|
||||
type Hint = String;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Highlighter for InspectorHelper {}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Validator for InspectorHelper {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct Printer {
|
||||
ps: Arc<SyntaxSet>,
|
||||
ts: Arc<ThemeSet>,
|
||||
json: bool,
|
||||
color: bool,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Printer {
|
||||
fn new(json: bool, color: bool) -> Self {
|
||||
let syntax_set: SyntaxSet = from_binary(include_bytes!("./syntaxes.bin"));
|
||||
let themes: ThemeSet = from_binary(include_bytes!("./themes.bin"));
|
||||
|
||||
Self { ps: syntax_set.into(), ts: themes.into(), json, color }
|
||||
}
|
||||
|
||||
fn pretty_print_struct<T: Debug + Serialize>(&self, data: &T) {
|
||||
let data = if self.json {
|
||||
serde_json::to_string_pretty(data).expect("Can't serialize struct")
|
||||
} else {
|
||||
format!("{:#?}", data)
|
||||
};
|
||||
|
||||
let syntax = if self.json {
|
||||
self.ps.find_syntax_by_extension("rs").expect("Can't find rust syntax extension")
|
||||
} else {
|
||||
self.ps.find_syntax_by_extension("json").expect("Can't find json syntax extension")
|
||||
};
|
||||
|
||||
if self.color {
|
||||
let mut h = HighlightLines::new(syntax, &self.ts.themes["Forest Night"]);
|
||||
|
||||
for line in LinesWithEndings::from(&data) {
|
||||
let ranges: Vec<(Style, &str)> = h.highlight(line, &self.ps);
|
||||
let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
|
||||
print!("{}", escaped);
|
||||
}
|
||||
|
||||
// Clear the formatting
|
||||
println!("\x1b[0m");
|
||||
} else {
|
||||
println!("{}", data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Inspector {
|
||||
fn new(database_path: &str, json: bool, color: bool) -> Self {
|
||||
let printer = Printer::new(json, color);
|
||||
let (store, _) = Store::open_default(database_path, None).unwrap();
|
||||
|
||||
Self { store, printer }
|
||||
}
|
||||
|
||||
async fn run(&self, matches: ArgMatches<'_>) {
|
||||
match matches.subcommand() {
|
||||
("get-profiles", args) => {
|
||||
let args = args.expect("No args provided for get-state");
|
||||
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
|
||||
|
||||
self.get_profiles(room_id).await;
|
||||
}
|
||||
|
||||
("get-members", args) => {
|
||||
let args = args.expect("No args provided for get-state");
|
||||
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
|
||||
|
||||
self.get_members(room_id).await;
|
||||
}
|
||||
("list-rooms", _) => self.list_rooms().await,
|
||||
("get-display-names", args) => {
|
||||
let args = args.expect("No args provided for get-state");
|
||||
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
|
||||
let display_name = args.value_of("display-name").unwrap().to_string();
|
||||
self.get_display_name_owners(room_id, display_name).await;
|
||||
}
|
||||
("get-state", args) => {
|
||||
let args = args.expect("No args provided for get-state");
|
||||
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
|
||||
let event_type = EventType::try_from(args.value_of("event-type").unwrap()).unwrap();
|
||||
self.get_state(room_id, event_type).await;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_rooms(&self) {
|
||||
let rooms: Vec<RoomInfo> = self.store.get_room_infos().await.unwrap();
|
||||
self.printer.pretty_print_struct(&rooms);
|
||||
}
|
||||
|
||||
async fn get_display_name_owners(&self, room_id: RoomId, display_name: String) {
|
||||
let users = self.store.get_users_with_display_name(&room_id, &display_name).await.unwrap();
|
||||
self.printer.pretty_print_struct(&users);
|
||||
}
|
||||
|
||||
async fn get_profiles(&self, room_id: RoomId) {
|
||||
let joined: Vec<UserId> = self.store.get_joined_user_ids(&room_id).await.unwrap();
|
||||
|
||||
for member in joined {
|
||||
let event = self.store.get_profile(&room_id, &member).await.unwrap();
|
||||
self.printer.pretty_print_struct(&event);
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_members(&self, room_id: RoomId) {
|
||||
let joined: Vec<UserId> = self.store.get_joined_user_ids(&room_id).await.unwrap();
|
||||
|
||||
for member in joined {
|
||||
let event = self.store.get_member_event(&room_id, &member).await.unwrap();
|
||||
self.printer.pretty_print_struct(&event);
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_state(&self, room_id: RoomId, event_type: EventType) {
|
||||
self.printer.pretty_print_struct(
|
||||
&self.store.get_state_event(&room_id, event_type, "").await.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn subcommands() -> Vec<Argparse<'static, 'static>> {
|
||||
vec![
|
||||
SubCommand::with_name("list-rooms"),
|
||||
SubCommand::with_name("get-members").arg(
|
||||
Arg::with_name("room-id").required(true).validator(|r| {
|
||||
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
|
||||
}),
|
||||
),
|
||||
SubCommand::with_name("get-profiles").arg(
|
||||
Arg::with_name("room-id").required(true).validator(|r| {
|
||||
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
|
||||
}),
|
||||
),
|
||||
SubCommand::with_name("get-display-names")
|
||||
.arg(Arg::with_name("room-id").required(true).validator(|r| {
|
||||
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
|
||||
}))
|
||||
.arg(Arg::with_name("display-name").required(true)),
|
||||
SubCommand::with_name("get-state")
|
||||
.arg(Arg::with_name("room-id").required(true).validator(|r| {
|
||||
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
|
||||
}))
|
||||
.arg(Arg::with_name("event-type").required(true).validator(|e| {
|
||||
EventType::try_from(e).map(|_| ()).map_err(|_| "Invalid event type".to_string())
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
async fn parse_and_run(&self, input: &str) {
|
||||
let argparse = Argparse::new("state-inspector")
|
||||
.global_setting(ArgParseSettings::DisableHelpFlags)
|
||||
.global_setting(ArgParseSettings::DisableVersion)
|
||||
.global_setting(ArgParseSettings::VersionlessSubcommands)
|
||||
.global_setting(ArgParseSettings::NoBinaryName)
|
||||
.setting(ArgParseSettings::SubcommandRequiredElseHelp)
|
||||
.subcommands(Inspector::subcommands());
|
||||
|
||||
match argparse.get_matches_from_safe(input.split_ascii_whitespace()) {
|
||||
Ok(m) => {
|
||||
self.run(m).await;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
let argparse = Argparse::new("state-inspector")
|
||||
.global_setting(ArgParseSettings::DisableVersion)
|
||||
.global_setting(ArgParseSettings::VersionlessSubcommands)
|
||||
.arg(Arg::with_name("database").required(true))
|
||||
.arg(
|
||||
Arg::with_name("json")
|
||||
.long("json")
|
||||
.help("set the output to raw json instead of Rust structs")
|
||||
.global(true)
|
||||
.takes_value(false),
|
||||
)
|
||||
.subcommands(Inspector::subcommands());
|
||||
|
||||
let matches = argparse.get_matches();
|
||||
|
||||
let database_path = matches.args.get("database").expect("No database path");
|
||||
let json = matches.is_present("json");
|
||||
let color = atty::is(Stream::Stdout);
|
||||
|
||||
let inspector = Inspector::new(&database_path.vals[0].to_string_lossy(), json, color);
|
||||
|
||||
if matches.subcommand.is_none() {
|
||||
let config = Config::builder()
|
||||
.history_ignore_space(true)
|
||||
.completion_type(CompletionType::List)
|
||||
.edit_mode(EditMode::Emacs)
|
||||
.output_stream(OutputStreamType::Stdout)
|
||||
.build();
|
||||
|
||||
let helper = InspectorHelper::new(inspector.store.clone());
|
||||
|
||||
let mut rl = Editor::<InspectorHelper>::with_config(config);
|
||||
rl.set_helper(Some(helper));
|
||||
|
||||
while let Ok(input) = rl.readline(">> ") {
|
||||
rl.add_history_entry(input.as_str());
|
||||
block_on(inspector.parse_and_run(input.as_str()));
|
||||
}
|
||||
} else {
|
||||
block_on(inspector.run(matches));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
panic!("This example doesn't run on WASM");
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
+1069
-1622
File diff suppressed because it is too large
Load Diff
@@ -15,27 +15,28 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use serde_json::Error as JsonError;
|
||||
use std::io::Error as IoError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
use matrix_sdk_crypto::{MegolmError, OlmError};
|
||||
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
|
||||
use serde_json::Error as JsonError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Result type of the rust-sdk.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// Internal representation of errors.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// Queried endpoint requires authentication but was called on an anonymous client.
|
||||
/// Queried endpoint requires authentication but was called on an anonymous
|
||||
/// client.
|
||||
#[error("the queried endpoint requires authentication but was called before logging in")]
|
||||
AuthenticationRequired,
|
||||
|
||||
/// A generic error returned when the state store fails not due to
|
||||
/// IO or (de)serialization.
|
||||
#[error("state store: {0}")]
|
||||
StateStore(String),
|
||||
#[error(transparent)]
|
||||
StateStore(#[from] crate::store::StoreError),
|
||||
|
||||
/// An error when (de)serializing JSON.
|
||||
#[error(transparent)]
|
||||
@@ -45,15 +46,21 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] IoError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[error(transparent)]
|
||||
CryptoStore(#[from] CryptoStoreError),
|
||||
|
||||
/// An error occurred during a E2EE operation.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[error(transparent)]
|
||||
OlmError(#[from] OlmError),
|
||||
|
||||
/// An error occurred during a E2EE group operation.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[error(transparent)]
|
||||
MegolmError(#[from] MegolmError),
|
||||
}
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
// 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.
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_common::locks::RwLock;
|
||||
|
||||
use crate::events::{
|
||||
fully_read::FullyReadEvent,
|
||||
ignored_user_list::IgnoredUserListEvent,
|
||||
presence::PresenceEvent,
|
||||
push_rules::PushRulesEvent,
|
||||
receipt::ReceiptEvent,
|
||||
room::{
|
||||
aliases::AliasesEvent,
|
||||
avatar::AvatarEvent,
|
||||
canonical_alias::CanonicalAliasEvent,
|
||||
join_rules::JoinRulesEvent,
|
||||
member::{MemberEvent, MemberEventContent},
|
||||
message::{feedback::FeedbackEvent, MessageEvent},
|
||||
name::NameEvent,
|
||||
power_levels::PowerLevelsEvent,
|
||||
redaction::RedactionEvent,
|
||||
tombstone::TombstoneEvent,
|
||||
},
|
||||
stripped::{
|
||||
StrippedRoomAliases, StrippedRoomAvatar, StrippedRoomCanonicalAlias, StrippedRoomJoinRules,
|
||||
StrippedRoomMember, StrippedRoomName, StrippedRoomPowerLevels,
|
||||
},
|
||||
typing::TypingEvent,
|
||||
};
|
||||
use crate::{Room, RoomState};
|
||||
|
||||
/// Type alias for `RoomState` enum when passed to `EventEmitter` methods.
|
||||
pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
|
||||
|
||||
/// This trait allows any type implementing `EventEmitter` to specify event callbacks for each event.
|
||||
/// The `Client` calls each method when the corresponding event is received.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::ops::Deref;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use std::{env, process::exit};
|
||||
/// # use matrix_sdk_base::{
|
||||
/// # self,
|
||||
/// # events::{
|
||||
/// # room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
/// # },
|
||||
/// # EventEmitter, SyncRoom
|
||||
/// # };
|
||||
/// # use matrix_sdk_common::locks::RwLock;
|
||||
///
|
||||
/// struct EventCallback;
|
||||
///
|
||||
/// #[async_trait::async_trait]
|
||||
/// impl EventEmitter for EventCallback {
|
||||
/// async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
|
||||
/// if let SyncRoom::Joined(room) = room {
|
||||
/// if let MessageEvent {
|
||||
/// content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
/// sender,
|
||||
/// ..
|
||||
/// } = event
|
||||
/// {
|
||||
/// let name = {
|
||||
/// let room = room.read().await;
|
||||
/// let member = room.members.get(&sender).unwrap();
|
||||
/// member
|
||||
/// .display_name
|
||||
/// .as_ref()
|
||||
/// .map(ToString::to_string)
|
||||
/// .unwrap_or(sender.to_string())
|
||||
/// };
|
||||
/// println!("{}: {}", name, msg_body);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[async_trait::async_trait]
|
||||
pub trait EventEmitter: Send + Sync {
|
||||
// ROOM EVENTS from `IncomingTimeline`
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMember` event.
|
||||
async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
|
||||
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
|
||||
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
|
||||
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
|
||||
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
|
||||
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
|
||||
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
|
||||
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
|
||||
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
|
||||
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {}
|
||||
|
||||
// `RoomEvent`s from `IncomingState`
|
||||
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
|
||||
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomName` event.
|
||||
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
|
||||
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
|
||||
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
|
||||
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
|
||||
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
|
||||
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {}
|
||||
|
||||
// `AnyStrippedStateEvent`s
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedRoomMember,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
|
||||
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
|
||||
async fn on_stripped_state_canonical_alias(&self, _: SyncRoom, _: &StrippedRoomCanonicalAlias) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
|
||||
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
|
||||
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
|
||||
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
|
||||
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {}
|
||||
|
||||
// `NonRoomEvent` (this is a type alias from ruma_events)
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomMember` event.
|
||||
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomName` event.
|
||||
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomCanonicalAlias` event.
|
||||
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
|
||||
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::Typing` event.
|
||||
async fn on_account_data_typing(&self, _: SyncRoom, _: &TypingEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::Receipt` event.
|
||||
///
|
||||
/// This is always a read receipt.
|
||||
async fn on_account_data_receipt(&self, _: SyncRoom, _: &ReceiptEvent) {}
|
||||
|
||||
// `PresenceEvent` is a struct so there is only the one method
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
|
||||
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use matrix_sdk_test::{async_test, sync_response, SyncResponseFile};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen_test::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EvEmitterTest(Arc<Mutex<Vec<String>>>);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventEmitter for EvEmitterTest {
|
||||
async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) {
|
||||
self.0.lock().await.push("member".to_string())
|
||||
}
|
||||
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {
|
||||
self.0.lock().await.push("name".to_string())
|
||||
}
|
||||
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
|
||||
self.0.lock().await.push("canonical".to_string())
|
||||
}
|
||||
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
|
||||
self.0.lock().await.push("aliases".to_string())
|
||||
}
|
||||
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
|
||||
self.0.lock().await.push("avatar".to_string())
|
||||
}
|
||||
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {
|
||||
self.0.lock().await.push("message".to_string())
|
||||
}
|
||||
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {
|
||||
self.0.lock().await.push("feedback".to_string())
|
||||
}
|
||||
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {
|
||||
self.0.lock().await.push("redaction".to_string())
|
||||
}
|
||||
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
|
||||
self.0.lock().await.push("power".to_string())
|
||||
}
|
||||
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {
|
||||
self.0.lock().await.push("tombstone".to_string())
|
||||
}
|
||||
|
||||
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {
|
||||
self.0.lock().await.push("state member".to_string())
|
||||
}
|
||||
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {
|
||||
self.0.lock().await.push("state name".to_string())
|
||||
}
|
||||
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
|
||||
self.0.lock().await.push("state canonical".to_string())
|
||||
}
|
||||
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
|
||||
self.0.lock().await.push("state aliases".to_string())
|
||||
}
|
||||
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
|
||||
self.0.lock().await.push("state avatar".to_string())
|
||||
}
|
||||
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
|
||||
self.0.lock().await.push("state power".to_string())
|
||||
}
|
||||
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {
|
||||
self.0.lock().await.push("state rules".to_string())
|
||||
}
|
||||
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedRoomMember,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.push("stripped state member".to_string())
|
||||
}
|
||||
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {
|
||||
self.0.lock().await.push("stripped state name".to_string())
|
||||
}
|
||||
async fn on_stripped_state_canonical_alias(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedRoomCanonicalAlias,
|
||||
) {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.push("stripped state canonical".to_string())
|
||||
}
|
||||
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.push("stripped state aliases".to_string())
|
||||
}
|
||||
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.push("stripped state avatar".to_string())
|
||||
}
|
||||
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {
|
||||
self.0.lock().await.push("stripped state power".to_string())
|
||||
}
|
||||
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {
|
||||
self.0.lock().await.push("stripped state rules".to_string())
|
||||
}
|
||||
|
||||
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {
|
||||
self.0.lock().await.push("account presence".to_string())
|
||||
}
|
||||
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {
|
||||
self.0.lock().await.push("account ignore".to_string())
|
||||
}
|
||||
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {
|
||||
self.0.lock().await.push("account push rules".to_string())
|
||||
}
|
||||
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {
|
||||
self.0.lock().await.push("account read".to_string())
|
||||
}
|
||||
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {
|
||||
self.0.lock().await.push("presence event".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::identifiers::UserId;
|
||||
use crate::{BaseClient, Session};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
async fn get_client() -> BaseClient {
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:example.com").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
let client = BaseClient::new().unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
client
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_emitter_joined() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let emitter = Box::new(EvEmitterTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.add_event_emitter(emitter).await;
|
||||
|
||||
let mut response = sync_response(SyncResponseFile::Default);
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"state rules",
|
||||
"state member",
|
||||
"state aliases",
|
||||
"state power",
|
||||
"state canonical",
|
||||
"state member",
|
||||
"state member",
|
||||
"message",
|
||||
"account read",
|
||||
"account ignore",
|
||||
"presence event"
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_emitter_invite() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let emitter = Box::new(EvEmitterTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.add_event_emitter(emitter).await;
|
||||
|
||||
let mut response = sync_response(SyncResponseFile::Invite);
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
["stripped state name", "stripped state member"],
|
||||
)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_emitter_leave() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let emitter = Box::new(EvEmitterTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.add_event_emitter(emitter).await;
|
||||
|
||||
let mut response = sync_response(SyncResponseFile::Leave);
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"state rules",
|
||||
"state member",
|
||||
"state aliases",
|
||||
"state power",
|
||||
"state canonical",
|
||||
"state member",
|
||||
"state member",
|
||||
"message"
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -20,13 +20,13 @@
|
||||
//! The following crate feature flags are available:
|
||||
//!
|
||||
//! * `encryption`: Enables end-to-end encryption support in the library.
|
||||
//! * `sqlite-cryptostore`: Enables a SQLite based store for the encryption
|
||||
//! * `sled_cryptostore`: Enables a Sled based store for the encryption
|
||||
//! keys. If this is disabled and `encryption` support is enabled the keys will
|
||||
//! by default be stored only in memory and thus lost after the client is
|
||||
//! destroyed.
|
||||
//! * `markdown`: Support for sending markdown formatted messages.
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
@@ -34,22 +34,25 @@
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
#![cfg_attr(feature = "docs", feature(doc_cfg))]
|
||||
|
||||
pub use crate::{error::Error, error::Result, session::Session};
|
||||
pub use matrix_sdk_common::*;
|
||||
|
||||
pub use crate::{
|
||||
error::{Error, Result},
|
||||
session::Session,
|
||||
};
|
||||
|
||||
mod client;
|
||||
mod error;
|
||||
mod event_emitter;
|
||||
mod models;
|
||||
pub mod media;
|
||||
mod rooms;
|
||||
mod session;
|
||||
mod state;
|
||||
mod store;
|
||||
|
||||
pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType};
|
||||
pub use event_emitter::{EventEmitter, SyncRoom};
|
||||
pub use client::{BaseClient, BaseClientConfig};
|
||||
#[cfg(feature = "encryption")]
|
||||
pub use matrix_sdk_crypto::{Device, TrustState};
|
||||
pub use models::Room;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use state::JsonStore;
|
||||
pub use state::StateStore;
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
pub use rooms::{Room, RoomInfo, RoomMember, RoomType};
|
||||
pub use store::{StateChanges, StateStore, Store, StoreError};
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
|
||||
|
||||
use ruma::{
|
||||
api::client::r0::media::get_content_thumbnail::Method,
|
||||
events::{
|
||||
room::{
|
||||
message::{
|
||||
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
|
||||
LocationMessageEventContent, VideoMessageEventContent,
|
||||
},
|
||||
EncryptedFile,
|
||||
},
|
||||
sticker::StickerEventContent,
|
||||
},
|
||||
MxcUri, 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for media data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MediaType {
|
||||
/// A media content URI.
|
||||
Uri(MxcUri),
|
||||
|
||||
/// An encrypted media content.
|
||||
Encrypted(Box<EncryptedFile>),
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaType {
|
||||
fn unique_key(&self) -> String {
|
||||
match self {
|
||||
Self::Uri(uri) => uri.to_string(),
|
||||
Self::Encrypted(file) => file.url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A request for media data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MediaRequest {
|
||||
/// The type of the media file.
|
||||
pub media_type: MediaType,
|
||||
|
||||
/// The requested format of the media data.
|
||||
pub format: MediaFormat,
|
||||
}
|
||||
|
||||
impl UniqueKey for MediaRequest {
|
||||
fn unique_key(&self) -> String {
|
||||
format!("{}{}{}", self.media_type.unique_key(), UNIQUE_SEPARATOR, self.format.unique_key())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for media event content.
|
||||
pub trait MediaEventContent {
|
||||
/// Get the type of the file for `Self`.
|
||||
///
|
||||
/// Returns `None` if `Self` has no file.
|
||||
fn file(&self) -> Option<MediaType>;
|
||||
|
||||
/// Get the type of the thumbnail for `Self`.
|
||||
///
|
||||
/// Returns `None` if `Self` has no thumbnail.
|
||||
fn thumbnail(&self) -> Option<MediaType>;
|
||||
}
|
||||
|
||||
impl MediaEventContent for StickerEventContent {
|
||||
fn file(&self) -> Option<MediaType> {
|
||||
Some(MediaType::Uri(self.url.clone()))
|
||||
}
|
||||
|
||||
fn thumbnail(&self) -> Option<MediaType> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for AudioMessageEventContent {
|
||||
fn file(&self) -> Option<MediaType> {
|
||||
self.url
|
||||
.as_ref()
|
||||
.map(|uri| MediaType::Uri(uri.clone()))
|
||||
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
|
||||
}
|
||||
|
||||
fn thumbnail(&self) -> Option<MediaType> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for FileMessageEventContent {
|
||||
fn file(&self) -> Option<MediaType> {
|
||||
self.url
|
||||
.as_ref()
|
||||
.map(|uri| MediaType::Uri(uri.clone()))
|
||||
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
|
||||
}
|
||||
|
||||
fn thumbnail(&self) -> Option<MediaType> {
|
||||
self.info.as_ref().and_then(|info| {
|
||||
if let Some(uri) = info.thumbnail_url.as_ref() {
|
||||
Some(MediaType::Uri(uri.clone()))
|
||||
} else {
|
||||
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for ImageMessageEventContent {
|
||||
fn file(&self) -> Option<MediaType> {
|
||||
self.url
|
||||
.as_ref()
|
||||
.map(|uri| MediaType::Uri(uri.clone()))
|
||||
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
|
||||
}
|
||||
|
||||
fn thumbnail(&self) -> Option<MediaType> {
|
||||
self.info
|
||||
.as_ref()
|
||||
.and_then(|info| {
|
||||
if let Some(uri) = info.thumbnail_url.as_ref() {
|
||||
Some(MediaType::Uri(uri.clone()))
|
||||
} else {
|
||||
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
|
||||
}
|
||||
})
|
||||
.or_else(|| self.url.as_ref().map(|uri| MediaType::Uri(uri.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for VideoMessageEventContent {
|
||||
fn file(&self) -> Option<MediaType> {
|
||||
self.url
|
||||
.as_ref()
|
||||
.map(|uri| MediaType::Uri(uri.clone()))
|
||||
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
|
||||
}
|
||||
|
||||
fn thumbnail(&self) -> Option<MediaType> {
|
||||
self.info
|
||||
.as_ref()
|
||||
.and_then(|info| {
|
||||
if let Some(uri) = info.thumbnail_url.as_ref() {
|
||||
Some(MediaType::Uri(uri.clone()))
|
||||
} else {
|
||||
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
|
||||
}
|
||||
})
|
||||
.or_else(|| self.url.as_ref().map(|uri| MediaType::Uri(uri.clone())))
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaEventContent for LocationMessageEventContent {
|
||||
fn file(&self) -> Option<MediaType> {
|
||||
None
|
||||
}
|
||||
|
||||
fn thumbnail(&self) -> Option<MediaType> {
|
||||
self.info.as_ref().and_then(|info| {
|
||||
if let Some(uri) = info.thumbnail_url.as_ref() {
|
||||
Some(MediaType::Uri(uri.clone()))
|
||||
} else {
|
||||
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//! De-/serialization functions to and from json strings, allows the type to be used as a query string.
|
||||
|
||||
use serde::de::{Deserialize, Deserializer, Error as _};
|
||||
|
||||
use crate::events::collections::all::Event;
|
||||
use crate::events::presence::PresenceEvent;
|
||||
use crate::events::EventJson;
|
||||
|
||||
pub fn deserialize_events<'de, D>(deserializer: D) -> Result<Vec<Event>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut events = vec![];
|
||||
let ev = Vec::<EventJson<Event>>::deserialize(deserializer)?;
|
||||
for event in ev {
|
||||
events.push(event.deserialize().map_err(D::Error::custom)?);
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
pub fn deserialize_presence<'de, D>(deserializer: D) -> Result<Vec<PresenceEvent>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut events = vec![];
|
||||
let ev = Vec::<EventJson<PresenceEvent>>::deserialize(deserializer)?;
|
||||
for event in ev {
|
||||
events.push(event.deserialize().map_err(D::Error::custom)?);
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::fs;
|
||||
|
||||
use crate::events::room::member::MemberEvent;
|
||||
use crate::events::EventJson;
|
||||
use crate::models::RoomMember;
|
||||
|
||||
#[test]
|
||||
fn events_and_presence_deserialization() {
|
||||
let ev_json = fs::read_to_string("../test_data/events/member.json").unwrap();
|
||||
let ev = serde_json::from_str::<EventJson<MemberEvent>>(&ev_json)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
let member = RoomMember::new(&ev);
|
||||
|
||||
let member_json = serde_json::to_string(&member).unwrap();
|
||||
let mem = serde_json::from_str::<RoomMember>(&member_json).unwrap();
|
||||
assert_eq!(member, mem);
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
//! A queue that holds at most ten of the most recent messages.
|
||||
//!
|
||||
//! The `Room` struct optionally holds a `MessageQueue` if the "messages"
|
||||
//! feature is enabled.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Deref;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
use crate::events::room::message::MessageEvent;
|
||||
use crate::events::EventJson;
|
||||
|
||||
use serde::{de, ser, Serialize};
|
||||
|
||||
/// A queue that holds the 10 most recent messages received from the server.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MessageQueue {
|
||||
msgs: Vec<MessageWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct MessageWrapper(MessageEvent);
|
||||
|
||||
impl Deref for MessageWrapper {
|
||||
type Target = MessageEvent;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MessageWrapper {
|
||||
fn eq(&self, other: &MessageWrapper) -> bool {
|
||||
self.0.event_id == other.0.event_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MessageWrapper {}
|
||||
|
||||
impl PartialOrd for MessageWrapper {
|
||||
fn partial_cmp(&self, other: &MessageWrapper) -> Option<Ordering> {
|
||||
Some(self.0.origin_server_ts.cmp(&other.0.origin_server_ts))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for MessageWrapper {
|
||||
fn cmp(&self, other: &MessageWrapper) -> Ordering {
|
||||
self.partial_cmp(other).unwrap_or(Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MessageQueue {
|
||||
fn eq(&self, other: &MessageQueue) -> bool {
|
||||
self.msgs.len() == other.msgs.len()
|
||||
&& self
|
||||
.msgs
|
||||
.iter()
|
||||
.zip(other.msgs.iter())
|
||||
.all(|(msg_a, msg_b)| msg_a.event_id == msg_b.event_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageQueue {
|
||||
/// Create a new empty `MessageQueue`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
msgs: Vec::with_capacity(20),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a `MessageEvent` into `MessageQueue`, sorted by by `origin_server_ts`.
|
||||
///
|
||||
/// Removes the oldest element in the queue if there are more than 10 elements.
|
||||
pub fn push(&mut self, msg: MessageEvent) -> bool {
|
||||
// only push new messages into the queue
|
||||
if let Some(latest) = self.msgs.last() {
|
||||
if msg.origin_server_ts < latest.origin_server_ts && self.msgs.len() >= 10 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let message = MessageWrapper(msg);
|
||||
match self.msgs.binary_search_by(|m| m.cmp(&message)) {
|
||||
Ok(pos) => {
|
||||
if self.msgs[pos] != message {
|
||||
self.msgs.insert(pos, message)
|
||||
}
|
||||
}
|
||||
Err(pos) => self.msgs.insert(pos, message),
|
||||
}
|
||||
if self.msgs.len() > 10 {
|
||||
self.msgs.remove(0);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &MessageWrapper> {
|
||||
self.msgs.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for MessageQueue {
|
||||
type Item = MessageWrapper;
|
||||
type IntoIter = IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.msgs.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod ser_deser {
|
||||
use super::*;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<MessageQueue, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let messages: Vec<EventJson<MessageEvent>> = de::Deserialize::deserialize(deserializer)?;
|
||||
|
||||
let mut msgs = vec![];
|
||||
for json in messages {
|
||||
let msg = json.deserialize().map_err(D::Error::custom)?;
|
||||
msgs.push(MessageWrapper(msg));
|
||||
}
|
||||
|
||||
Ok(MessageQueue { msgs })
|
||||
}
|
||||
|
||||
pub fn serialize<S>(msgs: &MessageQueue, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: ser::Serializer,
|
||||
{
|
||||
msgs.msgs.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use crate::events::{collections::all::RoomEvent, EventJson};
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use crate::Room;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let mut room = Room::new(&id, &user);
|
||||
|
||||
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
|
||||
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
|
||||
|
||||
let mut msgs = MessageQueue::new();
|
||||
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
|
||||
msgs.push(msg.clone());
|
||||
msg
|
||||
} else {
|
||||
panic!("this should always be a RoomMessage")
|
||||
};
|
||||
room.messages = msgs.clone();
|
||||
|
||||
let mut joined_rooms = HashMap::new();
|
||||
joined_rooms.insert(id, room);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::json!({
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"messages": [ message ],
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&joined_rooms).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let mut room = Room::new(&id, &user);
|
||||
|
||||
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
|
||||
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
|
||||
|
||||
let mut msgs = MessageQueue::new();
|
||||
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
|
||||
msgs.push(msg.clone());
|
||||
msg
|
||||
} else {
|
||||
panic!("this should always be a RoomMessage")
|
||||
};
|
||||
room.messages = msgs;
|
||||
|
||||
let mut joined_rooms = HashMap::new();
|
||||
joined_rooms.insert(id, room.clone());
|
||||
|
||||
let json = serde_json::json!({
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"messages": [ message ],
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
});
|
||||
assert_eq!(
|
||||
joined_rooms,
|
||||
serde_json::from_value::<HashMap<RoomId, Room>>(json).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mod event_deser;
|
||||
#[cfg(feature = "messages")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
|
||||
mod message;
|
||||
mod room;
|
||||
mod room_member;
|
||||
|
||||
pub use room::{Room, RoomName};
|
||||
pub use room_member::RoomMember;
|
||||
@@ -1,781 +0,0 @@
|
||||
// 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.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(feature = "messages")]
|
||||
use super::message::MessageQueue;
|
||||
use super::RoomMember;
|
||||
|
||||
use crate::api::r0::sync::sync_events::{RoomSummary, UnreadNotificationsCount};
|
||||
use crate::events::collections::all::{RoomEvent, StateEvent};
|
||||
use crate::events::presence::PresenceEvent;
|
||||
use crate::events::room::{
|
||||
aliases::AliasesEvent,
|
||||
canonical_alias::CanonicalAliasEvent,
|
||||
encryption::EncryptionEvent,
|
||||
member::{MemberEvent, MembershipChange},
|
||||
name::NameEvent,
|
||||
power_levels::{NotificationPowerLevels, PowerLevelsEvent, PowerLevelsEventContent},
|
||||
tombstone::TombstoneEvent,
|
||||
};
|
||||
use crate::events::stripped::{AnyStrippedStateEvent, StrippedRoomName};
|
||||
use crate::events::{Algorithm, EventType};
|
||||
|
||||
#[cfg(feature = "messages")]
|
||||
use crate::events::room::message::MessageEvent;
|
||||
|
||||
use crate::identifiers::{RoomAliasId, RoomId, UserId};
|
||||
|
||||
use crate::js_int::{Int, UInt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
/// `RoomName` allows the calculation of a text room name.
|
||||
pub struct RoomName {
|
||||
/// The displayed name of the room.
|
||||
name: Option<String>,
|
||||
/// The canonical alias of the room ex. `#room-name:example.com` and port number.
|
||||
canonical_alias: Option<RoomAliasId>,
|
||||
/// List of `RoomAliasId`s the room has been given.
|
||||
aliases: Vec<RoomAliasId>,
|
||||
/// Users which can be used to generate a room name if the room does not have
|
||||
/// one. Required if room name or canonical aliases are not set or empty.
|
||||
pub heroes: Vec<String>,
|
||||
/// Number of users whose membership status is `join`.
|
||||
/// Required if field has changed since last sync; otherwise, it may be
|
||||
/// omitted.
|
||||
pub joined_member_count: Option<UInt>,
|
||||
/// Number of users whose membership status is `invite`.
|
||||
/// Required if field has changed since last sync; otherwise, it may be
|
||||
/// omitted.
|
||||
pub invited_member_count: Option<UInt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct PowerLevels {
|
||||
/// The level required to ban a user.
|
||||
pub ban: Int,
|
||||
/// The level required to send specific event types.
|
||||
///
|
||||
/// This is a mapping from event type to power level required.
|
||||
pub events: BTreeMap<EventType, Int>,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: Int,
|
||||
/// The level required to invite a user.
|
||||
pub invite: Int,
|
||||
/// The level required to kick a user.
|
||||
pub kick: Int,
|
||||
/// The level required to redact an event.
|
||||
pub redact: Int,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: Int,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: Int,
|
||||
/// The power level requirements for specific notification types.
|
||||
///
|
||||
/// This is a mapping from `key` to power level for that notifications key.
|
||||
pub notifications: Int,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
/// Encryption info of the room.
|
||||
pub struct EncryptionInfo {
|
||||
/// The encryption algorithm that should be used to encrypt messages in the
|
||||
/// room.
|
||||
algorithm: Algorithm,
|
||||
/// How long should a session be used before it is rotated.
|
||||
rotation_period_ms: u64,
|
||||
/// The maximum amount of messages that should be encrypted using the same
|
||||
/// session.
|
||||
rotation_period_messages: u64,
|
||||
}
|
||||
|
||||
impl EncryptionInfo {
|
||||
/// The encryption algorithm that should be used to encrypt messages in the
|
||||
/// room.
|
||||
pub fn algorithm(&self) -> &Algorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
/// How long should a session be used before it is rotated.
|
||||
pub fn rotation_period(&self) -> u64 {
|
||||
self.rotation_period_ms
|
||||
}
|
||||
|
||||
/// The maximum amount of messages that should be encrypted using the same
|
||||
/// session.
|
||||
pub fn rotation_period_messages(&self) -> u64 {
|
||||
self.rotation_period_messages
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EncryptionEvent> for EncryptionInfo {
|
||||
fn from(event: &EncryptionEvent) -> Self {
|
||||
EncryptionInfo {
|
||||
algorithm: event.content.algorithm.clone(),
|
||||
rotation_period_ms: event
|
||||
.content
|
||||
.rotation_period_ms
|
||||
.map_or(604_800_000, Into::into),
|
||||
rotation_period_messages: event.content.rotation_period_msgs.map_or(100, Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
pub struct Tombstone {
|
||||
/// A server-defined message.
|
||||
body: String,
|
||||
/// The room that is now active.
|
||||
replacement: RoomId,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
/// A Matrix room.
|
||||
pub struct Room {
|
||||
/// The unique id of the room.
|
||||
pub room_id: RoomId,
|
||||
/// The name of the room, clients use this to represent a room.
|
||||
pub room_name: RoomName,
|
||||
/// The mxid of our own user.
|
||||
pub own_user_id: UserId,
|
||||
/// The mxid of the room creator.
|
||||
pub creator: Option<UserId>,
|
||||
/// The map of room members.
|
||||
pub members: HashMap<UserId, RoomMember>,
|
||||
/// A queue of messages, holds no more than 10 of the most recent messages.
|
||||
///
|
||||
/// This is helpful when using a `StateStore` to avoid multiple requests
|
||||
/// to the server for messages.
|
||||
#[cfg(feature = "messages")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
|
||||
#[serde(with = "super::message::ser_deser")]
|
||||
pub messages: MessageQueue,
|
||||
/// A list of users that are currently typing.
|
||||
pub typing_users: Vec<UserId>,
|
||||
/// The power level requirements for specific actions in this room
|
||||
pub power_levels: Option<PowerLevels>,
|
||||
/// Optional encryption info, will be `Some` if the room is encrypted.
|
||||
pub encrypted: Option<EncryptionInfo>,
|
||||
/// Number of unread notifications with highlight flag set.
|
||||
pub unread_highlight: Option<UInt>,
|
||||
/// Number of unread notifications.
|
||||
pub unread_notifications: Option<UInt>,
|
||||
/// The tombstone state of this room.
|
||||
pub tombstone: Option<Tombstone>,
|
||||
}
|
||||
|
||||
impl RoomName {
|
||||
pub fn push_alias(&mut self, alias: RoomAliasId) -> bool {
|
||||
self.aliases.push(alias);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool {
|
||||
self.canonical_alias = Some(alias);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: &str) -> bool {
|
||||
self.name = Some(name.to_string());
|
||||
true
|
||||
}
|
||||
|
||||
pub fn calculate_name(&self, members: &HashMap<UserId, RoomMember>) -> String {
|
||||
// https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room.
|
||||
// the order in which we check for a name ^^
|
||||
if let Some(name) = &self.name {
|
||||
let name = name.trim();
|
||||
name.to_string()
|
||||
} else if let Some(alias) = &self.canonical_alias {
|
||||
let alias = alias.alias().trim();
|
||||
alias.to_string()
|
||||
} else if !self.aliases.is_empty() && !self.aliases[0].alias().is_empty() {
|
||||
self.aliases[0].alias().trim().to_string()
|
||||
} else {
|
||||
let joined = self.joined_member_count.unwrap_or(UInt::MIN);
|
||||
let invited = self.invited_member_count.unwrap_or(UInt::MIN);
|
||||
let heroes = UInt::new(self.heroes.len() as u64).unwrap();
|
||||
let one = UInt::new(1).unwrap();
|
||||
|
||||
let invited_joined = if invited + joined == UInt::MIN {
|
||||
UInt::MIN
|
||||
} else {
|
||||
invited + joined - one
|
||||
};
|
||||
|
||||
// TODO this should use `self.heroes but it is always empty??
|
||||
if heroes >= invited_joined {
|
||||
let mut names = members
|
||||
.values()
|
||||
.take(3)
|
||||
.map(|mem| {
|
||||
mem.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| mem.user_id.localpart().to_string())
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
// stabilize ordering
|
||||
names.sort();
|
||||
names.join(", ")
|
||||
} else if heroes < invited_joined && invited + joined > one {
|
||||
let mut names = members
|
||||
.values()
|
||||
.take(3)
|
||||
.map(|mem| {
|
||||
mem.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| mem.user_id.localpart().to_string())
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
// TODO what length does the spec want us to use here and in the `else`
|
||||
format!("{}, and {} others", names.join(", "), (joined + invited))
|
||||
} else {
|
||||
format!("Empty Room (was {} others)", members.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// Create a new room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The unique id of the room.
|
||||
///
|
||||
/// * `own_user_id` - The mxid of our own user.
|
||||
pub fn new(room_id: &RoomId, own_user_id: &UserId) -> Self {
|
||||
Room {
|
||||
room_id: room_id.clone(),
|
||||
room_name: RoomName::default(),
|
||||
own_user_id: own_user_id.clone(),
|
||||
creator: None,
|
||||
members: HashMap::new(),
|
||||
#[cfg(feature = "messages")]
|
||||
messages: MessageQueue::new(),
|
||||
typing_users: Vec::new(),
|
||||
power_levels: None,
|
||||
encrypted: None,
|
||||
unread_highlight: None,
|
||||
unread_notifications: None,
|
||||
tombstone: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the display name of the room.
|
||||
pub fn display_name(&self) -> String {
|
||||
self.room_name.calculate_name(&self.members)
|
||||
}
|
||||
|
||||
/// Is the room a encrypted room.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.encrypted.is_some()
|
||||
}
|
||||
|
||||
/// Get the encryption info if any of the room.
|
||||
///
|
||||
/// Returns None if the room is not encrypted.
|
||||
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
|
||||
self.encrypted.as_ref()
|
||||
}
|
||||
|
||||
fn add_member(&mut self, event: &MemberEvent) -> bool {
|
||||
if self
|
||||
.members
|
||||
.contains_key(&UserId::try_from(event.state_key.as_str()).unwrap())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let member = RoomMember::new(event);
|
||||
|
||||
self.members
|
||||
.insert(UserId::try_from(event.state_key.as_str()).unwrap(), member);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Add to the list of `RoomAliasId`s.
|
||||
fn push_room_alias(&mut self, alias: &RoomAliasId) -> bool {
|
||||
self.room_name.push_alias(alias.clone());
|
||||
true
|
||||
}
|
||||
|
||||
/// RoomAliasId is `#alias:hostname` and `port`
|
||||
fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool {
|
||||
self.room_name.set_canonical(alias.clone());
|
||||
true
|
||||
}
|
||||
|
||||
fn set_room_name(&mut self, name: &str) -> bool {
|
||||
self.room_name.set_name(name);
|
||||
true
|
||||
}
|
||||
|
||||
fn set_room_power_level(&mut self, event: &PowerLevelsEvent) -> bool {
|
||||
let PowerLevelsEventContent {
|
||||
ban,
|
||||
events,
|
||||
events_default,
|
||||
invite,
|
||||
kick,
|
||||
redact,
|
||||
state_default,
|
||||
users_default,
|
||||
notifications: NotificationPowerLevels { room },
|
||||
..
|
||||
} = &event.content;
|
||||
|
||||
let power = PowerLevels {
|
||||
ban: *ban,
|
||||
events: events.clone(),
|
||||
events_default: *events_default,
|
||||
invite: *invite,
|
||||
kick: *kick,
|
||||
redact: *redact,
|
||||
state_default: *state_default,
|
||||
users_default: *users_default,
|
||||
notifications: *room,
|
||||
};
|
||||
self.power_levels = Some(power);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn set_room_summary(&mut self, summary: &RoomSummary) {
|
||||
let RoomSummary {
|
||||
heroes,
|
||||
joined_member_count,
|
||||
invited_member_count,
|
||||
} = summary;
|
||||
self.room_name.heroes = heroes.clone();
|
||||
self.room_name.invited_member_count = *invited_member_count;
|
||||
self.room_name.joined_member_count = *joined_member_count;
|
||||
}
|
||||
|
||||
pub(crate) fn set_unread_notice_count(&mut self, notifications: &UnreadNotificationsCount) {
|
||||
self.unread_highlight = notifications.highlight_count;
|
||||
self.unread_notifications = notifications.notification_count;
|
||||
}
|
||||
|
||||
/// Handle a room.member updating the room state if necessary.
|
||||
///
|
||||
/// Returns true if the joined member list changed, false otherwise.
|
||||
pub fn handle_membership(&mut self, event: &MemberEvent) -> bool {
|
||||
// TODO this would not be handled correctly as all the MemberEvents have the `prev_content`
|
||||
// inside of `unsigned` field
|
||||
match event.membership_change() {
|
||||
MembershipChange::Invited | MembershipChange::Joined => self.add_member(event),
|
||||
_ => {
|
||||
let user = if let Ok(id) = UserId::try_from(event.state_key.as_str()) {
|
||||
id
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
if let Some(member) = self.members.get_mut(&user) {
|
||||
member.update_member(event)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a room.message event and update the `MessageQueue` if necessary.
|
||||
///
|
||||
/// Returns true if `MessageQueue` was added to.
|
||||
#[cfg(feature = "messages")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
|
||||
pub fn handle_message(&mut self, event: &MessageEvent) -> bool {
|
||||
self.messages.push(event.clone())
|
||||
}
|
||||
|
||||
/// Handle a room.aliases event, updating the room state if necessary.
|
||||
///
|
||||
/// Returns true if the room name changed, false otherwise.
|
||||
pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool {
|
||||
match event.content.aliases.as_slice() {
|
||||
[alias] => self.push_room_alias(alias),
|
||||
[alias, ..] => self.push_room_alias(alias),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a room.canonical_alias event, updating the room state if necessary.
|
||||
///
|
||||
/// Returns true if the room name changed, false otherwise.
|
||||
pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool {
|
||||
match &event.content.alias {
|
||||
Some(name) => self.canonical_alias(&name),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a room.name event, updating the room state if necessary.
|
||||
///
|
||||
/// Returns true if the room name changed, false otherwise.
|
||||
pub fn handle_room_name(&mut self, event: &NameEvent) -> bool {
|
||||
match event.content.name() {
|
||||
Some(name) => self.set_room_name(name),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a room.name event, updating the room state if necessary.
|
||||
///
|
||||
/// Returns true if the room name changed, false otherwise.
|
||||
pub fn handle_stripped_room_name(&mut self, event: &StrippedRoomName) -> bool {
|
||||
match event.content.name() {
|
||||
Some(name) => self.set_room_name(name),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a room.power_levels event, updating the room state if necessary.
|
||||
///
|
||||
/// Returns true if the room name changed, false otherwise.
|
||||
pub fn handle_power_level(&mut self, event: &PowerLevelsEvent) -> bool {
|
||||
// NOTE: this is always true, we assume that if we get an event their is an update.
|
||||
let mut updated = self.set_room_power_level(event);
|
||||
|
||||
let mut max_power = event.content.users_default;
|
||||
for power in event.content.users.values() {
|
||||
max_power = *power.max(&max_power);
|
||||
}
|
||||
|
||||
for user in event.content.users.keys() {
|
||||
if let Some(member) = self.members.get_mut(user) {
|
||||
if member.update_power(event, max_power) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
updated
|
||||
}
|
||||
|
||||
fn handle_tombstone(&mut self, event: &TombstoneEvent) -> bool {
|
||||
self.tombstone = Some(Tombstone {
|
||||
body: event.content.body.clone(),
|
||||
replacement: event.content.replacement_room.clone(),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_encryption_event(&mut self, event: &EncryptionEvent) -> bool {
|
||||
self.encrypted = Some(event.into());
|
||||
true
|
||||
}
|
||||
|
||||
/// Receive a timeline event for this room and update the room state.
|
||||
///
|
||||
/// Returns true if the joined member list changed, false otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event of the room.
|
||||
pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool {
|
||||
match event {
|
||||
// update to the current members of the room
|
||||
RoomEvent::RoomMember(member) => self.handle_membership(member),
|
||||
// finds all events related to the name of the room for later use
|
||||
RoomEvent::RoomName(name) => self.handle_room_name(name),
|
||||
RoomEvent::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias),
|
||||
RoomEvent::RoomAliases(alias) => self.handle_room_aliases(alias),
|
||||
// power levels of the room members
|
||||
RoomEvent::RoomPowerLevels(power) => self.handle_power_level(power),
|
||||
RoomEvent::RoomTombstone(tomb) => self.handle_tombstone(tomb),
|
||||
RoomEvent::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt),
|
||||
#[cfg(feature = "messages")]
|
||||
RoomEvent::RoomMessage(msg) => self.handle_message(msg),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a state event for this room and update the room state.
|
||||
///
|
||||
/// Returns true if the state of the `Room` has changed, false otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event of the room.
|
||||
pub fn receive_state_event(&mut self, event: &StateEvent) -> bool {
|
||||
match event {
|
||||
// update to the current members of the room
|
||||
StateEvent::RoomMember(member) => self.handle_membership(member),
|
||||
// finds all events related to the name of the room for later use
|
||||
StateEvent::RoomName(name) => self.handle_room_name(name),
|
||||
StateEvent::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias),
|
||||
StateEvent::RoomAliases(alias) => self.handle_room_aliases(alias),
|
||||
// power levels of the room members
|
||||
StateEvent::RoomPowerLevels(power) => self.handle_power_level(power),
|
||||
StateEvent::RoomTombstone(tomb) => self.handle_tombstone(tomb),
|
||||
StateEvent::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a stripped state event for this room and update the room state.
|
||||
///
|
||||
/// Returns true if the state of the `Room` has changed, false otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The `AnyStrippedStateEvent` sent by the server for invited but not
|
||||
/// joined rooms.
|
||||
pub fn receive_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
|
||||
match event {
|
||||
AnyStrippedStateEvent::RoomName(n) => self.handle_stripped_room_name(n),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a presence event from an `IncomingResponse` and updates the client state.
|
||||
///
|
||||
/// This will only update the user if found in the current room looped through
|
||||
/// by `Client::sync`.
|
||||
/// Returns true if the specific users presence has changed, false otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The presence event for a specified room member.
|
||||
pub fn receive_presence_event(&mut self, event: &PresenceEvent) -> bool {
|
||||
if let Some(member) = self.members.get_mut(&event.sender) {
|
||||
if member.did_update_presence(event) {
|
||||
false
|
||||
} else {
|
||||
member.update_presence(event);
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// this is probably an error as we have a `PresenceEvent` for a user
|
||||
// we don't know about
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::events::{
|
||||
room::{encryption::EncryptionEventContent, member::MembershipState},
|
||||
UnsignedData,
|
||||
};
|
||||
use crate::identifiers::{EventId, UserId};
|
||||
use crate::{BaseClient, Session};
|
||||
use matrix_sdk_test::{async_test, sync_response, EventBuilder, EventsFile, SyncResponseFile};
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::ops::Deref;
|
||||
|
||||
async fn get_client() -> BaseClient {
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
let client = BaseClient::new().unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
client
|
||||
}
|
||||
|
||||
fn get_room_id() -> RoomId {
|
||||
RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn user_presence() {
|
||||
let client = get_client().await;
|
||||
|
||||
let mut response = sync_response(SyncResponseFile::Default);
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let rooms_lock = &client.joined_rooms();
|
||||
let rooms = rooms_lock.read().await;
|
||||
let room = &rooms
|
||||
.get(&RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap())
|
||||
.unwrap()
|
||||
.read()
|
||||
.await;
|
||||
|
||||
assert_eq!(2, room.members.len());
|
||||
for member in room.members.values() {
|
||||
assert_eq!(MembershipState::Join, member.membership);
|
||||
}
|
||||
|
||||
assert!(room.deref().power_levels.is_some())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn room_events() {
|
||||
let client = get_client().await;
|
||||
let room_id = get_room_id();
|
||||
let user_id = UserId::try_from("@example:localhost").unwrap();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
|
||||
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
let room = room.read().await;
|
||||
|
||||
assert_eq!(room.members.len(), 1);
|
||||
assert!(room.power_levels.is_some());
|
||||
assert_eq!(
|
||||
room.power_levels.as_ref().unwrap().kick,
|
||||
crate::js_int::Int::new(50).unwrap()
|
||||
);
|
||||
let admin = room.members.get(&user_id).unwrap();
|
||||
assert_eq!(
|
||||
admin.power_level.unwrap(),
|
||||
crate::js_int::Int::new(100).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn calculate_aliases() {
|
||||
let client = get_client().await;
|
||||
|
||||
let room_id = get_room_id();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_state_event(EventsFile::Aliases, StateEvent::RoomAliases)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
let room = room.read().await;
|
||||
|
||||
assert_eq!("tutorial", room.display_name());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn calculate_alias() {
|
||||
let client = get_client().await;
|
||||
|
||||
let room_id = get_room_id();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_state_event(EventsFile::Alias, StateEvent::RoomCanonicalAlias)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
let room = room.read().await;
|
||||
|
||||
assert_eq!("tutorial", room.display_name());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn calculate_name() {
|
||||
let client = get_client().await;
|
||||
|
||||
let room_id = get_room_id();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_state_event(EventsFile::Name, StateEvent::RoomName)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
let room = room.read().await;
|
||||
|
||||
assert_eq!("room name", room.display_name());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn calculate_room_names_from_summary() {
|
||||
let mut response = sync_response(SyncResponseFile::DefaultWithSummary);
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
let client = BaseClient::new().unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let mut room_names = vec![];
|
||||
for room in client.joined_rooms().read().await.values() {
|
||||
room_names.push(room.read().await.display_name())
|
||||
}
|
||||
|
||||
assert_eq!(vec!["example, example2"], room_names);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn encryption_info_test() {
|
||||
let mut response = sync_response(SyncResponseFile::DefaultWithSummary);
|
||||
let user_id = UserId::try_from("@example:localhost").unwrap();
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: user_id.clone(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
let client = BaseClient::new().unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let event = EncryptionEvent {
|
||||
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
|
||||
origin_server_ts: SystemTime::now(),
|
||||
sender: user_id,
|
||||
state_key: "".into(),
|
||||
unsigned: UnsignedData::default(),
|
||||
content: EncryptionEventContent {
|
||||
algorithm: Algorithm::MegolmV1AesSha2,
|
||||
rotation_period_ms: Some(100_000u32.into()),
|
||||
rotation_period_msgs: Some(100u32.into()),
|
||||
},
|
||||
prev_content: None,
|
||||
room_id: None,
|
||||
};
|
||||
|
||||
let room_id = get_room_id();
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
|
||||
assert!(!room.read().await.is_encrypted());
|
||||
room.write().await.handle_encryption_event(&event);
|
||||
assert!(room.read().await.is_encrypted());
|
||||
|
||||
let room_lock = room.read().await;
|
||||
let encryption_info = room_lock.encryption_info().unwrap();
|
||||
|
||||
assert_eq!(encryption_info.algorithm(), &Algorithm::MegolmV1AesSha2);
|
||||
assert_eq!(encryption_info.rotation_period(), 100_000);
|
||||
assert_eq!(encryption_info.rotation_period_messages(), 100);
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
// 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.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::events::collections::all::Event;
|
||||
use crate::events::presence::{PresenceEvent, PresenceEventContent, PresenceState};
|
||||
use crate::events::room::{
|
||||
member::{MemberEvent, MembershipChange, MembershipState},
|
||||
power_levels::PowerLevelsEvent,
|
||||
};
|
||||
use crate::identifiers::UserId;
|
||||
|
||||
use crate::js_int::{Int, UInt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob.
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
/// A Matrix room member.
|
||||
///
|
||||
pub struct RoomMember {
|
||||
/// The unique mxid of the user.
|
||||
pub user_id: UserId,
|
||||
/// The human readable name of the user.
|
||||
pub display_name: Option<String>,
|
||||
/// The matrix url of the users avatar.
|
||||
pub avatar_url: Option<String>,
|
||||
/// The time, in ms, since the user interacted with the server.
|
||||
pub last_active_ago: Option<UInt>,
|
||||
/// If the user should be considered active.
|
||||
pub currently_active: Option<bool>,
|
||||
/// The unique id of the room.
|
||||
pub room_id: Option<String>,
|
||||
/// If the member is typing.
|
||||
pub typing: Option<bool>,
|
||||
/// The presence of the user, if found.
|
||||
pub presence: Option<PresenceState>,
|
||||
/// The presence status message, if found.
|
||||
pub status_msg: Option<String>,
|
||||
/// The users power level.
|
||||
pub power_level: Option<Int>,
|
||||
/// The normalized power level of this `RoomMember` (0-100).
|
||||
pub power_level_norm: Option<Int>,
|
||||
/// The `MembershipState` of this `RoomMember`.
|
||||
pub membership: MembershipState,
|
||||
/// The human readable name of this room member.
|
||||
pub name: String,
|
||||
/// The events that created the state of this room member.
|
||||
#[serde(deserialize_with = "super::event_deser::deserialize_events")]
|
||||
pub events: Vec<Event>,
|
||||
/// The `PresenceEvent`s connected to this user.
|
||||
#[serde(deserialize_with = "super::event_deser::deserialize_presence")]
|
||||
pub presence_events: Vec<PresenceEvent>,
|
||||
}
|
||||
|
||||
impl PartialEq for RoomMember {
|
||||
fn eq(&self, other: &RoomMember) -> bool {
|
||||
// TODO check everything but events and presence_events they don't impl PartialEq
|
||||
self.room_id == other.room_id
|
||||
&& self.user_id == other.user_id
|
||||
&& self.name == other.name
|
||||
&& self.display_name == other.display_name
|
||||
&& self.avatar_url == other.avatar_url
|
||||
&& self.last_active_ago == other.last_active_ago
|
||||
&& self.membership == other.membership
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub fn new(event: &MemberEvent) -> Self {
|
||||
Self {
|
||||
name: event.state_key.clone(),
|
||||
room_id: event.room_id.as_ref().map(|id| id.to_string()),
|
||||
user_id: UserId::try_from(event.state_key.as_str()).unwrap(),
|
||||
display_name: event.content.displayname.clone(),
|
||||
avatar_url: event.content.avatar_url.clone(),
|
||||
presence: None,
|
||||
status_msg: None,
|
||||
last_active_ago: None,
|
||||
currently_active: None,
|
||||
typing: None,
|
||||
power_level: None,
|
||||
power_level_norm: None,
|
||||
membership: event.content.membership,
|
||||
presence_events: Vec::default(),
|
||||
events: vec![Event::RoomMember(event.clone())],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_member(&mut self, event: &MemberEvent) -> bool {
|
||||
use MembershipChange::*;
|
||||
|
||||
match event.membership_change() {
|
||||
ProfileChanged => {
|
||||
self.display_name = event.content.displayname.clone();
|
||||
self.avatar_url = event.content.avatar_url.clone();
|
||||
true
|
||||
}
|
||||
Banned | Kicked | KickedAndBanned | InvitationRejected | InvitationRevoked | Left
|
||||
| Unbanned | Joined | Invited => {
|
||||
self.membership = event.content.membership;
|
||||
true
|
||||
}
|
||||
NotImplemented => false,
|
||||
None => false,
|
||||
// we ignore the error here as only a buggy or malicious server would send this
|
||||
Error => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_power(&mut self, event: &PowerLevelsEvent, max_power: Int) -> bool {
|
||||
let changed;
|
||||
if let Some(user_power) = event.content.users.get(&self.user_id) {
|
||||
changed = self.power_level != Some(*user_power);
|
||||
self.power_level = Some(*user_power);
|
||||
} else {
|
||||
changed = self.power_level != Some(event.content.users_default);
|
||||
self.power_level = Some(event.content.users_default);
|
||||
}
|
||||
|
||||
if max_power > Int::from(0) {
|
||||
self.power_level_norm = Some((self.power_level.unwrap() * Int::from(100)) / max_power);
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
/// If the current `PresenceEvent` updated the state of this `User`.
|
||||
///
|
||||
/// Returns true if the specific users presence has changed, false otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `presence` - The presence event for a this room member.
|
||||
pub fn did_update_presence(&self, presence: &PresenceEvent) -> bool {
|
||||
let PresenceEvent {
|
||||
content:
|
||||
PresenceEventContent {
|
||||
avatar_url,
|
||||
currently_active,
|
||||
displayname,
|
||||
last_active_ago,
|
||||
presence,
|
||||
status_msg,
|
||||
},
|
||||
..
|
||||
} = presence;
|
||||
self.display_name == *displayname
|
||||
&& self.avatar_url == *avatar_url
|
||||
&& self.presence.as_ref() == Some(presence)
|
||||
&& self.status_msg == *status_msg
|
||||
&& self.last_active_ago == *last_active_ago
|
||||
&& self.currently_active == *currently_active
|
||||
}
|
||||
|
||||
/// Updates the `User`s presence.
|
||||
///
|
||||
/// This should only be used if `did_update_presence` was true.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `presence` - The presence event for a this room member.
|
||||
pub fn update_presence(&mut self, presence_ev: &PresenceEvent) {
|
||||
let PresenceEvent {
|
||||
content:
|
||||
PresenceEventContent {
|
||||
avatar_url,
|
||||
currently_active,
|
||||
displayname,
|
||||
last_active_ago,
|
||||
presence,
|
||||
status_msg,
|
||||
},
|
||||
..
|
||||
} = presence_ev;
|
||||
|
||||
self.presence_events.push(presence_ev.clone());
|
||||
self.avatar_url = avatar_url.clone();
|
||||
self.currently_active = *currently_active;
|
||||
self.display_name = displayname.clone();
|
||||
self.last_active_ago = *last_active_ago;
|
||||
self.presence = Some(*presence);
|
||||
self.status_msg = status_msg.clone();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use matrix_sdk_test::{async_test, EventBuilder, EventsFile};
|
||||
|
||||
use crate::events::collections::all::RoomEvent;
|
||||
use crate::events::room::member::MembershipState;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use crate::{BaseClient, Session};
|
||||
|
||||
use crate::js_int::Int;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
async fn get_client() -> BaseClient {
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
let client = BaseClient::new().unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
client
|
||||
}
|
||||
|
||||
fn get_room_id() -> RoomId {
|
||||
RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn room_member_events() {
|
||||
let client = get_client().await;
|
||||
|
||||
let room_id = get_room_id();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
|
||||
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
let room = room.read().await;
|
||||
|
||||
let member = room
|
||||
.members
|
||||
.get(&UserId::try_from("@example:localhost").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(member.membership, MembershipState::Join);
|
||||
assert_eq!(member.power_level, Int::new(100));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn member_presence_events() {
|
||||
let client = get_client().await;
|
||||
|
||||
let room_id = get_room_id();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
|
||||
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
|
||||
.add_presence_event(EventsFile::Presence)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
let room = room.read().await;
|
||||
|
||||
let member = room
|
||||
.members
|
||||
.get(&UserId::try_from("@example:localhost").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(member.membership, MembershipState::Join);
|
||||
assert_eq!(member.power_level, Int::new(100));
|
||||
|
||||
assert!(member.avatar_url.is_none());
|
||||
assert_eq!(member.last_active_ago, None);
|
||||
assert_eq!(member.presence, None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruma::{
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
room::{member::MemberEventContent, power_levels::PowerLevelsEventContent},
|
||||
SyncStateEvent,
|
||||
},
|
||||
MxcUri, UserId,
|
||||
};
|
||||
|
||||
use crate::deserialized_responses::MemberEvent;
|
||||
|
||||
/// A member of a room.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RoomMember {
|
||||
pub(crate) event: Arc<MemberEvent>,
|
||||
pub(crate) profile: Arc<Option<MemberEventContent>>,
|
||||
pub(crate) presence: Arc<Option<PresenceEvent>>,
|
||||
pub(crate) power_levels: Arc<Option<SyncStateEvent<PowerLevelsEventContent>>>,
|
||||
pub(crate) max_power_level: i64,
|
||||
pub(crate) is_room_creator: bool,
|
||||
pub(crate) display_name_ambiguous: bool,
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
/// Get the unique user id of this member.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.event.state_key
|
||||
}
|
||||
|
||||
/// 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()
|
||||
} else {
|
||||
self.event.content.displayname.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of the member.
|
||||
///
|
||||
/// This returns either the display name or the local part of the user id if
|
||||
/// the member didn't set a display name.
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(d) = self.display_name() {
|
||||
d
|
||||
} else {
|
||||
self.user_id().localpart()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the normalized power level of this member.
|
||||
///
|
||||
/// The normalized power level depends on the maximum power level that can
|
||||
/// be found in a certain room, it's always in the range of 0-100.
|
||||
pub fn normalized_power_level(&self) -> i64 {
|
||||
if self.max_power_level > 0 {
|
||||
(self.power_level() * 100) / self.max_power_level
|
||||
} else {
|
||||
self.power_level()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the power level of this member.
|
||||
pub fn power_level(&self) -> i64 {
|
||||
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())
|
||||
})
|
||||
.unwrap_or_else(|| if self.is_room_creator { 100 } else { 0 })
|
||||
}
|
||||
|
||||
/// Is the name that the member uses ambiguous in the room.
|
||||
///
|
||||
/// A name is considered to be ambiguous if at least one other member shares
|
||||
/// the same name.
|
||||
pub fn name_ambiguous(&self) -> bool {
|
||||
self.display_name_ambiguous
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
mod members;
|
||||
mod normal;
|
||||
|
||||
use std::cmp::max;
|
||||
|
||||
pub use members::RoomMember;
|
||||
pub use normal::{Room, RoomInfo, RoomType};
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
create::CreateEventContent, encryption::EncryptionEventContent,
|
||||
guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule,
|
||||
tombstone::TombstoneEventContent,
|
||||
},
|
||||
AnyStateEventContent,
|
||||
},
|
||||
MxcUri, RoomAliasId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 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.
|
||||
pub avatar_url: Option<MxcUri>,
|
||||
/// The canonical alias of this room.
|
||||
pub canonical_alias: Option<RoomAliasId>,
|
||||
/// The `m.room.create` event content of this room.
|
||||
pub create: Option<CreateEventContent>,
|
||||
/// The user id this room is sharing the direct message with, if the room is
|
||||
/// a direct message.
|
||||
pub dm_target: Option<UserId>,
|
||||
/// The `m.room.encryption` event content that enabled E2EE in this room.
|
||||
pub encryption: Option<EncryptionEventContent>,
|
||||
/// The guest access policy of this room.
|
||||
pub guest_access: GuestAccess,
|
||||
/// The history visibility policy of this room.
|
||||
pub history_visibility: HistoryVisibility,
|
||||
/// The join rule policy of this room.
|
||||
pub join_rule: JoinRule,
|
||||
/// The maximal power level that can be found in this room.
|
||||
pub max_power_level: i64,
|
||||
/// The `m.room.name` of this room.
|
||||
pub name: Option<String>,
|
||||
/// The `m.room.tombstone` event content of this room.
|
||||
pub tombstone: Option<TombstoneEventContent>,
|
||||
/// The topic of this room.
|
||||
pub topic: Option<String>,
|
||||
}
|
||||
|
||||
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>,
|
||||
) -> String {
|
||||
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, content: &AnyStateEventContent) -> bool {
|
||||
match content {
|
||||
AnyStateEventContent::RoomEncryption(encryption) => {
|
||||
self.encryption = Some(encryption.clone());
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomAvatar(a) => {
|
||||
self.avatar_url = a.url.clone();
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomName(n) => {
|
||||
self.name = n.name().map(|n| n.to_string());
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomCreate(c) => {
|
||||
if self.create.is_none() {
|
||||
self.create = Some(c.clone());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
AnyStateEventContent::RoomHistoryVisibility(h) => {
|
||||
self.history_visibility = h.history_visibility.clone();
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomGuestAccess(g) => {
|
||||
self.guest_access = g.guest_access.clone();
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomJoinRules(c) => {
|
||||
self.join_rule = c.join_rule.clone();
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomCanonicalAlias(a) => {
|
||||
self.canonical_alias = a.alias.clone();
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomTopic(t) => {
|
||||
self.topic = Some(t.topic.clone());
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomTombstone(t) => {
|
||||
self.tombstone = Some(t.clone());
|
||||
true
|
||||
}
|
||||
AnyStateEventContent::RoomPowerLevels(p) => {
|
||||
let max_power_level =
|
||||
p.users.values().fold(self.max_power_level, |acc, p| max(acc, (*p).into()));
|
||||
self.max_power_level = max_power_level;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BaseRoomInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
avatar_url: None,
|
||||
canonical_alias: None,
|
||||
create: None,
|
||||
dm_target: None,
|
||||
encryption: None,
|
||||
guest_access: GuestAccess::CanJoin,
|
||||
history_visibility: HistoryVisibility::WorldReadable,
|
||||
join_rule: JoinRule::Public,
|
||||
max_power_level: 100,
|
||||
name: None,
|
||||
tombstone: None,
|
||||
topic: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate room name according to step 3 of the [naming algorithm.][spec]
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
fn calculate_room_name(
|
||||
joined_member_count: u64,
|
||||
invited_member_count: u64,
|
||||
heroes: Vec<&str>,
|
||||
) -> String {
|
||||
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_string()
|
||||
};
|
||||
|
||||
// User is alone.
|
||||
if invited_joined <= 1 {
|
||||
if names.is_empty() {
|
||||
"Empty room".to_string()
|
||||
} else {
|
||||
format!("Empty room (was {})", names)
|
||||
}
|
||||
} else {
|
||||
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!("a", actual);
|
||||
|
||||
actual = calculate_room_name(3, 0, vec!["a", "b"]);
|
||||
assert_eq!("a, b", actual);
|
||||
|
||||
actual = calculate_room_name(4, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!("a, b, c", actual);
|
||||
|
||||
actual = calculate_room_name(5, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!("a, b, c, and 2 others", actual);
|
||||
|
||||
actual = calculate_room_name(0, 0, vec![]);
|
||||
assert_eq!("Empty room", actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec![]);
|
||||
assert_eq!("Empty room", actual);
|
||||
|
||||
actual = calculate_room_name(0, 1, vec![]);
|
||||
assert_eq!("Empty room", actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a"]);
|
||||
assert_eq!("Empty room (was a)", actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a", "b"]);
|
||||
assert_eq!("Empty room (was a, b)", actual);
|
||||
|
||||
actual = calculate_room_name(1, 0, vec!["a", "b", "c"]);
|
||||
assert_eq!("Empty room (was a, b, c)", actual);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
sync::{Arc, RwLock as SyncRwLock},
|
||||
};
|
||||
|
||||
use futures::{
|
||||
future,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use ruma::{
|
||||
api::client::r0::sync::sync_events::RoomSummary as RumaSummary,
|
||||
events::{
|
||||
receipt::Receipt,
|
||||
room::{
|
||||
create::CreateEventContent, encryption::EncryptionEventContent,
|
||||
guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule,
|
||||
tombstone::TombstoneEventContent,
|
||||
},
|
||||
tag::Tags,
|
||||
AnyRoomAccountDataEvent, AnyStateEventContent, AnySyncStateEvent, EventType,
|
||||
},
|
||||
receipt::ReceiptType,
|
||||
EventId, MxcUri, RoomAliasId, RoomId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
use super::{BaseRoomInfo, RoomMember};
|
||||
use crate::{
|
||||
deserialized_responses::UnreadNotificationsCount,
|
||||
store::{Result as StoreResult, StateStore},
|
||||
};
|
||||
|
||||
/// The underlying room data structure collecting state for joined, left and
|
||||
/// invited rooms.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Room {
|
||||
room_id: Arc<RoomId>,
|
||||
own_user_id: Arc<UserId>,
|
||||
inner: Arc<SyncRwLock<RoomInfo>>,
|
||||
store: Arc<dyn StateStore>,
|
||||
}
|
||||
|
||||
/// The room summary containing member counts and members that should be used to
|
||||
/// calculate the room display name.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct RoomSummary {
|
||||
/// The heroes of the room, members that should be used for the room display
|
||||
/// name.
|
||||
heroes: Vec<String>,
|
||||
/// The number of members that are considered to be joined to the room.
|
||||
joined_member_count: u64,
|
||||
/// The number of members that are considered to be invited to the room.
|
||||
invited_member_count: u64,
|
||||
}
|
||||
|
||||
/// Enum keeping track in which state the room is, e.g. if our own user is
|
||||
/// joined, invited, or has left the room.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RoomType {
|
||||
/// The room is in a joined state.
|
||||
Joined,
|
||||
/// The room is in a left state.
|
||||
Left,
|
||||
/// The room is in a invited state.
|
||||
Invited,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
pub(crate) fn new(
|
||||
own_user_id: &UserId,
|
||||
store: Arc<dyn StateStore>,
|
||||
room_id: &RoomId,
|
||||
room_type: RoomType,
|
||||
) -> Self {
|
||||
let room_id = Arc::new(room_id.clone());
|
||||
|
||||
let room_info = RoomInfo {
|
||||
room_id,
|
||||
room_type,
|
||||
notification_counts: Default::default(),
|
||||
summary: Default::default(),
|
||||
members_synced: false,
|
||||
last_prev_batch: None,
|
||||
base_info: BaseRoomInfo::new(),
|
||||
};
|
||||
|
||||
Self::restore(own_user_id, store, room_info)
|
||||
}
|
||||
|
||||
pub(crate) fn restore(
|
||||
own_user_id: &UserId,
|
||||
store: Arc<dyn StateStore>,
|
||||
room_info: RoomInfo,
|
||||
) -> Self {
|
||||
Self {
|
||||
own_user_id: Arc::new(own_user_id.clone()),
|
||||
room_id: room_info.room_id.clone(),
|
||||
store,
|
||||
inner: Arc::new(SyncRwLock::new(room_info)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the unique room id of the room.
|
||||
pub fn room_id(&self) -> &RoomId {
|
||||
&self.room_id
|
||||
}
|
||||
|
||||
/// Get our own user id.
|
||||
pub fn own_user_id(&self) -> &UserId {
|
||||
&self.own_user_id
|
||||
}
|
||||
|
||||
/// Get the type of the room.
|
||||
pub fn room_type(&self) -> RoomType {
|
||||
self.inner.read().unwrap().room_type
|
||||
}
|
||||
|
||||
/// Get the unread notification counts.
|
||||
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
|
||||
self.inner.read().unwrap().notification_counts
|
||||
}
|
||||
|
||||
/// Check if the room has it's members fully synced.
|
||||
///
|
||||
/// Members might be missing if lazy member loading was enabled for the
|
||||
/// sync.
|
||||
///
|
||||
/// Returns true if no members are missing, false otherwise.
|
||||
pub fn are_members_synced(&self) -> bool {
|
||||
self.inner.read().unwrap().members_synced
|
||||
}
|
||||
|
||||
/// Get the `prev_batch` token that was received from the last sync. May be
|
||||
/// `None` if the last sync contained the full room history.
|
||||
pub fn last_prev_batch(&self) -> Option<String> {
|
||||
self.inner.read().unwrap().last_prev_batch.clone()
|
||||
}
|
||||
|
||||
/// Get the avatar url of this room.
|
||||
pub fn avatar_url(&self) -> Option<MxcUri> {
|
||||
self.inner.read().unwrap().base_info.avatar_url.clone()
|
||||
}
|
||||
|
||||
/// Get the canonical alias of this room.
|
||||
pub fn canonical_alias(&self) -> Option<RoomAliasId> {
|
||||
self.inner.read().unwrap().base_info.canonical_alias.clone()
|
||||
}
|
||||
|
||||
/// Get the `m.room.create` content of this room.
|
||||
///
|
||||
/// This usually isn't optional but some servers might not send an
|
||||
/// `m.room.create` event as the first event for a given room, thus this can
|
||||
/// be optional.
|
||||
pub fn create_content(&self) -> Option<CreateEventContent> {
|
||||
self.inner.read().unwrap().base_info.create.clone()
|
||||
}
|
||||
|
||||
/// Is this room considered a direct message.
|
||||
pub fn is_direct(&self) -> bool {
|
||||
self.inner.read().unwrap().base_info.dm_target.is_some()
|
||||
}
|
||||
|
||||
/// If this room is a direct message, get the member that we're sharing the
|
||||
/// room with.
|
||||
///
|
||||
/// *Note*: The member list might have been modified in the meantime and
|
||||
/// the target might not even be in the room anymore. This setting should
|
||||
/// only be considered as guidance.
|
||||
pub fn direct_target(&self) -> Option<UserId> {
|
||||
self.inner.read().unwrap().base_info.dm_target.clone()
|
||||
}
|
||||
|
||||
/// Is the room encrypted.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.inner.read().unwrap().is_encrypted()
|
||||
}
|
||||
|
||||
/// Get the `m.room.encryption` content that enabled end to end encryption
|
||||
/// in the room.
|
||||
pub fn encryption_settings(&self) -> Option<EncryptionEventContent> {
|
||||
self.inner.read().unwrap().base_info.encryption.clone()
|
||||
}
|
||||
|
||||
/// Get the guest access policy of this room.
|
||||
pub fn guest_access(&self) -> GuestAccess {
|
||||
self.inner.read().unwrap().base_info.guest_access.clone()
|
||||
}
|
||||
|
||||
/// Get the history visibility policy of this room.
|
||||
pub fn history_visibility(&self) -> HistoryVisibility {
|
||||
self.inner.read().unwrap().base_info.history_visibility.clone()
|
||||
}
|
||||
|
||||
/// Is the room considered to be public.
|
||||
pub fn is_public(&self) -> bool {
|
||||
matches!(self.join_rule(), JoinRule::Public)
|
||||
}
|
||||
|
||||
/// Get the join rule policy of this room.
|
||||
pub fn join_rule(&self) -> JoinRule {
|
||||
self.inner.read().unwrap().base_info.join_rule.clone()
|
||||
}
|
||||
|
||||
/// Get the maximum power level that this room contains.
|
||||
///
|
||||
/// This is useful if one wishes to normalize the power levels, e.g. from
|
||||
/// 0-100 where 100 would be the max power level.
|
||||
pub fn max_power_level(&self) -> i64 {
|
||||
self.inner.read().unwrap().base_info.max_power_level
|
||||
}
|
||||
|
||||
/// Get the `m.room.name` of this room.
|
||||
pub fn name(&self) -> Option<String> {
|
||||
self.inner.read().unwrap().base_info.name.clone()
|
||||
}
|
||||
|
||||
/// Has the room been tombstoned.
|
||||
pub fn is_tombstoned(&self) -> bool {
|
||||
self.inner.read().unwrap().base_info.tombstone.is_some()
|
||||
}
|
||||
|
||||
/// Get the `m.room.tombstone` content of this room if there is one.
|
||||
pub fn tombstone(&self) -> Option<TombstoneEventContent> {
|
||||
self.inner.read().unwrap().base_info.tombstone.clone()
|
||||
}
|
||||
|
||||
/// Get the topic of the room.
|
||||
pub fn topic(&self) -> Option<String> {
|
||||
self.inner.read().unwrap().base_info.topic.clone()
|
||||
}
|
||||
|
||||
/// Calculate the canonical display name of the room, taking into account
|
||||
/// its name, aliases and members.
|
||||
///
|
||||
/// The display name is calculated according to [this algorithm][spec].
|
||||
///
|
||||
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
|
||||
pub async fn display_name(&self) -> StoreResult<String> {
|
||||
self.calculate_name().await
|
||||
}
|
||||
|
||||
/// Get the list of users ids that are considered to be joined members of
|
||||
/// this room.
|
||||
pub async fn joined_user_ids(&self) -> StoreResult<Vec<UserId>> {
|
||||
self.store.get_joined_user_ids(self.room_id()).await
|
||||
}
|
||||
|
||||
/// Get the all `RoomMember`s of this room that are known to the store.
|
||||
pub async fn members(&self) -> StoreResult<Vec<RoomMember>> {
|
||||
let user_ids = self.store.get_user_ids(self.room_id()).await?;
|
||||
let mut members = Vec::new();
|
||||
|
||||
for u in user_ids {
|
||||
let m = self.get_member(&u).await?;
|
||||
|
||||
if let Some(member) = m {
|
||||
members.push(member);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
/// Get the list of `RoomMember`s that are considered to be joined members
|
||||
/// of this room.
|
||||
pub async fn joined_members(&self) -> StoreResult<Vec<RoomMember>> {
|
||||
let joined = self.store.get_joined_user_ids(self.room_id()).await?;
|
||||
let mut members = Vec::new();
|
||||
|
||||
for u in joined {
|
||||
let m = self.get_member(&u).await?;
|
||||
|
||||
if let Some(member) = m {
|
||||
members.push(member);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
/// Get the list of `RoomMember`s that are considered to be joined or
|
||||
/// invited members of this room.
|
||||
pub async fn active_members(&self) -> StoreResult<Vec<RoomMember>> {
|
||||
let joined = self.store.get_joined_user_ids(self.room_id()).await?;
|
||||
let invited = self.store.get_invited_user_ids(self.room_id()).await?;
|
||||
|
||||
let mut members = Vec::new();
|
||||
|
||||
for u in joined.iter().chain(&invited) {
|
||||
let m = self.get_member(u).await?;
|
||||
|
||||
if let Some(member) = m {
|
||||
members.push(member);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
async fn calculate_name(&self) -> StoreResult<String> {
|
||||
let summary = {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
if let Some(name) = &inner.base_info.name {
|
||||
let name = name.trim();
|
||||
return Ok(name.to_string());
|
||||
} else if let Some(alias) = &inner.base_info.canonical_alias {
|
||||
let alias = alias.alias().trim();
|
||||
return Ok(alias.to_string());
|
||||
}
|
||||
inner.summary.clone()
|
||||
};
|
||||
// TODO what should we do here? We have correct counts only if lazy
|
||||
// loading is used.
|
||||
let joined = summary.joined_member_count;
|
||||
let invited = summary.invited_member_count;
|
||||
let heroes_count = summary.heroes.len() as u64;
|
||||
|
||||
let is_own_member = |m: &RoomMember| m.user_id() == &*self.own_user_id;
|
||||
let is_own_user_id = |u: &str| u == self.own_user_id().as_str();
|
||||
|
||||
let members: Vec<RoomMember> = if summary.heroes.is_empty() {
|
||||
self.active_members().await?.into_iter().filter(|u| !is_own_member(u)).take(5).collect()
|
||||
} else {
|
||||
let members: Vec<_> = stream::iter(summary.heroes.iter())
|
||||
.filter(|u| future::ready(!is_own_user_id(u)))
|
||||
.filter_map(|u| async move {
|
||||
let user_id = UserId::try_from(u.as_str()).ok()?;
|
||||
self.get_member(&user_id).await.transpose()
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let members: StoreResult<Vec<_>> = members.into_iter().collect();
|
||||
|
||||
members?
|
||||
};
|
||||
|
||||
info!(
|
||||
"Calculating name for {}, own user {} hero count {} heroes {:#?}",
|
||||
self.room_id(),
|
||||
self.own_user_id,
|
||||
heroes_count,
|
||||
summary.heroes
|
||||
);
|
||||
|
||||
let inner = self.inner.read().unwrap();
|
||||
Ok(inner.base_info.calculate_room_name(joined, invited, members))
|
||||
}
|
||||
|
||||
pub(crate) fn clone_info(&self) -> RoomInfo {
|
||||
(*self.inner.read().unwrap()).clone()
|
||||
}
|
||||
|
||||
pub(crate) fn update_summary(&self, summary: RoomInfo) {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
*inner = summary;
|
||||
}
|
||||
|
||||
/// Get the `RoomMember` with the given `user_id`.
|
||||
///
|
||||
/// Returns `None` if the member was never part of this room, otherwise
|
||||
/// return a `RoomMember` that can be in a joined, invited, left, banned
|
||||
/// state.
|
||||
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
|
||||
let member_event =
|
||||
if let Some(m) = self.store.get_member_event(self.room_id(), user_id).await? {
|
||||
m
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let presence =
|
||||
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
|
||||
let profile = self.store.get_profile(self.room_id(), user_id).await?;
|
||||
let max_power_level = self.max_power_level();
|
||||
let is_room_creator = self
|
||||
.inner
|
||||
.read()
|
||||
.unwrap()
|
||||
.base_info
|
||||
.create
|
||||
.as_ref()
|
||||
.map(|c| &c.creator == user_id)
|
||||
.unwrap_or(false);
|
||||
|
||||
let power =
|
||||
self.store
|
||||
.get_state_event(self.room_id(), EventType::RoomPowerLevels, "")
|
||||
.await?
|
||||
.and_then(|e| e.deserialize().ok())
|
||||
.and_then(|e| {
|
||||
if let AnySyncStateEvent::RoomPowerLevels(e) = e {
|
||||
Some(e)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let ambiguous = self
|
||||
.store
|
||||
.get_users_with_display_name(
|
||||
self.room_id(),
|
||||
member_event.content.displayname.as_deref().unwrap_or_else(|| user_id.localpart()),
|
||||
)
|
||||
.await?
|
||||
.len()
|
||||
> 1;
|
||||
|
||||
Ok(Some(RoomMember {
|
||||
event: member_event.into(),
|
||||
profile: profile.into(),
|
||||
presence: presence.into(),
|
||||
power_levels: power.into(),
|
||||
max_power_level,
|
||||
is_room_creator,
|
||||
display_name_ambiguous: ambiguous,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get the `Tags` for this room.
|
||||
pub async fn tags(&self) -> StoreResult<Option<Tags>> {
|
||||
if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
|
||||
.store
|
||||
.get_room_account_data_event(self.room_id(), EventType::Tag)
|
||||
.await?
|
||||
.and_then(|r| r.deserialize().ok())
|
||||
{
|
||||
Ok(Some(event.content.tags))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the read receipt as a `EventId` and `Receipt` tuple for the given
|
||||
/// `user_id` in this room.
|
||||
pub async fn user_read_receipt(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> StoreResult<Option<(EventId, Receipt)>> {
|
||||
self.store.get_user_room_receipt_event(self.room_id(), ReceiptType::Read, user_id).await
|
||||
}
|
||||
|
||||
/// Get the read receipts as a list of `UserId` and `Receipt` tuples for the
|
||||
/// given `event_id` in this room.
|
||||
pub async fn event_read_receipts(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
) -> StoreResult<Vec<(UserId, Receipt)>> {
|
||||
self.store.get_event_room_receipt_events(self.room_id(), ReceiptType::Read, event_id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// The underlying pure data structure for joined and left rooms.
|
||||
///
|
||||
/// Holds all the info needed to persist a room into the state store.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RoomInfo {
|
||||
/// The unique room id of the room.
|
||||
pub room_id: Arc<RoomId>,
|
||||
/// The type of the room.
|
||||
pub room_type: RoomType,
|
||||
/// The unread notifications counts.
|
||||
pub notification_counts: UnreadNotificationsCount,
|
||||
/// The summary of this room.
|
||||
pub summary: RoomSummary,
|
||||
/// Flag remembering if the room members are synced.
|
||||
pub members_synced: bool,
|
||||
/// The prev batch of this room we received during the last sync.
|
||||
pub last_prev_batch: Option<String>,
|
||||
/// Base room info which holds some basic event contents important for the
|
||||
/// room state.
|
||||
pub base_info: BaseRoomInfo,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
pub(crate) fn mark_as_joined(&mut self) {
|
||||
self.room_type = RoomType::Joined;
|
||||
}
|
||||
|
||||
pub(crate) fn mark_as_left(&mut self) {
|
||||
self.room_type = RoomType::Left;
|
||||
}
|
||||
|
||||
pub(crate) fn mark_as_invited(&mut self) {
|
||||
self.room_type = RoomType::Invited;
|
||||
}
|
||||
|
||||
pub(crate) fn mark_members_synced(&mut self) {
|
||||
self.members_synced = true;
|
||||
}
|
||||
|
||||
pub(crate) fn mark_members_missing(&mut self) {
|
||||
self.members_synced = false;
|
||||
}
|
||||
|
||||
pub(crate) fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
|
||||
if self.last_prev_batch.as_deref() != prev_batch {
|
||||
self.last_prev_batch = prev_batch.map(|p| p.to_string());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_encrypted(&self) -> bool {
|
||||
self.base_info.encryption.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_state_event(&mut self, event: &AnyStateEventContent) -> bool {
|
||||
self.base_info.handle_state_event(event)
|
||||
}
|
||||
|
||||
pub(crate) fn update_notification_count(
|
||||
&mut self,
|
||||
notification_counts: UnreadNotificationsCount,
|
||||
) {
|
||||
self.notification_counts = notification_counts;
|
||||
}
|
||||
|
||||
pub(crate) fn update_summary(&mut self, summary: &RumaSummary) -> bool {
|
||||
let mut changed = false;
|
||||
|
||||
if !summary.is_empty() {
|
||||
if !summary.heroes.is_empty() {
|
||||
self.summary.heroes = summary.heroes.clone();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if let Some(joined) = summary.joined_member_count {
|
||||
self.summary.joined_member_count = joined.into();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if let Some(invited) = summary.invited_member_count {
|
||||
self.summary.invited_member_count = invited.into();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
/// The number of active members (invited + joined) in the room.
|
||||
///
|
||||
/// The return value is saturated at `u64::MAX`.
|
||||
pub fn active_members_count(&self) -> u64 {
|
||||
self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,17 @@
|
||||
|
||||
//! User sessions.
|
||||
|
||||
use crate::identifiers::UserId;
|
||||
use ruma::{DeviceId, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A user session, containing an access token and information about the
|
||||
/// associated user account.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
#[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,
|
||||
/// The ID of the client device
|
||||
pub device_id: String,
|
||||
pub device_id: Box<DeviceId>,
|
||||
}
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::identifiers::RoomId;
|
||||
use matrix_sdk_common::locks::RwLock;
|
||||
use tokio::fs as async_fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use super::{AllRooms, ClientState, StateStore};
|
||||
use crate::{Error, Result, Room, RoomState, Session};
|
||||
|
||||
/// A default `StateStore` implementation that serializes state as json
|
||||
/// and saves it to disk.
|
||||
///
|
||||
/// When logged in the `JsonStore` appends the user_id to it's folder path,
|
||||
/// so all files are saved in `my_client/user_id_localpart/*`.
|
||||
pub struct JsonStore {
|
||||
path: Arc<RwLock<PathBuf>>,
|
||||
user_path_set: AtomicBool,
|
||||
}
|
||||
|
||||
impl JsonStore {
|
||||
/// Create a `JsonStore` to store the client and room state.
|
||||
///
|
||||
/// Checks if the provided path exists and creates the directories if not.
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let p = path.as_ref();
|
||||
if !p.exists() {
|
||||
fs::create_dir_all(p)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: Arc::new(RwLock::new(p.to_path_buf())),
|
||||
user_path_set: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for JsonStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("JsonStore")
|
||||
.field("path", &self.path)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl StateStore for JsonStore {
|
||||
async fn load_client_state(&self, sess: &Session) -> Result<Option<ClientState>> {
|
||||
if !self.user_path_set.load(Ordering::SeqCst) {
|
||||
self.user_path_set.swap(true, Ordering::SeqCst);
|
||||
self.path.write().await.push(sess.user_id.localpart())
|
||||
}
|
||||
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("client.json");
|
||||
|
||||
let json = async_fs::read_to_string(path)
|
||||
.await
|
||||
.map_or(String::default(), |s| s);
|
||||
if json.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
serde_json::from_str(&json).map(Some).map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_all_rooms(&self) -> Result<AllRooms> {
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("rooms");
|
||||
|
||||
let mut joined = HashMap::new();
|
||||
let mut left = HashMap::new();
|
||||
let mut invited = HashMap::new();
|
||||
for room_state_type in &["joined", "invited", "left"] {
|
||||
path.push(room_state_type);
|
||||
// don't load rooms that aren't saved yet
|
||||
if !path.exists() {
|
||||
path.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
for file in fs::read_dir(&path)? {
|
||||
let file = file?.path();
|
||||
|
||||
if file.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let json = async_fs::read_to_string(&file).await?;
|
||||
|
||||
let room = serde_json::from_str::<Room>(&json).map_err(Error::from)?;
|
||||
let room_id = room.room_id.clone();
|
||||
|
||||
match *room_state_type {
|
||||
"joined" => joined.insert(room_id, room),
|
||||
"invited" => invited.insert(room_id, room),
|
||||
"left" => left.insert(room_id, room),
|
||||
_ => unreachable!("an array with 3 const elements was altered in JsonStore"),
|
||||
};
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
|
||||
Ok(AllRooms {
|
||||
joined,
|
||||
left,
|
||||
invited,
|
||||
})
|
||||
}
|
||||
|
||||
async fn store_client_state(&self, state: ClientState) -> Result<()> {
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("client.json");
|
||||
|
||||
if !path.exists() {
|
||||
let mut dir = path.clone();
|
||||
dir.pop();
|
||||
async_fs::create_dir_all(dir).await?;
|
||||
}
|
||||
|
||||
let json = serde_json::to_string(&state).map_err(Error::from)?;
|
||||
|
||||
let mut file = async_fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.await?;
|
||||
file.write_all(json.as_bytes()).await.map_err(Error::from)
|
||||
}
|
||||
|
||||
async fn store_room_state(&self, room: RoomState<&Room>) -> Result<()> {
|
||||
let (room, room_state) = match room {
|
||||
RoomState::Joined(room) => (room, "joined"),
|
||||
RoomState::Invited(room) => (room, "invited"),
|
||||
RoomState::Left(room) => (room, "left"),
|
||||
};
|
||||
|
||||
if !self.user_path_set.load(Ordering::SeqCst) {
|
||||
self.user_path_set.swap(true, Ordering::SeqCst);
|
||||
self.path.write().await.push(room.own_user_id.localpart())
|
||||
}
|
||||
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("rooms");
|
||||
path.push(&format!("{}/{}.json", room_state, room.room_id));
|
||||
|
||||
if !path.exists() {
|
||||
let mut dir = path.clone();
|
||||
dir.pop();
|
||||
async_fs::create_dir_all(dir).await?;
|
||||
}
|
||||
|
||||
let json = serde_json::to_string(&room).map_err(Error::from)?;
|
||||
|
||||
let mut file = async_fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.await?;
|
||||
file.write_all(json.as_bytes()).await.map_err(Error::from)
|
||||
}
|
||||
|
||||
async fn delete_room_state(&self, room: RoomState<&RoomId>) -> Result<()> {
|
||||
let (room_id, room_state) = match &room {
|
||||
RoomState::Joined(id) => (id, "joined"),
|
||||
RoomState::Invited(id) => (id, "invited"),
|
||||
RoomState::Left(id) => (id, "left"),
|
||||
};
|
||||
|
||||
if !self.user_path_set.load(Ordering::SeqCst) {
|
||||
return Err(Error::StateStore("path for JsonStore not set".into()));
|
||||
}
|
||||
|
||||
let mut to_del = self.path.read().await.clone();
|
||||
to_del.push("rooms");
|
||||
to_del.push(&format!("{}/{}.json", room_state, room_id));
|
||||
|
||||
if !to_del.exists() {
|
||||
return Err(Error::StateStore(format!("file {:?} not found", to_del)));
|
||||
}
|
||||
|
||||
tokio::fs::remove_file(to_del).await.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use http::Response;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::api::r0::sync::sync_events::Response as SyncResponse;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use crate::{BaseClient, BaseClientConfig, Session};
|
||||
|
||||
fn sync_response(file: &str) -> SyncResponse {
|
||||
let mut file = File::open(file).unwrap();
|
||||
let mut data = vec![];
|
||||
file.read_to_end(&mut data).unwrap();
|
||||
let response = Response::builder().body(data).unwrap();
|
||||
SyncResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_client_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let sess = Session {
|
||||
access_token: "32nj9zu034btz90".to_string(),
|
||||
user_id: user.clone(),
|
||||
device_id: "Tester".to_string(),
|
||||
};
|
||||
|
||||
let state = ClientState {
|
||||
sync_token: Some("hello".into()),
|
||||
ignored_users: vec![user],
|
||||
push_ruleset: None,
|
||||
};
|
||||
|
||||
let mut path_with_user = PathBuf::from(path);
|
||||
path_with_user.push(sess.user_id.localpart());
|
||||
// we have to set the path since `JsonStore::store_client_state()` doesn't append to the path
|
||||
let store = JsonStore::open(path_with_user).unwrap();
|
||||
store.store_client_state(state.clone()).await.unwrap();
|
||||
|
||||
// the newly loaded store sets it own user_id local part when `load_client_state`
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
let loaded = store.load_client_state(&sess).await.unwrap();
|
||||
assert_eq!(loaded, Some(state));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_load_joined_room_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
.store_room_state(RoomState::Joined(&room))
|
||||
.await
|
||||
.unwrap();
|
||||
let AllRooms { joined, .. } = store.load_all_rooms().await.unwrap();
|
||||
assert_eq!(joined.get(&id), Some(&Room::new(&id, &user)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_load_left_room_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
.store_room_state(RoomState::Left(&room))
|
||||
.await
|
||||
.unwrap();
|
||||
let AllRooms { left, .. } = store.load_all_rooms().await.unwrap();
|
||||
assert_eq!(left.get(&id), Some(&Room::new(&id, &user)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_load_invited_room_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
.store_room_state(RoomState::Invited(&room))
|
||||
.await
|
||||
.unwrap();
|
||||
let AllRooms { invited, .. } = store.load_all_rooms().await.unwrap();
|
||||
assert_eq!(invited.get(&id), Some(&Room::new(&id, &user)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_load_join_leave_room_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
.store_room_state(RoomState::Joined(&room))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store
|
||||
.delete_room_state(RoomState::Joined(&id))
|
||||
.await
|
||||
.is_ok());
|
||||
let AllRooms { joined, .. } = store.load_all_rooms().await.unwrap();
|
||||
|
||||
// test that we have removed the correct room
|
||||
assert!(joined.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_load_invite_join_room_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
.store_room_state(RoomState::Invited(&room))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store
|
||||
.delete_room_state(RoomState::Invited(&id))
|
||||
.await
|
||||
.is_ok());
|
||||
let AllRooms { invited, .. } = store.load_all_rooms().await.unwrap();
|
||||
// test that we have removed the correct room
|
||||
assert!(invited.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_client_sync_store() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@cheeky_monkey:matrix.org").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
|
||||
// a sync response to populate our JSON store
|
||||
let store = Box::new(JsonStore::open(path).unwrap());
|
||||
let client =
|
||||
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
|
||||
client.restore_login(session.clone()).await.unwrap();
|
||||
|
||||
let mut response = sync_response("../test_data/sync.json");
|
||||
|
||||
// gather state to save to the db, the first time through loading will be skipped
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
// now syncing the client will update from the state store
|
||||
let store = Box::new(JsonStore::open(path).unwrap());
|
||||
let client =
|
||||
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
|
||||
client.restore_login(session.clone()).await.unwrap();
|
||||
|
||||
// assert the synced client and the logged in client are equal
|
||||
assert_eq!(*client.session().read().await, Some(session));
|
||||
assert_eq!(
|
||||
client.sync_token().await,
|
||||
Some("s526_47314_0_7_1_1_1_11444_1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
*client.ignored_users.read().await,
|
||||
vec![UserId::try_from("@someone:example.org").unwrap()]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
// Copyright 2020 Devin Ragotzy
|
||||
// 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.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod json_store;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use json_store::JsonStore;
|
||||
|
||||
use crate::client::{BaseClient, Token};
|
||||
use crate::events::push_rules::Ruleset;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use crate::{Result, Room, RoomState, Session};
|
||||
|
||||
/// `ClientState` holds all the information to restore a `BaseClient`
|
||||
/// except the `access_token` as the default store is not secure.
|
||||
///
|
||||
/// When implementing `StateStore` for something other than the filesystem
|
||||
/// implement `From<ClientState> for YourDbType` this allows for easy conversion
|
||||
/// when needed in `StateStore::load/store_client_state`
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ClientState {
|
||||
/// The current sync token that should be used for the next sync call.
|
||||
pub sync_token: Option<Token>,
|
||||
/// A list of ignored users.
|
||||
pub ignored_users: Vec<UserId>,
|
||||
/// The push ruleset for the logged in user.
|
||||
pub push_ruleset: Option<Ruleset>,
|
||||
}
|
||||
|
||||
impl PartialEq for ClientState {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.sync_token == other.sync_token && self.ignored_users == other.ignored_users
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
/// Create a JSON serialize-able `ClientState`.
|
||||
///
|
||||
/// This enables non sensitive information to be saved by `JsonStore`.
|
||||
#[allow(clippy::eval_order_dependence)]
|
||||
// TODO is this ok ^^^?? https://github.com/rust-lang/rust-clippy/issues/4637
|
||||
pub async fn from_base_client(client: &BaseClient) -> ClientState {
|
||||
let BaseClient {
|
||||
sync_token,
|
||||
ignored_users,
|
||||
push_ruleset,
|
||||
..
|
||||
} = client;
|
||||
Self {
|
||||
sync_token: sync_token.read().await.clone(),
|
||||
ignored_users: ignored_users.read().await.clone(),
|
||||
push_ruleset: push_ruleset.read().await.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `JsonStore::load_all_rooms` returns `AllRooms`.
|
||||
///
|
||||
/// `AllRooms` is made of the `joined`, `invited` and `left` room maps.
|
||||
#[derive(Debug)]
|
||||
pub struct AllRooms {
|
||||
/// The joined room mapping of `RoomId` to `Room`.
|
||||
pub joined: HashMap<RoomId, Room>,
|
||||
/// The invited room mapping of `RoomId` to `Room`.
|
||||
pub invited: HashMap<RoomId, Room>,
|
||||
/// The left room mapping of `RoomId` to `Room`.
|
||||
pub left: HashMap<RoomId, Room>,
|
||||
}
|
||||
|
||||
/// Abstraction around the data store to avoid unnecessary request on client initialization.
|
||||
#[async_trait::async_trait]
|
||||
pub trait StateStore: Send + Sync {
|
||||
/// Loads the state of `BaseClient` through `ClientState` type.
|
||||
///
|
||||
/// An `Option::None` should be returned only if the `StateStore` tries to
|
||||
/// load but no state has been stored.
|
||||
async fn load_client_state(&self, _: &Session) -> Result<Option<ClientState>>;
|
||||
|
||||
/// Load the state of all `Room`s.
|
||||
///
|
||||
/// This will be mapped over in the client in order to store `Room`s in an async safe way.
|
||||
async fn load_all_rooms(&self) -> Result<AllRooms>;
|
||||
|
||||
/// Save the current state of the `BaseClient` using the `StateStore::Store` type.
|
||||
async fn store_client_state(&self, _: ClientState) -> Result<()>;
|
||||
|
||||
/// Save the state a single `Room`.
|
||||
async fn store_room_state(&self, _: RoomState<&Room>) -> Result<()>;
|
||||
|
||||
/// Remove state for a room.
|
||||
///
|
||||
/// This is used when a user leaves a room or rejects an invitation.
|
||||
async fn delete_room_state(&self, _room: RoomState<&RoomId>) -> Result<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::identifiers::RoomId;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
|
||||
let state = ClientState {
|
||||
sync_token: Some("hello".into()),
|
||||
ignored_users: vec![user],
|
||||
push_ruleset: None,
|
||||
};
|
||||
assert_eq!(
|
||||
r#"{"sync_token":"hello","ignored_users":["@example:example.com"],"push_ruleset":null}"#,
|
||||
serde_json::to_string(&state).unwrap()
|
||||
);
|
||||
|
||||
let mut joined_rooms = HashMap::new();
|
||||
joined_rooms.insert(id, room);
|
||||
|
||||
#[cfg(not(feature = "messages"))]
|
||||
assert_eq!(
|
||||
r#"{
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
}"#,
|
||||
serde_json::to_string_pretty(&joined_rooms).unwrap()
|
||||
);
|
||||
|
||||
#[cfg(feature = "messages")]
|
||||
assert_eq!(
|
||||
r#"{
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"messages": [],
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
}"#,
|
||||
serde_json::to_string_pretty(&joined_rooms).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
|
||||
let state = ClientState {
|
||||
sync_token: Some("hello".into()),
|
||||
ignored_users: vec![user],
|
||||
push_ruleset: None,
|
||||
};
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
|
||||
assert_eq!(state, serde_json::from_str(&json).unwrap());
|
||||
|
||||
let mut joined_rooms = HashMap::new();
|
||||
joined_rooms.insert(id, room);
|
||||
let json = serde_json::to_string(&joined_rooms).unwrap();
|
||||
|
||||
assert_eq!(joined_rooms, serde_json::from_str(&json).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// 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.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use matrix_sdk_common::deserialized_responses::{AmbiguityChange, MemberEvent};
|
||||
use ruma::{events::room::member::MembershipState, EventId, RoomId, UserId};
|
||||
use tracing::trace;
|
||||
|
||||
use super::{Result, StateChanges};
|
||||
use crate::Store;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AmbiguityCache {
|
||||
pub store: Store,
|
||||
pub cache: BTreeMap<RoomId, BTreeMap<String, BTreeSet<UserId>>>,
|
||||
pub changes: BTreeMap<RoomId, BTreeMap<EventId, AmbiguityChange>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AmbiguityMap {
|
||||
display_name: String,
|
||||
users: BTreeSet<UserId>,
|
||||
}
|
||||
|
||||
impl AmbiguityMap {
|
||||
fn remove(&mut self, user_id: &UserId) -> Option<UserId> {
|
||||
self.users.remove(user_id);
|
||||
|
||||
if self.user_count() == 1 {
|
||||
self.users.iter().next().cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, user_id: UserId) -> Option<UserId> {
|
||||
let ambiguous_user =
|
||||
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
|
||||
|
||||
self.users.insert(user_id);
|
||||
|
||||
ambiguous_user
|
||||
}
|
||||
|
||||
fn user_count(&self) -> usize {
|
||||
self.users.len()
|
||||
}
|
||||
|
||||
fn is_ambiguous(&self) -> bool {
|
||||
self.user_count() > 1
|
||||
}
|
||||
}
|
||||
|
||||
impl AmbiguityCache {
|
||||
pub fn new(store: Store) -> Self {
|
||||
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
|
||||
}
|
||||
|
||||
pub async fn handle_event(
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &MemberEvent,
|
||||
) -> Result<()> {
|
||||
// Synapse seems to have a bug where it puts the same event into the
|
||||
// state and the timeline sometimes.
|
||||
//
|
||||
// Since our state, e.g. the old display name, already ended up inside
|
||||
// the state changes and we're pulling stuff out of the cache if it's
|
||||
// there calculating this twice for the same event will result in an
|
||||
// incorrect AmbiguityChange overwriting the correct one. In other
|
||||
// words, this method is not idempotent so we make it by ignoring
|
||||
// duplicate events.
|
||||
if self
|
||||
.changes
|
||||
.get(room_id)
|
||||
.map(|c| c.contains_key(&member_event.event_id))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?;
|
||||
|
||||
let display_names_same = match (&old_map, &new_map) {
|
||||
(Some(a), Some(b)) => a.display_name == b.display_name,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if display_names_same {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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()));
|
||||
let ambiguous = new_map.as_ref().map(|n| n.is_ambiguous()).unwrap_or(false);
|
||||
|
||||
self.update(room_id, old_map, new_map);
|
||||
|
||||
let change = AmbiguityChange {
|
||||
disambiguated_member,
|
||||
ambiguated_member,
|
||||
member_ambiguous: ambiguous,
|
||||
};
|
||||
|
||||
trace!("Handling display name ambiguity for {}: {:#?}", member_event.state_key, change);
|
||||
|
||||
self.add_change(room_id, member_event.event_id.clone(), change);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
old_map: Option<AmbiguityMap>,
|
||||
new_map: Option<AmbiguityMap>,
|
||||
) {
|
||||
let entry = self.cache.entry(room_id.clone()).or_insert_with(BTreeMap::new);
|
||||
|
||||
if let Some(old) = old_map {
|
||||
entry.insert(old.display_name, old.users);
|
||||
}
|
||||
|
||||
if let Some(new) = new_map {
|
||||
entry.insert(new.display_name, new.users);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async fn get(
|
||||
&mut self,
|
||||
changes: &StateChanges,
|
||||
room_id: &RoomId,
|
||||
member_event: &MemberEvent,
|
||||
) -> 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))
|
||||
{
|
||||
Some(m.clone())
|
||||
} else {
|
||||
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) {
|
||||
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())
|
||||
{
|
||||
Some(d.to_string())
|
||||
} else if let Some(d) = self
|
||||
.store
|
||||
.get_profile(room_id, &member_event.state_key)
|
||||
.await?
|
||||
.and_then(|c| c.displayname)
|
||||
{
|
||||
Some(d)
|
||||
} else {
|
||||
event.content.displayname.clone()
|
||||
};
|
||||
|
||||
Some(display_name.unwrap_or_else(|| event.state_key.localpart().to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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?
|
||||
};
|
||||
|
||||
Some(AmbiguityMap { display_name: old_name.to_string(), users: old_display_name_map })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let new_map = if matches!(member_event.content.membership, Join | Invite) {
|
||||
let new = member_event
|
||||
.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 {
|
||||
new
|
||||
} else if let Some(old) = old_display_name.as_deref() {
|
||||
old
|
||||
} else {
|
||||
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)
|
||||
{
|
||||
u.clone()
|
||||
} else {
|
||||
self.store.get_users_with_display_name(room_id, new_display_name).await?
|
||||
};
|
||||
|
||||
Some(AmbiguityMap {
|
||||
display_name: new_display_name.to_string(),
|
||||
users: new_display_name_map,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((old_map, new_map))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use lru::LruCache;
|
||||
use matrix_sdk_common::{async_trait, instant::Instant, locks::Mutex};
|
||||
use ruma::{
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
receipt::Receipt,
|
||||
room::member::{MemberEventContent, MembershipState},
|
||||
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
AnySyncStateEvent, EventType,
|
||||
},
|
||||
identifiers::{EventId, MxcUri, RoomId, UserId},
|
||||
receipt::ReceiptType,
|
||||
serde::Raw,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use super::{Result, RoomInfo, StateChanges, StateStore};
|
||||
use crate::{
|
||||
deserialized_responses::{MemberEvent, StrippedMemberEvent},
|
||||
media::{MediaRequest, UniqueKey},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStore {
|
||||
sync_token: Arc<RwLock<Option<String>>>,
|
||||
filters: Arc<DashMap<String, String>>,
|
||||
account_data: Arc<DashMap<String, Raw<AnyGlobalAccountDataEvent>>>,
|
||||
members: Arc<DashMap<RoomId, DashMap<UserId, MemberEvent>>>,
|
||||
profiles: Arc<DashMap<RoomId, DashMap<UserId, MemberEventContent>>>,
|
||||
display_names: Arc<DashMap<RoomId, DashMap<String, BTreeSet<UserId>>>>,
|
||||
joined_user_ids: Arc<DashMap<RoomId, DashSet<UserId>>>,
|
||||
invited_user_ids: Arc<DashMap<RoomId, DashSet<UserId>>>,
|
||||
room_info: Arc<DashMap<RoomId, RoomInfo>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
room_state: Arc<DashMap<RoomId, DashMap<String, DashMap<String, Raw<AnySyncStateEvent>>>>>,
|
||||
room_account_data: Arc<DashMap<RoomId, DashMap<String, Raw<AnyRoomAccountDataEvent>>>>,
|
||||
stripped_room_info: Arc<DashMap<RoomId, RoomInfo>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
stripped_room_state:
|
||||
Arc<DashMap<RoomId, DashMap<String, DashMap<String, Raw<AnyStrippedStateEvent>>>>>,
|
||||
stripped_members: Arc<DashMap<RoomId, DashMap<UserId, StrippedMemberEvent>>>,
|
||||
presence: Arc<DashMap<UserId, Raw<PresenceEvent>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
room_user_receipts: Arc<DashMap<RoomId, DashMap<String, DashMap<UserId, (EventId, Receipt)>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
room_event_receipts:
|
||||
Arc<DashMap<RoomId, DashMap<String, DashMap<EventId, DashMap<UserId, Receipt>>>>>,
|
||||
media: Arc<Mutex<LruCache<String, Vec<u8>>>>,
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
#[cfg(not(feature = "sled_state_store"))]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sync_token: Arc::new(RwLock::new(None)),
|
||||
filters: DashMap::new().into(),
|
||||
account_data: DashMap::new().into(),
|
||||
members: DashMap::new().into(),
|
||||
profiles: DashMap::new().into(),
|
||||
display_names: DashMap::new().into(),
|
||||
joined_user_ids: DashMap::new().into(),
|
||||
invited_user_ids: DashMap::new().into(),
|
||||
room_info: DashMap::new().into(),
|
||||
room_state: DashMap::new().into(),
|
||||
room_account_data: DashMap::new().into(),
|
||||
stripped_room_info: DashMap::new().into(),
|
||||
stripped_room_state: DashMap::new().into(),
|
||||
stripped_members: DashMap::new().into(),
|
||||
presence: DashMap::new().into(),
|
||||
room_user_receipts: DashMap::new().into(),
|
||||
room_event_receipts: DashMap::new().into(),
|
||||
media: Arc::new(Mutex::new(LruCache::new(100))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> {
|
||||
self.filters.insert(filter_name.to_string(), filter_id.to_string());
|
||||
|
||||
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.content.membership {
|
||||
MembershipState::Join => {
|
||||
self.joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(event.state_key.clone());
|
||||
self.invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
}
|
||||
MembershipState::Invite => {
|
||||
self.invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(event.state_key.clone());
|
||||
self.joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
}
|
||||
_ => {
|
||||
self.joined_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
self.invited_user_ids
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.remove(&event.state_key);
|
||||
}
|
||||
}
|
||||
|
||||
self.members
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashMap::new)
|
||||
.insert(event.state_key.clone(), event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (room, users) in &changes.profiles {
|
||||
for (user_id, profile) in users {
|
||||
self.profiles
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashMap::new)
|
||||
.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_insert_with(DashMap::new)
|
||||
.insert(display_name.clone(), display_names.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (event_type, event) in &changes.account_data {
|
||||
self.account_data.insert(event_type.to_string(), event.clone());
|
||||
}
|
||||
|
||||
for (room, events) in &changes.room_account_data {
|
||||
for (event_type, event) in events {
|
||||
self.room_account_data
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashMap::new)
|
||||
.insert(event_type.to_string(), 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_insert_with(DashMap::new)
|
||||
.entry(event_type.to_owned())
|
||||
.or_insert_with(DashMap::new)
|
||||
.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.invited_room_info {
|
||||
self.stripped_room_info.insert(room_id.clone(), info.clone());
|
||||
}
|
||||
|
||||
for (room, events) in &changes.stripped_members {
|
||||
for event in events.values() {
|
||||
self.stripped_members
|
||||
.entry(room.clone())
|
||||
.or_insert_with(DashMap::new)
|
||||
.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_insert_with(DashMap::new)
|
||||
.entry(event_type.to_owned())
|
||||
.or_insert_with(DashMap::new)
|
||||
.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_insert_with(DashMap::new)
|
||||
.entry(receipt_type.to_string())
|
||||
.or_insert_with(DashMap::new)
|
||||
.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_insert_with(DashMap::new)
|
||||
.entry(receipt_type.to_string())
|
||||
.or_insert_with(DashMap::new)
|
||||
.entry(event_id.clone())
|
||||
.or_insert_with(DashMap::new)
|
||||
.insert(user_id.clone(), receipt.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Saved changes in {:?}", now.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>> {
|
||||
#[allow(clippy::map_clone)]
|
||||
Ok(self.presence.get(user_id).map(|p| p.clone()))
|
||||
}
|
||||
|
||||
async fn get_state_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: EventType,
|
||||
state_key: &str,
|
||||
) -> Result<Option<Raw<AnySyncStateEvent>>> {
|
||||
#[allow(clippy::map_clone)]
|
||||
Ok(self.room_state.get(room_id).and_then(|e| {
|
||||
e.get(event_type.as_ref()).and_then(|s| s.get(state_key).map(|e| e.clone()))
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_profile(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<MemberEventContent>> {
|
||||
#[allow(clippy::map_clone)]
|
||||
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>> {
|
||||
#[allow(clippy::map_clone)]
|
||||
Ok(self.members.get(room_id).and_then(|m| m.get(state_key).map(|m| m.clone())))
|
||||
}
|
||||
|
||||
fn get_user_ids(&self, room_id: &RoomId) -> Vec<UserId> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.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<UserId> {
|
||||
#[allow(clippy::map_clone)]
|
||||
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<UserId> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.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> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.room_info.iter().map(|r| r.clone()).collect()
|
||||
}
|
||||
|
||||
fn get_stripped_room_infos(&self) -> Vec<RoomInfo> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.stripped_room_info.iter().map(|r| r.clone()).collect()
|
||||
}
|
||||
|
||||
async fn get_account_data_event(
|
||||
&self,
|
||||
event_type: EventType,
|
||||
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
|
||||
Ok(self.account_data.get(event_type.as_ref()).map(|e| e.clone()))
|
||||
}
|
||||
|
||||
async fn get_room_account_data_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: EventType,
|
||||
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
|
||||
Ok(self
|
||||
.room_account_data
|
||||
.get(room_id)
|
||||
.and_then(|m| m.get(event_type.as_ref()).map(|e| e.clone())))
|
||||
}
|
||||
|
||||
async fn get_user_room_receipt_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<(EventId, 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<(UserId, 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_else(Vec::new))
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[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: EventType,
|
||||
state_key: &str,
|
||||
) -> Result<Option<Raw<AnySyncStateEvent>>> {
|
||||
self.get_state_event(room_id, event_type, state_key).await
|
||||
}
|
||||
|
||||
async fn get_profile(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<MemberEventContent>> {
|
||||
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<UserId>> {
|
||||
Ok(self.get_user_ids(room_id))
|
||||
}
|
||||
|
||||
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>> {
|
||||
Ok(self.get_invited_user_ids(room_id))
|
||||
}
|
||||
|
||||
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>> {
|
||||
Ok(self.get_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<UserId>> {
|
||||
#[allow(clippy::map_clone)]
|
||||
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: EventType,
|
||||
) -> 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: EventType,
|
||||
) -> 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<(EventId, 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<(UserId, Receipt)>> {
|
||||
self.get_event_room_receipt_events(room_id, receipt_type, event_id).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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(feature = "sled_state_store"))]
|
||||
mod test {
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
api::client::r0::media::get_content_thumbnail::Method,
|
||||
identifiers::{event_id, mxc_uri, room_id, user_id, UserId},
|
||||
receipt::ReceiptType,
|
||||
uint,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{MemoryStore, StateChanges};
|
||||
use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSize, MediaType};
|
||||
|
||||
fn user_id() -> UserId {
|
||||
user_id!("@example:localhost")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_receipts_saving() {
|
||||
let store = MemoryStore::new();
|
||||
|
||||
let room_id = room_id!("!test: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.clone(): {
|
||||
"m.read": {
|
||||
user_id(): {
|
||||
"ts": 1436451550453u64
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let second_receipt_event = serde_json::from_value(json!({
|
||||
second_event_id.clone(): {
|
||||
"m.read": {
|
||||
user_id(): {
|
||||
"ts": 1436451551453u64
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(&room_id, ReceiptType::Read, &user_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &first_event_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &second_event_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_receipts(&room_id, first_receipt_event);
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(&room_id, ReceiptType::Read, &user_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some(),);
|
||||
assert_eq!(
|
||||
store
|
||||
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &first_event_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &second_event_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
let mut changes = StateChanges::default();
|
||||
changes.add_receipts(&room_id, second_receipt_event);
|
||||
|
||||
store.save_changes(&changes).await.unwrap();
|
||||
assert!(store
|
||||
.get_user_room_receipt_event(&room_id, ReceiptType::Read, &user_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(store
|
||||
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &first_event_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
assert_eq!(
|
||||
store
|
||||
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &second_event_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_media_content() {
|
||||
let store = MemoryStore::new();
|
||||
|
||||
let uri = mxc_uri!("mxc://localhost/media");
|
||||
let content: Vec<u8> = "somebinarydata".into();
|
||||
|
||||
let request_file =
|
||||
MediaRequest { media_type: MediaType::Uri(uri.clone()), format: MediaFormat::File };
|
||||
|
||||
let request_thumbnail = MediaRequest {
|
||||
media_type: MediaType::Uri(uri.clone()),
|
||||
format: MediaFormat::Thumbnail(MediaThumbnailSize {
|
||||
method: Method::Crop,
|
||||
width: uint!(100),
|
||||
height: uint!(100),
|
||||
}),
|
||||
};
|
||||
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_none());
|
||||
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_none());
|
||||
|
||||
store.add_media_content(&request_file, content.clone()).await.unwrap();
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_some());
|
||||
|
||||
store.remove_media_content(&request_file).await.unwrap();
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_none());
|
||||
|
||||
store.add_media_content(&request_file, content.clone()).await.unwrap();
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_some());
|
||||
|
||||
store.add_media_content(&request_thumbnail, content.clone()).await.unwrap();
|
||||
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_some());
|
||||
|
||||
store.remove_media_content_for_uri(&uri).await.unwrap();
|
||||
assert!(store.get_media_content(&request_file).await.unwrap().is_none());
|
||||
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
// 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 = "sled_state_store")]
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use matrix_sdk_common::{async_trait, locks::RwLock, AsyncTraitDeps};
|
||||
use ruma::{
|
||||
api::client::r0::push::get_notifications::Notification,
|
||||
events::{
|
||||
presence::PresenceEvent,
|
||||
receipt::{Receipt, ReceiptEventContent},
|
||||
room::member::MemberEventContent,
|
||||
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
AnySyncStateEvent, EventContent, EventType,
|
||||
},
|
||||
receipt::ReceiptType,
|
||||
serde::Raw,
|
||||
EventId, MxcUri, RoomId, UserId,
|
||||
};
|
||||
#[cfg(feature = "sled_state_store")]
|
||||
use sled::Db;
|
||||
|
||||
use crate::{
|
||||
deserialized_responses::{MemberEvent, StrippedMemberEvent},
|
||||
media::MediaRequest,
|
||||
rooms::{RoomInfo, RoomType},
|
||||
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;
|
||||
|
||||
/// 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")]
|
||||
#[error(transparent)]
|
||||
Sled(#[from] sled::Error),
|
||||
/// 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),
|
||||
/// The store is locked with a passphrase and an incorrect passphrase was
|
||||
/// given.
|
||||
#[error("The store failed to be unlocked")]
|
||||
StoreLocked,
|
||||
/// An unencrypted store was tried to be unlocked with a passphrase.
|
||||
#[error("The store is not encrypted but was tried to be opened with a passphrase")]
|
||||
UnencryptedStore,
|
||||
/// The store failed to encrypt or decrypt some data.
|
||||
#[error("Error encrypting or decrypting data from the store: {0}")]
|
||||
Encryption(String),
|
||||
}
|
||||
|
||||
/// A `StateStore` specific result type.
|
||||
pub type Result<T, E = StoreError> = std::result::Result<T, E>;
|
||||
|
||||
/// An abstract state store trait that can be used to implement different stores
|
||||
/// for the SDK.
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait StateStore: AsyncTraitDeps {
|
||||
/// Save the given filter id under the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `filter_name` - The name that should be used to store the filter id.
|
||||
///
|
||||
/// * `filter_id` - The filter id that should be stored in the state store.
|
||||
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()>;
|
||||
|
||||
/// Save the set of state changes in the store.
|
||||
async fn save_changes(&self, changes: &StateChanges) -> Result<()>;
|
||||
|
||||
/// Get the filter id that was stored under the given filter name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `filter_name` - The name that was used to store the filter id.
|
||||
async fn get_filter(&self, filter_name: &str) -> Result<Option<String>>;
|
||||
|
||||
/// Get the last stored sync token.
|
||||
async fn get_sync_token(&self) -> Result<Option<String>>;
|
||||
|
||||
/// Get the stored presence event for the given user.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The id of the user for which we wish to fetch the presence
|
||||
/// event for.
|
||||
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>>;
|
||||
|
||||
/// Get a state event out of the state store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room the state event was received for.
|
||||
///
|
||||
/// * `event_type` - The event type of the state event.
|
||||
async fn get_state_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: EventType,
|
||||
state_key: &str,
|
||||
) -> Result<Option<Raw<AnySyncStateEvent>>>;
|
||||
|
||||
/// Get the current profile for the given user in the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The room id the profile is used in.
|
||||
///
|
||||
/// * `user_id` - The id of the user the profile belongs to.
|
||||
async fn get_profile(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<MemberEventContent>>;
|
||||
|
||||
/// Get a raw `MemberEvent` for the given state key in the given room id.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The room id the member event belongs to.
|
||||
///
|
||||
/// * `state_key` - The user id that the member event defines the state for.
|
||||
async fn get_member_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
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 that are in the invited state for a
|
||||
/// given room.
|
||||
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>>;
|
||||
|
||||
/// 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>>;
|
||||
|
||||
/// Get all the pure `RoomInfo`s the store knows about.
|
||||
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>>;
|
||||
|
||||
/// Get all the pure `RoomInfo`s the store knows about.
|
||||
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>>;
|
||||
|
||||
/// Get all the users that use the given display name in the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room for which the display name users should
|
||||
/// be fetched for.
|
||||
///
|
||||
/// * `display_name` - The display name that the users use.
|
||||
async fn get_users_with_display_name(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
display_name: &str,
|
||||
) -> Result<BTreeSet<UserId>>;
|
||||
|
||||
/// Get an event out of the account data store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_type` - The event type of the account data event.
|
||||
async fn get_account_data_event(
|
||||
&self,
|
||||
event_type: EventType,
|
||||
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>>;
|
||||
|
||||
/// Get an event out of the room account data store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room for which the room account data event
|
||||
/// should
|
||||
/// be fetched.
|
||||
///
|
||||
/// * `event_type` - The event type of the room account data event.
|
||||
async fn get_room_account_data_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_type: EventType,
|
||||
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>>;
|
||||
|
||||
/// Get an event out of the user room receipt store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room for which the receipt should be
|
||||
/// fetched.
|
||||
///
|
||||
/// * `receipt_type` - The type of the receipt.
|
||||
///
|
||||
/// * `user_id` - The id of the user for who the receipt should be fetched.
|
||||
async fn get_user_room_receipt_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<(EventId, Receipt)>>;
|
||||
|
||||
/// Get events out of the event room receipt store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room for which the receipts should be
|
||||
/// fetched.
|
||||
///
|
||||
/// * `receipt_type` - The type of the receipts.
|
||||
///
|
||||
/// * `event_id` - The id of the event for which the receipts should be
|
||||
/// fetched.
|
||||
async fn get_event_room_receipt_events(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
receipt_type: ReceiptType,
|
||||
event_id: &EventId,
|
||||
) -> Result<Vec<(UserId, Receipt)>>;
|
||||
|
||||
/// Add a media file's content in the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
///
|
||||
/// * `content` - The content of the file.
|
||||
async fn add_media_content(&self, request: &MediaRequest, content: Vec<u8>) -> Result<()>;
|
||||
|
||||
/// Get a media file's content out of the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>>;
|
||||
|
||||
/// Removes a media file's content from the media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The `MediaRequest` of the file.
|
||||
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()>;
|
||||
|
||||
/// Removes all the media files' content associated to an `MxcUri` from the
|
||||
/// media store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `uri` - The `MxcUri` of the media files.
|
||||
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()>;
|
||||
}
|
||||
|
||||
/// A state store wrapper for the SDK.
|
||||
///
|
||||
/// This adds additional higher level store functionality on top of a
|
||||
/// `StateStore` implementation.
|
||||
#[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>>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
fn new(inner: Box<dyn StateStore>) -> Self {
|
||||
let session = Arc::new(RwLock::new(None));
|
||||
let sync_token = Arc::new(RwLock::new(None));
|
||||
|
||||
Self {
|
||||
inner: inner.into(),
|
||||
session,
|
||||
sync_token,
|
||||
rooms: DashMap::new().into(),
|
||||
stripped_rooms: DashMap::new().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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);
|
||||
}
|
||||
|
||||
for info in self.inner.get_stripped_room_infos().await? {
|
||||
let room = Room::restore(&session.user_id, self.inner.clone(), info);
|
||||
self.stripped_rooms.insert(room.room_id().to_owned(), room);
|
||||
}
|
||||
|
||||
let token = self.get_sync_token().await?;
|
||||
|
||||
*self.sync_token.write().await = token;
|
||||
*self.session.write().await = Some(session);
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
/// Get all the rooms this store knows about.
|
||||
pub fn get_rooms(&self) -> Vec<Room> {
|
||||
self.rooms.iter().filter_map(|r| self.get_room(r.key())).collect()
|
||||
}
|
||||
|
||||
/// Get the room with the given room id.
|
||||
pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
|
||||
self.rooms
|
||||
.get(room_id)
|
||||
.and_then(|r| match r.room_type() {
|
||||
RoomType::Joined => Some(r.clone()),
|
||||
RoomType::Left => Some(r.clone()),
|
||||
RoomType::Invited => self.get_stripped_room(room_id),
|
||||
})
|
||||
.or_else(|| self.get_stripped_room(room_id))
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
self.stripped_rooms
|
||||
.entry(room_id.clone())
|
||||
.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;
|
||||
|
||||
self.rooms
|
||||
.entry(room_id.clone())
|
||||
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, room_type))
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Store {
|
||||
type Target = dyn StateStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Store state changes and pass them to the StateStore.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StateChanges {
|
||||
/// The sync token that relates to this update.
|
||||
pub sync_token: Option<String>,
|
||||
/// A user session, containing an access token and information about the
|
||||
/// associated user account.
|
||||
pub session: Option<Session>,
|
||||
/// A mapping of event type string to `AnyBasicEvent`.
|
||||
pub account_data: BTreeMap<String, Raw<AnyGlobalAccountDataEvent>>,
|
||||
/// A mapping of `UserId` to `PresenceEvent`.
|
||||
pub presence: BTreeMap<UserId, 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 event type string to a state key and
|
||||
/// `AnySyncStateEvent`.
|
||||
pub state: BTreeMap<RoomId, BTreeMap<String, 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>>>,
|
||||
/// A map of `RoomId` to `RoomInfo`.
|
||||
pub room_infos: BTreeMap<RoomId, RoomInfo>,
|
||||
/// A map of `RoomId` to `ReceiptEventContent`.
|
||||
pub receipts: BTreeMap<RoomId, 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>,
|
||||
|
||||
/// 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>>>,
|
||||
/// A map of `RoomId` to a vector of `Notification`s
|
||||
pub notifications: BTreeMap<RoomId, Vec<Notification>>,
|
||||
}
|
||||
|
||||
impl StateChanges {
|
||||
/// Create a new `StateChanges` struct with the given sync_token.
|
||||
pub fn new(sync_token: String) -> Self {
|
||||
Self { sync_token: Some(sync_token), ..Default::default() }
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given `PresenceEvent`.
|
||||
pub fn add_presence_event(&mut self, event: PresenceEvent, raw_event: Raw<PresenceEvent>) {
|
||||
self.presence.insert(event.sender, raw_event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given `RoomInfo`.
|
||||
pub fn add_room(&mut self, room: RoomInfo) {
|
||||
self.room_infos.insert(room.room_id.as_ref().to_owned(), room);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given `AnyBasicEvent`.
|
||||
pub fn add_account_data(
|
||||
&mut self,
|
||||
event: AnyGlobalAccountDataEvent,
|
||||
raw_event: Raw<AnyGlobalAccountDataEvent>,
|
||||
) {
|
||||
self.account_data.insert(event.content().event_type().to_owned(), raw_event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `AnyBasicEvent`.
|
||||
pub fn add_room_account_data(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
event: AnyRoomAccountDataEvent,
|
||||
raw_event: Raw<AnyRoomAccountDataEvent>,
|
||||
) {
|
||||
self.room_account_data
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(event.content().event_type().to_owned(), 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) {
|
||||
let user_id = event.state_key.clone();
|
||||
|
||||
self.stripped_members
|
||||
.entry(room_id.to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(user_id, event);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `AnySyncStateEvent`.
|
||||
pub fn add_state_event(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
event: AnySyncStateEvent,
|
||||
raw_event: Raw<AnySyncStateEvent>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Update the `StateChanges` struct with the given room with a new
|
||||
/// `Receipts`.
|
||||
pub fn add_receipts(&mut self, room_id: &RoomId, event: ReceiptEventContent) {
|
||||
self.receipts.insert(room_id.to_owned(), event);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,286 @@
|
||||
// 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.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use chacha20poly1305::{
|
||||
aead::{Aead, Error as EncryptionError, NewAead},
|
||||
ChaCha20Poly1305, Key, Nonce, XChaCha20Poly1305, XNonce,
|
||||
};
|
||||
use hmac::Hmac;
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{thread_rng, Error as RngError, Fill};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use crate::StoreError;
|
||||
|
||||
const VERSION: u8 = 1;
|
||||
const KEY_SIZE: usize = 32;
|
||||
const NONCE_SIZE: usize = 12;
|
||||
const XNONCE_SIZE: usize = 24;
|
||||
const KDF_SALT_SIZE: usize = 32;
|
||||
#[cfg(not(test))]
|
||||
const KDF_ROUNDS: u32 = 200_000;
|
||||
#[cfg(test)]
|
||||
const KDF_ROUNDS: u32 = 1000;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
#[error("Error encrypting or decrypting an event {0}")]
|
||||
Encryption(String),
|
||||
#[error("Error generating enough random data for a cryptographic operation")]
|
||||
Random(#[from] RngError),
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<StoreError> for Error {
|
||||
fn into(self) -> StoreError {
|
||||
match self {
|
||||
Error::Serialization(e) => StoreError::Json(e),
|
||||
Error::Encryption(e) => StoreError::Encryption(e),
|
||||
Error::Random(_) => StoreError::Encryption(self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncryptionError> for Error {
|
||||
fn from(e: EncryptionError) -> Self {
|
||||
Error::Encryption(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EncryptedEvent {
|
||||
version: u8,
|
||||
ciphertext: Vec<u8>,
|
||||
nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Version specific info for the key derivation method that is used.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum KdfInfo {
|
||||
Pbkdf2ToChaCha20Poly1305 {
|
||||
/// The number of PBKDF rounds that were used when deriving the store
|
||||
/// key.
|
||||
rounds: u32,
|
||||
/// The salt that was used when the passphrase was expanded into a store
|
||||
/// key.
|
||||
kdf_salt: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Version specific info for encryption method that is used to encrypt our
|
||||
/// store key.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum CipherTextInfo {
|
||||
ChaCha20Poly1305 {
|
||||
/// The nonce that was used to encrypt the ciphertext.
|
||||
nonce: Vec<u8>,
|
||||
/// The encrypted store key.
|
||||
ciphertext: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// An encrypted version of our store key, this can be safely stored in a
|
||||
/// database.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EncryptedStoreKey {
|
||||
/// Info about the key derivation method that was used to expand the
|
||||
/// passphrase into an encryption key.
|
||||
pub kdf_info: KdfInfo,
|
||||
/// The ciphertext with it's accompanying additional data that is needed to
|
||||
/// decrypt the store key.
|
||||
pub ciphertext_info: CipherTextInfo,
|
||||
}
|
||||
|
||||
/// A store key that can be used to encrypt entries in the store.
|
||||
#[derive(Debug, Zeroize, PartialEq)]
|
||||
pub struct StoreKey {
|
||||
inner: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for StoreKey {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
if value.len() != KEY_SIZE {
|
||||
Err(())
|
||||
} else {
|
||||
Ok(Self { inner: value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreKey {
|
||||
/// Generate a new random store key.
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let mut key = vec![0u8; KEY_SIZE];
|
||||
let mut rng = thread_rng();
|
||||
key.try_fill(&mut rng)?;
|
||||
|
||||
Ok(Self { inner: key })
|
||||
}
|
||||
|
||||
/// Expand the given passphrase into a KEY_SIZE long key.
|
||||
fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Zeroizing<Vec<u8>> {
|
||||
let mut key = Zeroizing::from(vec![0u8; KEY_SIZE]);
|
||||
pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), salt, rounds, &mut *key);
|
||||
key
|
||||
}
|
||||
|
||||
/// Get the store key.
|
||||
fn key(&self) -> &Key {
|
||||
Key::from_slice(&self.inner)
|
||||
}
|
||||
|
||||
/// Encrypt and export our store key using the given passphrase.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the
|
||||
/// store key.
|
||||
pub fn export(&self, passphrase: &str) -> Result<EncryptedStoreKey, Error> {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let mut salt = vec![0u8; KDF_SALT_SIZE];
|
||||
salt.try_fill(&mut rng)?;
|
||||
|
||||
let key = StoreKey::expand_key(passphrase, &salt, KDF_ROUNDS);
|
||||
let key = Key::from_slice(key.as_ref());
|
||||
let cipher = ChaCha20Poly1305::new(key);
|
||||
|
||||
let mut nonce = vec![0u8; NONCE_SIZE];
|
||||
nonce.try_fill(&mut rng)?;
|
||||
|
||||
let ciphertext =
|
||||
cipher.encrypt(Nonce::from_slice(nonce.as_ref()), self.inner.as_slice())?;
|
||||
|
||||
Ok(EncryptedStoreKey {
|
||||
kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds: KDF_ROUNDS, kdf_salt: salt },
|
||||
ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext },
|
||||
})
|
||||
}
|
||||
|
||||
fn get_nonce() -> Result<Vec<u8>, RngError> {
|
||||
let mut nonce = vec![0u8; XNONCE_SIZE];
|
||||
let mut rng = thread_rng();
|
||||
|
||||
nonce.try_fill(&mut rng)?;
|
||||
|
||||
Ok(nonce)
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, event: &impl Serialize) -> Result<EncryptedEvent, Error> {
|
||||
let event = serde_json::to_vec(event)?;
|
||||
|
||||
let nonce = StoreKey::get_nonce()?;
|
||||
let cipher = XChaCha20Poly1305::new(self.key());
|
||||
let xnonce = XNonce::from_slice(&nonce);
|
||||
|
||||
let ciphertext = cipher.encrypt(xnonce, event.as_ref())?;
|
||||
|
||||
Ok(EncryptedEvent { version: VERSION, ciphertext, nonce })
|
||||
}
|
||||
|
||||
pub fn decrypt<T: for<'b> Deserialize<'b>>(&self, event: EncryptedEvent) -> Result<T, Error> {
|
||||
if event.version != VERSION {
|
||||
return Err(Error::Encryption(
|
||||
"Error decrypting: Unknown ciphertext version".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let cipher = XChaCha20Poly1305::new(self.key());
|
||||
let nonce = XNonce::from_slice(&event.nonce);
|
||||
let plaintext = cipher.decrypt(nonce, event.ciphertext.as_ref())?;
|
||||
|
||||
Ok(serde_json::from_slice(&plaintext)?)
|
||||
}
|
||||
|
||||
/// Restore a store key from an encrypted export.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the
|
||||
/// store key.
|
||||
///
|
||||
/// * `encrypted` - The exported and encrypted version of the store key.
|
||||
pub fn import(passphrase: &str, encrypted: EncryptedStoreKey) -> Result<Self, EncryptionError> {
|
||||
let key = match encrypted.kdf_info {
|
||||
KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, kdf_salt } => {
|
||||
Self::expand_key(passphrase, &kdf_salt, rounds)
|
||||
}
|
||||
};
|
||||
|
||||
let key = Key::from_slice(key.as_ref());
|
||||
|
||||
let decrypted = match encrypted.ciphertext_info {
|
||||
CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => {
|
||||
let cipher = ChaCha20Poly1305::new(key);
|
||||
let nonce = Nonce::from_slice(&nonce);
|
||||
cipher.decrypt(nonce, ciphertext.as_ref())?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self { inner: decrypted })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::StoreKey;
|
||||
|
||||
#[test]
|
||||
fn generating() {
|
||||
StoreKey::new().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypting() {
|
||||
let passphrase = "it's a secret to everybody";
|
||||
let store_key = StoreKey::new().unwrap();
|
||||
|
||||
let encrypted = store_key.export(passphrase).unwrap();
|
||||
let decrypted = StoreKey::import(passphrase, encrypted).unwrap();
|
||||
|
||||
assert_eq!(store_key, decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypting_events() {
|
||||
let event = json!({
|
||||
"content": {
|
||||
"body": "Bee Gees - Stayin' Alive",
|
||||
"info": {
|
||||
"duration": 2140786,
|
||||
"mimetype": "audio/mpeg",
|
||||
"size": 1563685
|
||||
},
|
||||
"msgtype": "m.audio",
|
||||
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
|
||||
},
|
||||
});
|
||||
|
||||
let store_key = StoreKey::new().unwrap();
|
||||
|
||||
let encrypted = store_key.encrypt(&event).unwrap();
|
||||
let decrypted: Value = store_key.decrypt(encrypted).unwrap();
|
||||
assert_eq!(event, decrypted);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
description = "Collection of common types used in the matrix-sdk"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "Collection of common types and imports used in the matrix-sdk"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
@@ -8,24 +8,24 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
|
||||
[dependencies]
|
||||
js_int = "0.1.5"
|
||||
ruma-api = "0.16.1"
|
||||
ruma-client-api = "0.9.0"
|
||||
ruma-events = "0.21.2"
|
||||
ruma-identifiers = "0.16.1"
|
||||
instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] }
|
||||
async-trait = "0.1.50"
|
||||
instant = { version = "0.1.9", features = ["wasm-bindgen", "now"] }
|
||||
ruma = { version = "0.2.0", features = ["client-api-c"] }
|
||||
serde = "1.0.126"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
uuid = { version = "0.8.1", features = ["v4"] }
|
||||
uuid = { version = "0.8.2", default-features = false, features = ["v4", "serde"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||
version = "0.2.21"
|
||||
version = "1.7.1"
|
||||
default-features = false
|
||||
features = ["sync", "time", "fs"]
|
||||
features = ["rt", "sync"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
futures-locks = { version = "0.5.0", default-features = false }
|
||||
uuid = { version = "0.8.1", features = ["v4", "wasm-bindgen"] }
|
||||
futures = "0.3.15"
|
||||
futures-locks = { version = "0.6.0", default-features = false }
|
||||
wasm-bindgen-futures = "0.4.24"
|
||||
uuid = { version = "0.8.2", default-features = false, features = ["v4", "wasm-bindgen"] }
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
use std::{collections::BTreeMap, convert::TryFrom};
|
||||
|
||||
use ruma::{
|
||||
api::client::r0::{
|
||||
push::get_notifications::Notification,
|
||||
sync::sync_events::{
|
||||
DeviceLists, Ephemeral, GlobalAccountData, InvitedRoom, Presence, RoomAccountData,
|
||||
State, ToDevice, UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
},
|
||||
events::{
|
||||
room::member::MemberEventContent, AnySyncRoomEvent, StateEvent, StrippedStateEvent,
|
||||
SyncStateEvent, Unsigned,
|
||||
},
|
||||
identifiers::{DeviceKeyAlgorithm, EventId, RoomId, UserId},
|
||||
serde::Raw,
|
||||
DeviceIdBox, MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A change in ambiguity of room members that an `m.room.member` event
|
||||
/// triggers.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct AmbiguityChange {
|
||||
/// Is the member that is contained in the state key of the `m.room.member`
|
||||
/// event itself ambiguous because of the event.
|
||||
pub member_ambiguous: bool,
|
||||
/// Has another user been disambiguated because of this event.
|
||||
pub disambiguated_member: Option<UserId>,
|
||||
/// Has another user become ambiguous because of this event.
|
||||
pub ambiguated_member: Option<UserId>,
|
||||
}
|
||||
|
||||
/// Collection of ambiguioty changes that room member events trigger.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
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>>,
|
||||
}
|
||||
|
||||
/// The verification state of the device that sent an event to us.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum VerificationState {
|
||||
/// The device is trusted.
|
||||
Trusted,
|
||||
/// The device is not trusted.
|
||||
Untrusted,
|
||||
/// The device is not known to us.
|
||||
UnknownDevice,
|
||||
}
|
||||
|
||||
/// The algorithm specific information of a decrypted event.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum AlgorithmInfo {
|
||||
/// The info if the event was encrypted using m.megolm.v1.aes-sha2
|
||||
MegolmV1AesSha2 {
|
||||
/// The curve25519 key of the device that created the megolm decryption
|
||||
/// key originally.
|
||||
curve25519_key: String,
|
||||
/// The signing keys that have created the megolm key that was used to
|
||||
/// decrypt this session. This map will usually contain a single ed25519
|
||||
/// key.
|
||||
sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String>,
|
||||
/// Chain of curve25519 keys through which this session was forwarded,
|
||||
/// via m.forwarded_room_key events.
|
||||
forwarding_curve25519_key_chain: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Struct containing information on how an event was decrypted.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
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,
|
||||
/// 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,
|
||||
/// 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
|
||||
/// is the state of the device at the time of decryption. It may change in
|
||||
/// the future if a device gets verified or deleted.
|
||||
pub verification_state: VerificationState,
|
||||
}
|
||||
|
||||
/// A customized version of a room event coming from a sync that holds optional
|
||||
/// encryption info.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SyncRoomEvent {
|
||||
/// The actual event.
|
||||
pub event: Raw<AnySyncRoomEvent>,
|
||||
/// The encryption info about the event. Will be `None` if the event was not
|
||||
/// encrypted.
|
||||
pub encryption_info: Option<EncryptionInfo>,
|
||||
}
|
||||
|
||||
impl From<Raw<AnySyncRoomEvent>> for SyncRoomEvent {
|
||||
fn from(inner: Raw<AnySyncRoomEvent>) -> Self {
|
||||
Self { encryption_info: None, event: inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct SyncResponse {
|
||||
/// The batch token to supply in the `since` param of the next `/sync`
|
||||
/// request.
|
||||
pub next_batch: String,
|
||||
/// Updates to rooms.
|
||||
pub rooms: Rooms,
|
||||
/// Updates to the presence status of other users.
|
||||
pub presence: Presence,
|
||||
/// The global private data created by this user.
|
||||
pub account_data: GlobalAccountData,
|
||||
/// Messages sent directly between devices.
|
||||
pub to_device: ToDevice,
|
||||
/// Information on E2E device updates.
|
||||
///
|
||||
/// Only present on an incremental sync.
|
||||
pub device_lists: DeviceLists,
|
||||
/// For each key algorithm, the number of unclaimed one-time keys
|
||||
/// currently held on the server for a device.
|
||||
pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, u64>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
/// New notifications per room.
|
||||
pub notifications: BTreeMap<RoomId, Vec<Notification>>,
|
||||
}
|
||||
|
||||
impl SyncResponse {
|
||||
pub fn new(next_batch: String) -> Self {
|
||||
Self { next_batch, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
/// The rooms that the user has joined.
|
||||
pub join: BTreeMap<RoomId, JoinedRoom>,
|
||||
/// The rooms that the user has been invited to.
|
||||
pub invite: BTreeMap<RoomId, InvitedRoom>,
|
||||
}
|
||||
|
||||
/// Updates to joined rooms.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct JoinedRoom {
|
||||
/// Counts of unread notifications for this room.
|
||||
pub unread_notifications: UnreadNotificationsCount,
|
||||
/// The timeline of messages and state changes in the room.
|
||||
pub timeline: Timeline,
|
||||
/// Updates to the state, between the time indicated by the `since`
|
||||
/// parameter, and the start of the `timeline` (or all state up to the
|
||||
/// start of the `timeline`, if `since` is not given, or `full_state` is
|
||||
/// true).
|
||||
pub state: State,
|
||||
/// The private data that this user has attached to this room.
|
||||
pub account_data: RoomAccountData,
|
||||
/// The ephemeral events in the room that aren't recorded in the timeline or
|
||||
/// state of the room. e.g. typing.
|
||||
pub ephemeral: Ephemeral,
|
||||
}
|
||||
|
||||
impl JoinedRoom {
|
||||
pub fn new(
|
||||
timeline: Timeline,
|
||||
state: State,
|
||||
account_data: RoomAccountData,
|
||||
ephemeral: Ephemeral,
|
||||
unread_notifications: UnreadNotificationsCount,
|
||||
) -> Self {
|
||||
Self { unread_notifications, timeline, state, account_data, ephemeral }
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts of unread notifications for a room.
|
||||
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct UnreadNotificationsCount {
|
||||
/// The number of unread notifications for this room with the highlight flag
|
||||
/// set.
|
||||
pub highlight_count: u64,
|
||||
/// The total number of unread notifications for this room.
|
||||
pub notification_count: u64,
|
||||
}
|
||||
|
||||
impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
|
||||
fn from(notifications: RumaUnreadNotificationsCount) -> Self {
|
||||
Self {
|
||||
highlight_count: notifications.highlight_count.map(|c| c.into()).unwrap_or(0),
|
||||
notification_count: notifications.notification_count.map(|c| c.into()).unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct LeftRoom {
|
||||
/// The timeline of messages and state changes in the room up to the point
|
||||
/// when the user left.
|
||||
pub timeline: Timeline,
|
||||
/// Updates to the state, between the time indicated by the `since`
|
||||
/// parameter, and the start of the `timeline` (or all state up to the
|
||||
/// start of the `timeline`, if `since` is not given, or `full_state` is
|
||||
/// true).
|
||||
pub state: State,
|
||||
/// The private data that this user has attached to this room.
|
||||
pub account_data: RoomAccountData,
|
||||
}
|
||||
|
||||
impl LeftRoom {
|
||||
pub fn new(timeline: Timeline, state: State, account_data: RoomAccountData) -> Self {
|
||||
Self { timeline, state, account_data }
|
||||
}
|
||||
}
|
||||
|
||||
/// Events in the room.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Timeline {
|
||||
/// True if the number of events returned was limited by the `limit` on the
|
||||
/// filter.
|
||||
pub limited: bool,
|
||||
|
||||
/// A token that can be supplied to to the `from` parameter of the
|
||||
/// `/rooms/{roomId}/messages` endpoint.
|
||||
pub prev_batch: Option<String>,
|
||||
|
||||
/// A list of events.
|
||||
pub events: Vec<SyncRoomEvent>,
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
pub fn new(limited: bool, prev_batch: Option<String>) -> Self {
|
||||
Self { limited, prev_batch, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A deserialized response for the rooms members API call.
|
||||
///
|
||||
/// [GET /_matrix/client/r0/rooms/{roomId}/members](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-members)
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct MembersResponse {
|
||||
/// The list of members events.
|
||||
pub chunk: Vec<MemberEvent>,
|
||||
/// Collection of ambiguity changes that room member events trigger.
|
||||
pub ambiguity_changes: AmbiguityChanges,
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Abstraction over an executor so we can spawn tasks under WASM the same way
|
||||
//! we do usually.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use futures::{future::RemoteHandle, Future, FutureExt};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::spawn;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn spawn<F, T>(future: F) -> JoinHandle<T>
|
||||
where
|
||||
F: Future<Output = T> + 'static,
|
||||
{
|
||||
let fut = future.unit_error();
|
||||
let (fut, handle) = fut.remote_handle();
|
||||
spawn_local(fut);
|
||||
|
||||
JoinHandle { handle }
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct JoinHandle<T> {
|
||||
handle: RemoteHandle<Result<T, ()>>,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T: 'static> Future for JoinHandle<T> {
|
||||
type Output = Result<T, ()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.handle).poll(cx)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
pub use async_trait::async_trait;
|
||||
pub use instant;
|
||||
pub use js_int;
|
||||
pub use ruma_api::{
|
||||
error::{FromHttpResponseError, IntoHttpError, ServerError},
|
||||
Endpoint,
|
||||
};
|
||||
pub use ruma_client_api as api;
|
||||
pub use ruma_events as events;
|
||||
pub use ruma_identifiers as identifiers;
|
||||
|
||||
pub use uuid;
|
||||
|
||||
pub mod deserialized_responses;
|
||||
pub mod executor;
|
||||
pub mod locks;
|
||||
|
||||
/// Super trait that is used for our store traits, this trait will differ if
|
||||
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
|
||||
/// implemented, while other targets will.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
|
||||
|
||||
/// Super trait that is used for our store traits, this trait will differ if
|
||||
/// 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 {}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
// https://www.reddit.com/r/rust/comments/f4zldz/i_audited_3_different_implementation_of_async/
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use futures_locks::Mutex;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use futures_locks::RwLock;
|
||||
|
||||
pub use futures_locks::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::sync::Mutex;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::sync::RwLock;
|
||||
pub use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "Matrix encryption library"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
@@ -8,44 +8,57 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-crypto"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docs"]
|
||||
rustdoc-args = ["--cfg", "feature=\"docs\""]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
sqlite-cryptostore = ["sqlx"]
|
||||
sled_cryptostore = ["sled"]
|
||||
docs = ["sled_cryptostore"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.31"
|
||||
matrix-qrcode = { version = "0.1.0", path = "../matrix_qrcode" }
|
||||
matrix-sdk-common = { version = "0.3.0", path = "../matrix_sdk_common" }
|
||||
ruma = { version = "0.2.0", features = ["client-api-c", "unstable-pre-spec"] }
|
||||
|
||||
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
|
||||
|
||||
olm-rs = { version = "0.5.0", features = ["serde"] }
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
serde_json = "1.0.53"
|
||||
cjson = "0.1.0"
|
||||
zeroize = { version = "1.1.0", features = ["zeroize_derive"] }
|
||||
url = "2.1.1"
|
||||
olm-rs = { version = "1.0.1", features = ["serde"] }
|
||||
getrandom = "0.2.3"
|
||||
serde = { version = "1.0.126", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.64"
|
||||
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
|
||||
|
||||
# Misc dependencies
|
||||
thiserror = "1.0.19"
|
||||
tracing = "0.1.14"
|
||||
atomic = "0.4.5"
|
||||
dashmap = "3.11.2"
|
||||
|
||||
[dependencies.tracing-futures]
|
||||
version = "0.2.4"
|
||||
default-features = false
|
||||
features = ["std", "std-future"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.sqlx]
|
||||
version = "0.3.5"
|
||||
optional = true
|
||||
default-features = false
|
||||
features = ["runtime-tokio", "sqlite"]
|
||||
futures = "0.3.15"
|
||||
sled = { version = "0.34.6", optional = true }
|
||||
thiserror = "1.0.25"
|
||||
tracing = "0.1.26"
|
||||
atomic = "0.5.0"
|
||||
dashmap = "4.0.2"
|
||||
sha2 = "0.9.5"
|
||||
aes-gcm = "0.9.2"
|
||||
aes = { version = "0.7.4", features = ["ctr"] }
|
||||
pbkdf2 = { version = "0.8.0", default-features = false }
|
||||
hmac = "0.11.0"
|
||||
base64 = "0.13.0"
|
||||
byteorder = "1.4.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
|
||||
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
|
||||
serde_json = "1.0.53"
|
||||
tempfile = "3.1.0"
|
||||
http = "0.2.1"
|
||||
tokio = { version = "1.7.1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
proptest = "1.0.0"
|
||||
serde_json = "1.0.64"
|
||||
tempfile = "3.2.0"
|
||||
http = "0.2.4"
|
||||
matrix-sdk-test = { version = "0.3.0", path = "../matrix_sdk_test" }
|
||||
indoc = "1.0.3"
|
||||
criterion = { version = "0.3.4", features = ["async", "async_tokio", "html_reports"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
pprof = { version = "0.4.3", features = ["flamegraph"] }
|
||||
|
||||
[[bench]]
|
||||
name = "crypto_bench"
|
||||
harness = false
|
||||
required-features = ["sled_cryptostore"]
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod perf;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use criterion::*;
|
||||
use matrix_sdk_common::uuid::Uuid;
|
||||
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
|
||||
use matrix_sdk_test::response_from_file;
|
||||
use ruma::{
|
||||
api::{
|
||||
client::r0::{
|
||||
keys::{claim_keys, get_keys},
|
||||
to_device::send_event_to_device::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
room_id, user_id, DeviceIdBox, UserId,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
fn alice_id() -> UserId {
|
||||
user_id!("@alice:example.org")
|
||||
}
|
||||
|
||||
fn alice_device_id() -> DeviceIdBox {
|
||||
"JLAFKJWSCS".into()
|
||||
}
|
||||
|
||||
fn keys_query_response() -> get_keys::Response {
|
||||
let data = include_bytes!("./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)
|
||||
.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");
|
||||
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")
|
||||
}
|
||||
|
||||
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 response = keys_query_response();
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
let count = response.device_keys.values().fold(0, |acc, d| acc + d.len())
|
||||
+ response.master_keys.len()
|
||||
+ response.self_signing_keys.len()
|
||||
+ response.user_signing_keys.len();
|
||||
|
||||
let mut group = c.benchmark_group("Keys querying");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
|
||||
let name = format!("{} device and cross signing keys", count);
|
||||
|
||||
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() })
|
||||
});
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let machine = runtime
|
||||
.block_on(OlmMachine::new_with_default_store(
|
||||
&alice_id(),
|
||||
&alice_device_id(),
|
||||
dir.path(),
|
||||
None,
|
||||
))
|
||||
.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() })
|
||||
});
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
pub fn keys_claiming(c: &mut Criterion) {
|
||||
let runtime = Arc::new(Builder::new_multi_thread().build().expect("Can't create runtime"));
|
||||
|
||||
let keys_query_response = keys_query_response();
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
let response = keys_claim_response();
|
||||
|
||||
let count = response.one_time_keys.values().fold(0, |acc, d| acc + d.len());
|
||||
|
||||
let mut group = c.benchmark_group("Olm session creation");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
|
||||
let name = format!("{} one-time keys", count);
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
|
||||
runtime
|
||||
.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response))
|
||||
.unwrap();
|
||||
(machine, runtime.clone())
|
||||
},
|
||||
move |(machine, runtime)| {
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, response)).unwrap()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
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();
|
||||
(machine, runtime.clone())
|
||||
},
|
||||
move |(machine, runtime)| {
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, response)).unwrap()
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
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 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 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 mut group = c.benchmark_group("Room key sharing");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
let name = format!("{} devices", count);
|
||||
|
||||
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())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!requests.is_empty());
|
||||
|
||||
for request in requests {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).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();
|
||||
|
||||
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())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!requests.is_empty());
|
||||
|
||||
for request in requests {
|
||||
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
|
||||
}
|
||||
|
||||
machine.invalidate_group_session(&room_id).await.unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
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 count = response.device_keys.values().fold(0, |acc, d| acc + d.len());
|
||||
|
||||
let mut group = c.benchmark_group("Devices missing sessions collecting");
|
||||
group.throughput(Throughput::Elements(count as u64));
|
||||
|
||||
let name = format!("{} devices", count);
|
||||
|
||||
runtime.block_on(machine.mark_request_as_sent(&uuid, &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()
|
||||
})
|
||||
});
|
||||
|
||||
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, &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() })
|
||||
});
|
||||
|
||||
group.finish()
|
||||
}
|
||||
|
||||
fn criterion() -> Criterion {
|
||||
#[cfg(target_os = "linux")]
|
||||
let criterion = Criterion::default().with_profiler(perf::FlamegraphProfiler::new(100));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let criterion = Criterion::default();
|
||||
|
||||
criterion
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = criterion();
|
||||
targets = keys_query, keys_claiming, room_key_sharing, devices_missing_sessions_collecting,
|
||||
}
|
||||
criterion_main!(benches);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
//! This is a simple Criterion Profiler implementation using pprof.
|
||||
//!
|
||||
//! It's mostly a direct copy from here: https://www.jibbow.com/posts/criterion-flamegraphs/
|
||||
use std::{fs::File, os::raw::c_int, path::Path};
|
||||
|
||||
use criterion::profiler::Profiler;
|
||||
use pprof::ProfilerGuard;
|
||||
|
||||
/// Small custom profiler that can be used with Criterion to create a flamegraph
|
||||
/// for benchmarks. Also see [the Criterion documentation on
|
||||
/// this][custom-profiler].
|
||||
///
|
||||
/// ## Example on how to enable the custom profiler:
|
||||
///
|
||||
/// ```
|
||||
/// mod perf;
|
||||
/// use perf::FlamegraphProfiler;
|
||||
///
|
||||
/// fn fibonacci_profiled(criterion: &mut Criterion) {
|
||||
/// // Use the criterion struct as normal here.
|
||||
/// }
|
||||
///
|
||||
/// fn custom() -> Criterion {
|
||||
/// Criterion::default().with_profiler(FlamegraphProfiler::new())
|
||||
/// }
|
||||
///
|
||||
/// criterion_group! {
|
||||
/// name = benches;
|
||||
/// config = custom();
|
||||
/// targets = fibonacci_profiled
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The neat thing about this is that it will sample _only_ the benchmark, and
|
||||
/// not other stuff like the setup process.
|
||||
///
|
||||
/// Further, it will only kick in if `--profile-time <time>` is passed to the
|
||||
/// benchmark binary. A flamegraph will be created for each individual benchmark
|
||||
/// in its report directory under `profile/flamegraph.svg`.
|
||||
///
|
||||
/// [custom-profiler]: https://bheisler.github.io/criterion.rs/book/user_guide/profiling.html#implementing-in-process-profiling-hooks
|
||||
pub struct FlamegraphProfiler<'a> {
|
||||
frequency: c_int,
|
||||
active_profiler: Option<ProfilerGuard<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> FlamegraphProfiler<'a> {
|
||||
pub fn new(frequency: c_int) -> Self {
|
||||
FlamegraphProfiler { frequency, active_profiler: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Profiler for FlamegraphProfiler<'a> {
|
||||
fn start_profiling(&mut self, _benchmark_id: &str, _benchmark_dir: &Path) {
|
||||
self.active_profiler = Some(ProfilerGuard::new(self.frequency).unwrap());
|
||||
}
|
||||
|
||||
fn stop_profiling(&mut self, _benchmark_id: &str, benchmark_dir: &Path) {
|
||||
std::fs::create_dir_all(benchmark_dir)
|
||||
.expect("Can't create a directory to store the benchmarking report");
|
||||
|
||||
let flamegraph_path = benchmark_dir.join("flamegraph.svg");
|
||||
|
||||
let flamegraph_file = File::create(&flamegraph_path)
|
||||
.expect("File system error while creating flamegraph.svg");
|
||||
|
||||
if let Some(profiler) = self.active_profiler.take() {
|
||||
profiler
|
||||
.report()
|
||||
.build()
|
||||
.expect("Can't build profiling report")
|
||||
.flamegraph(flamegraph_file)
|
||||
.expect("Error writing flamegraph");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
// 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.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(test)]
|
||||
use std::convert::TryFrom;
|
||||
use std::mem;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use atomic::Atomic;
|
||||
|
||||
#[cfg(test)]
|
||||
use super::OlmMachine;
|
||||
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
|
||||
use matrix_sdk_common::events::Algorithm;
|
||||
use matrix_sdk_common::identifiers::{DeviceId, UserId};
|
||||
|
||||
/// A device represents a E2EE capable client of an user.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Device {
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<DeviceId>,
|
||||
algorithms: Arc<Vec<Algorithm>>,
|
||||
keys: Arc<BTreeMap<KeyAlgorithm, String>>,
|
||||
display_name: Arc<Option<String>>,
|
||||
deleted: Arc<AtomicBool>,
|
||||
trust_state: Arc<Atomic<TrustState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// The trust state of a device.
|
||||
pub enum TrustState {
|
||||
/// The device has been verified and is trusted.
|
||||
Verified = 0,
|
||||
/// The device been blacklisted from communicating.
|
||||
BlackListed = 1,
|
||||
/// The trust state of the device is being ignored.
|
||||
Ignored = 2,
|
||||
/// The trust state is unset.
|
||||
Unset = 3,
|
||||
}
|
||||
|
||||
impl From<i64> for TrustState {
|
||||
fn from(state: i64) -> Self {
|
||||
match state {
|
||||
0 => TrustState::Verified,
|
||||
1 => TrustState::BlackListed,
|
||||
2 => TrustState::Ignored,
|
||||
3 => TrustState::Unset,
|
||||
_ => TrustState::Unset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Create a new Device.
|
||||
pub fn new(
|
||||
user_id: UserId,
|
||||
device_id: DeviceId,
|
||||
display_name: Option<String>,
|
||||
trust_state: TrustState,
|
||||
algorithms: Vec<Algorithm>,
|
||||
keys: BTreeMap<KeyAlgorithm, String>,
|
||||
) -> Self {
|
||||
Device {
|
||||
user_id: Arc::new(user_id),
|
||||
device_id: Arc::new(device_id),
|
||||
display_name: Arc::new(display_name),
|
||||
trust_state: Arc::new(Atomic::new(trust_state)),
|
||||
algorithms: Arc::new(algorithms),
|
||||
keys: Arc::new(keys),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The user id of the device owner.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
/// The unique ID of the device.
|
||||
pub fn device_id(&self) -> &DeviceId {
|
||||
&self.device_id
|
||||
}
|
||||
|
||||
/// Get the human readable name of the device.
|
||||
pub fn display_name(&self) -> &Option<String> {
|
||||
&self.display_name
|
||||
}
|
||||
|
||||
/// Get the key of the given key algorithm belonging to this device.
|
||||
pub fn get_key(&self, algorithm: KeyAlgorithm) -> Option<&String> {
|
||||
self.keys.get(&algorithm)
|
||||
}
|
||||
|
||||
/// Get a map containing all the device keys.
|
||||
pub fn keys(&self) -> &BTreeMap<KeyAlgorithm, String> {
|
||||
&self.keys
|
||||
}
|
||||
|
||||
/// Get the trust state of the device.
|
||||
pub fn trust_state(&self) -> TrustState {
|
||||
self.trust_state.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Get the list of algorithms this device supports.
|
||||
pub fn algorithms(&self) -> &[Algorithm] {
|
||||
&self.algorithms
|
||||
}
|
||||
|
||||
/// Is the device deleted.
|
||||
pub fn deleted(&self) -> bool {
|
||||
self.deleted.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Update a device with a new device keys struct.
|
||||
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) {
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (key_id, key) in device_keys.keys.iter() {
|
||||
let key_id = key_id.0;
|
||||
let _ = keys.insert(key_id, key.clone());
|
||||
}
|
||||
|
||||
let display_name = Arc::new(
|
||||
device_keys
|
||||
.unsigned
|
||||
.as_ref()
|
||||
.map(|d| d.device_display_name.clone())
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
let _ = mem::replace(
|
||||
&mut self.algorithms,
|
||||
Arc::new(device_keys.algorithms.clone()),
|
||||
);
|
||||
let _ = mem::replace(&mut self.keys, Arc::new(keys));
|
||||
let _ = mem::replace(&mut self.display_name, display_name);
|
||||
}
|
||||
|
||||
/// Mark the device as deleted.
|
||||
pub(crate) fn mark_as_deleted(&self) {
|
||||
self.deleted.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<&OlmMachine> for Device {
|
||||
fn from(machine: &OlmMachine) -> Self {
|
||||
Device {
|
||||
user_id: Arc::new(machine.user_id().clone()),
|
||||
device_id: Arc::new(machine.device_id().clone()),
|
||||
algorithms: Arc::new(vec![
|
||||
Algorithm::MegolmV1AesSha2,
|
||||
Algorithm::OlmV1Curve25519AesSha2,
|
||||
]),
|
||||
keys: Arc::new(
|
||||
machine
|
||||
.identity_keys()
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
KeyAlgorithm::try_from(key.as_ref()).unwrap(),
|
||||
value.to_owned(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
display_name: Arc::new(None),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&DeviceKeys> for Device {
|
||||
fn from(device_keys: &DeviceKeys) -> Self {
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (key_id, key) in device_keys.keys.iter() {
|
||||
let key_id = key_id.0;
|
||||
let _ = keys.insert(key_id, key.clone());
|
||||
}
|
||||
|
||||
Device {
|
||||
user_id: Arc::new(device_keys.user_id.clone()),
|
||||
device_id: Arc::new(device_keys.device_id.clone()),
|
||||
algorithms: Arc::new(device_keys.algorithms.clone()),
|
||||
keys: Arc::new(keys),
|
||||
display_name: Arc::new(
|
||||
device_keys
|
||||
.unsigned
|
||||
.as_ref()
|
||||
.map(|d| d.device_display_name.clone())
|
||||
.flatten(),
|
||||
),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.user_id() == other.user_id() && self.device_id() == other.device_id()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use serde_json::json;
|
||||
use std::convert::{From, TryFrom};
|
||||
|
||||
use crate::device::{Device, TrustState};
|
||||
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
|
||||
use matrix_sdk_common::identifiers::UserId;
|
||||
|
||||
fn device_keys() -> DeviceKeys {
|
||||
let user_id = UserId::try_from("@alice:example.org").unwrap();
|
||||
let device_id = "DEVICEID";
|
||||
|
||||
let device_keys = json!({
|
||||
"algorithms": vec![
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": device_id,
|
||||
"user_id": user_id.to_string(),
|
||||
"keys": {
|
||||
"curve25519:DEVICEID": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
|
||||
"ed25519:DEVICEID": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
|
||||
},
|
||||
"signatures": {
|
||||
user_id.to_string(): {
|
||||
"ed25519:DEVICEID": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA"
|
||||
}
|
||||
},
|
||||
"unsigned": {
|
||||
"device_display_name": "Alice's mobile phone"
|
||||
}
|
||||
});
|
||||
|
||||
serde_json::from_value(device_keys).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device() -> Device {
|
||||
let device_keys = device_keys();
|
||||
Device::from(&device_keys)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_a_device() {
|
||||
let user_id = UserId::try_from("@alice:example.org").unwrap();
|
||||
let device_id = "DEVICEID";
|
||||
|
||||
let device = get_device();
|
||||
|
||||
assert_eq!(&user_id, device.user_id());
|
||||
assert_eq!(device_id, device.device_id());
|
||||
assert_eq!(device.algorithms.len(), 2);
|
||||
assert_eq!(TrustState::Unset, device.trust_state());
|
||||
assert_eq!(
|
||||
"Alice's mobile phone",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
device.get_key(KeyAlgorithm::Curve25519).unwrap(),
|
||||
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
|
||||
);
|
||||
assert_eq!(
|
||||
device.get_key(KeyAlgorithm::Ed25519).unwrap(),
|
||||
"nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_a_device() {
|
||||
let mut device = get_device();
|
||||
|
||||
assert_eq!(
|
||||
"Alice's mobile phone",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
|
||||
let mut device_keys = device_keys();
|
||||
device_keys.unsigned.as_mut().unwrap().device_display_name =
|
||||
Some("Alice's work computer".to_owned());
|
||||
device.update_device(&device_keys);
|
||||
|
||||
assert_eq!(
|
||||
"Alice's work computer",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_device() {
|
||||
let device = get_device();
|
||||
assert!(!device.deleted());
|
||||
|
||||
let device_clone = device.clone();
|
||||
|
||||
device.mark_as_deleted();
|
||||
assert!(device.deleted());
|
||||
assert!(device_clone.deleted());
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use cjson::Error as CjsonError;
|
||||
use olm_rs::errors::{OlmGroupSessionError, OlmSessionError};
|
||||
use ruma::{identifiers::Error as IdentifierError, DeviceId, UserId};
|
||||
use serde_json::Error as SerdeError;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -47,8 +47,23 @@ pub enum OlmError {
|
||||
Store(#[from] CryptoStoreError),
|
||||
|
||||
/// The session with a device has become corrupted.
|
||||
#[error("decryption failed likely because a Olm session was wedged")]
|
||||
SessionWedged,
|
||||
#[error(
|
||||
"decryption failed likely because an Olm session from {0} with sender key {1} was wedged"
|
||||
)]
|
||||
SessionWedged(UserId, String),
|
||||
|
||||
/// An Olm message got replayed while the Olm ratchet has already moved
|
||||
/// forward.
|
||||
#[error("decryption failed because an Olm message from {0} with sender key {1} was replayed")]
|
||||
ReplayedMessage(UserId, String),
|
||||
|
||||
/// Encryption failed because the device does not have a valid Olm session
|
||||
/// with us.
|
||||
#[error(
|
||||
"encryption failed because the device does not \
|
||||
have a valid Olm session with us"
|
||||
)]
|
||||
MissingSession,
|
||||
}
|
||||
|
||||
/// Error representing a failure during a group encryption operation.
|
||||
@@ -71,6 +86,10 @@ pub enum MegolmError {
|
||||
#[error("can't finish Olm group session operation {0}")]
|
||||
OlmGroupSession(#[from] OlmGroupSessionError),
|
||||
|
||||
/// The room where a group session should be shared is not encrypted.
|
||||
#[error("The room where a group session should be shared is not encrypted")]
|
||||
EncryptionNotEnabled,
|
||||
|
||||
/// The storage layer returned an error.
|
||||
#[error(transparent)]
|
||||
Store(#[from] CryptoStoreError),
|
||||
@@ -93,6 +112,9 @@ pub enum EventError {
|
||||
#[error("the Encrypted message is missing the signing key of the sender")]
|
||||
MissingSigningKey,
|
||||
|
||||
#[error("the Encrypted message is missing the sender key")]
|
||||
MissingSenderKey,
|
||||
|
||||
#[error("the Encrypted message is missing the field {0}")]
|
||||
MissingField(String),
|
||||
|
||||
@@ -104,22 +126,66 @@ pub enum EventError {
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum SignatureError {
|
||||
pub enum SessionUnpicklingError {
|
||||
/// The underlying Olm session operation returned an error.
|
||||
#[error("can't finish Olm Session operation {0}")]
|
||||
OlmSession(#[from] OlmSessionError),
|
||||
/// The Session timestamp was invalid.
|
||||
#[error("can't load session timestamps")]
|
||||
SessionTimestampError,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SignatureError {
|
||||
#[error("the signature used a unsupported algorithm")]
|
||||
UnsupportedAlgorithm,
|
||||
|
||||
#[error("the key id of the signing key is invalid")]
|
||||
InvalidKeyId(#[from] IdentifierError),
|
||||
|
||||
#[error("the signing key is missing from the object that signed the message")]
|
||||
MissingSigningKey,
|
||||
|
||||
#[error("the user id of the signing differs from the subkey user id")]
|
||||
UserIdMissmatch,
|
||||
|
||||
#[error("the provided JSON value isn't an object")]
|
||||
NotAnObject,
|
||||
|
||||
#[error("the provided JSON object doesn't contain a signatures field")]
|
||||
NoSignatureFound,
|
||||
|
||||
#[error("the provided JSON object can't be converted to a canonical representation")]
|
||||
CanonicalJsonError(CjsonError),
|
||||
|
||||
#[error("the signature didn't match the provided key")]
|
||||
VerificationError,
|
||||
|
||||
#[error(transparent)]
|
||||
JsonError(#[from] SerdeError),
|
||||
}
|
||||
|
||||
impl From<CjsonError> for SignatureError {
|
||||
fn from(error: CjsonError) -> Self {
|
||||
Self::CanonicalJsonError(error)
|
||||
}
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum SessionCreationError {
|
||||
#[error(
|
||||
"Failed to create a new Olm session for {0} {1}, the requested \
|
||||
one-time key isn't a signed curve key"
|
||||
)]
|
||||
OneTimeKeyNotSigned(UserId, Box<DeviceId>),
|
||||
#[error(
|
||||
"Tried to create a new Olm session for {0} {1}, but the signed \
|
||||
one-time key is missing"
|
||||
)]
|
||||
OneTimeKeyMissing(UserId, Box<DeviceId>),
|
||||
#[error(
|
||||
"Tried to create a new Olm session for {0} {1}, but the one-time \
|
||||
key algorithm is unsupported"
|
||||
)]
|
||||
OneTimeKeyUnknown(UserId, Box<DeviceId>),
|
||||
#[error("Failed to verify the one-time key signatures for {0} {1}: {2:?}")]
|
||||
InvalidSignature(UserId, Box<DeviceId>, SignatureError),
|
||||
#[error(
|
||||
"Tried to create an Olm session for {0} {1}, but the device is missing \
|
||||
a curve25519 key"
|
||||
)]
|
||||
DeviceMissingCurveKey(UserId, Box<DeviceId>),
|
||||
#[error("Error creating new Olm session for {0} {1}: {2:?}")]
|
||||
OlmError(UserId, Box<DeviceId>, OlmSessionError),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::{Error as IoError, ErrorKind, Read},
|
||||
};
|
||||
|
||||
use aes::{
|
||||
cipher::{generic_array::GenericArray, FromBlockCipher, NewBlockCipher, StreamCipher},
|
||||
Aes256, Aes256Ctr,
|
||||
};
|
||||
use base64::DecodeError;
|
||||
use getrandom::getrandom;
|
||||
use ruma::events::room::{EncryptedFile, JsonWebKey, JsonWebKeyInit};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::utilities::{decode, decode_url_safe, encode, encode_url_safe};
|
||||
|
||||
const IV_SIZE: usize = 16;
|
||||
const KEY_SIZE: usize = 32;
|
||||
const VERSION: &str = "v2";
|
||||
|
||||
/// A wrapper that transparently encrypts anything that implements `Read` as an
|
||||
/// Matrix attachment.
|
||||
pub struct AttachmentDecryptor<'a, R: 'a + Read> {
|
||||
inner: &'a mut R,
|
||||
expected_hash: Vec<u8>,
|
||||
sha: Sha256,
|
||||
aes: Aes256Ctr,
|
||||
}
|
||||
|
||||
impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor<'a, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AttachmentDecryptor")
|
||||
.field("inner", &self.inner)
|
||||
.field("expected_hash", &self.expected_hash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let read_bytes = self.inner.read(buf)?;
|
||||
|
||||
if read_bytes == 0 {
|
||||
let hash = self.sha.finalize_reset();
|
||||
|
||||
if hash.as_slice() == self.expected_hash.as_slice() {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(IoError::new(ErrorKind::Other, "Hash mismatch while decrypting"))
|
||||
}
|
||||
} else {
|
||||
self.sha.update(&buf[0..read_bytes]);
|
||||
self.aes.apply_keystream(&mut buf[0..read_bytes]);
|
||||
|
||||
Ok(read_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for attachment decryption.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecryptorError {
|
||||
/// Some data in the encrypted attachment coldn't be decoded, this may be a
|
||||
/// hash, the secret key, or the initialization vector.
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
/// A hash is missing from the encryption info.
|
||||
#[error("The encryption info is missing a hash")]
|
||||
MissingHash,
|
||||
/// The supplied key or IV has an invalid length.
|
||||
#[error("The supplied key or IV has an invalid length.")]
|
||||
KeyNonceLength,
|
||||
/// The supplied data was encrypted with an unknown version of the
|
||||
/// attachment encryption spec.
|
||||
#[error("Unknown version for the encrypted attachment.")]
|
||||
UnknownVersion,
|
||||
}
|
||||
|
||||
impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> {
|
||||
/// Wrap the given reader decrypting all the data we read from it.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reader` - The `Reader` that should be wrapped and decrypted.
|
||||
///
|
||||
/// * `info` - The encryption info that is necessary to decrypt data from
|
||||
/// the reader.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::io::{Cursor, Read};
|
||||
/// # use matrix_sdk_crypto::{AttachmentEncryptor, AttachmentDecryptor};
|
||||
/// let data = "Hello world".to_owned();
|
||||
/// let mut cursor = Cursor::new(data.clone());
|
||||
///
|
||||
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
|
||||
///
|
||||
/// let mut encrypted = Vec::new();
|
||||
/// encryptor.read_to_end(&mut encrypted).unwrap();
|
||||
/// let info = encryptor.finish();
|
||||
///
|
||||
/// let mut cursor = Cursor::new(encrypted);
|
||||
/// let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).unwrap();
|
||||
/// let mut decrypted_data = Vec::new();
|
||||
/// decryptor.read_to_end(&mut decrypted_data).unwrap();
|
||||
///
|
||||
/// let decrypted = String::from_utf8(decrypted_data).unwrap();
|
||||
/// ```
|
||||
pub fn new(
|
||||
input: &'a mut R,
|
||||
info: EncryptionInfo,
|
||||
) -> Result<AttachmentDecryptor<'a, R>, DecryptorError> {
|
||||
if info.version != VERSION {
|
||||
return Err(DecryptorError::UnknownVersion);
|
||||
}
|
||||
|
||||
let hash = decode(info.hashes.get("sha256").ok_or(DecryptorError::MissingHash)?)?;
|
||||
let key = Zeroizing::from(decode_url_safe(info.web_key.k)?);
|
||||
let iv = decode(info.iv)?;
|
||||
let iv = GenericArray::from_exact_iter(iv).ok_or(DecryptorError::KeyNonceLength)?;
|
||||
|
||||
let sha = Sha256::default();
|
||||
let aes = Aes256::new_from_slice(&key).map_err(|_| DecryptorError::KeyNonceLength)?;
|
||||
let aes = Aes256Ctr::from_block_cipher(aes, &iv);
|
||||
|
||||
Ok(AttachmentDecryptor { inner: input, expected_hash: hash, sha, aes })
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that transparently encrypts anything that implements `Read`.
|
||||
pub struct AttachmentEncryptor<'a, R: Read + 'a> {
|
||||
finished: bool,
|
||||
inner: &'a mut R,
|
||||
web_key: JsonWebKey,
|
||||
iv: String,
|
||||
hashes: BTreeMap<String, String>,
|
||||
aes: Aes256Ctr,
|
||||
sha: Sha256,
|
||||
}
|
||||
|
||||
impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentEncryptor<'a, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AttachmentEncryptor")
|
||||
.field("inner", &self.inner)
|
||||
.field("finished", &self.finished)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let read_bytes = self.inner.read(buf)?;
|
||||
|
||||
if read_bytes == 0 {
|
||||
let hash = self.sha.finalize_reset();
|
||||
self.hashes.entry("sha256".to_owned()).or_insert_with(|| encode(hash));
|
||||
Ok(0)
|
||||
} else {
|
||||
self.aes.apply_keystream(&mut buf[0..read_bytes]);
|
||||
self.sha.update(&buf[0..read_bytes]);
|
||||
|
||||
Ok(read_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> {
|
||||
/// Wrap the given reader encrypting all the data we read from it.
|
||||
///
|
||||
/// After all the reads are done, and all the data is encrypted that we wish
|
||||
/// to encrypt a call to [`finish()`](#method.finish) is necessary to get
|
||||
/// the decryption key for the data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reader` - The `Reader` that should be wrapped and enrypted.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if we can't generate enough random data to create a fresh
|
||||
/// encryption key.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::io::{Cursor, Read};
|
||||
/// # use matrix_sdk_crypto::AttachmentEncryptor;
|
||||
/// let data = "Hello world".to_owned();
|
||||
/// let mut cursor = Cursor::new(data.clone());
|
||||
///
|
||||
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
|
||||
///
|
||||
/// let mut encrypted = Vec::new();
|
||||
/// encryptor.read_to_end(&mut encrypted).unwrap();
|
||||
/// let key = encryptor.finish();
|
||||
/// ```
|
||||
pub fn new(reader: &'a mut R) -> Self {
|
||||
let mut key = Zeroizing::new([0u8; KEY_SIZE]);
|
||||
let mut iv = Zeroizing::new([0u8; IV_SIZE]);
|
||||
|
||||
getrandom(&mut *key).expect("Can't generate randomness");
|
||||
// Only populate the first 8 bits with randomness, the rest is 0
|
||||
// initialized.
|
||||
getrandom(&mut iv[0..8]).expect("Can't generate randomness");
|
||||
|
||||
let web_key = JsonWebKey::from(JsonWebKeyInit {
|
||||
kty: "oct".to_owned(),
|
||||
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
|
||||
alg: "A256CTR".to_owned(),
|
||||
k: encode_url_safe(&*key),
|
||||
ext: true,
|
||||
});
|
||||
let encoded_iv = encode(&*iv);
|
||||
let iv = GenericArray::from_slice(&*iv);
|
||||
let key = GenericArray::from_slice(&*key);
|
||||
|
||||
let aes = Aes256::new(key);
|
||||
let aes = Aes256Ctr::from_block_cipher(aes, iv);
|
||||
|
||||
AttachmentEncryptor {
|
||||
finished: false,
|
||||
inner: reader,
|
||||
iv: encoded_iv,
|
||||
web_key,
|
||||
hashes: BTreeMap::new(),
|
||||
aes,
|
||||
sha: Sha256::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the encryptor and get the encryption key.
|
||||
pub fn finish(mut self) -> EncryptionInfo {
|
||||
let hash = self.sha.finalize();
|
||||
self.hashes.entry("sha256".to_owned()).or_insert_with(|| encode(hash));
|
||||
|
||||
EncryptionInfo {
|
||||
version: VERSION.to_string(),
|
||||
hashes: self.hashes,
|
||||
iv: self.iv,
|
||||
web_key: self.web_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct holding all the information that is needed to decrypt an encrypted
|
||||
/// file.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EncryptionInfo {
|
||||
#[serde(rename = "v")]
|
||||
/// The version of the encryption scheme.
|
||||
pub version: String,
|
||||
/// The web key that was used to encrypt the file.
|
||||
pub web_key: JsonWebKey,
|
||||
/// The initialization vector that was used to encrypt the file.
|
||||
pub iv: String,
|
||||
/// The hashes that can be used to check the validity of the file.
|
||||
pub hashes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl From<EncryptedFile> for EncryptionInfo {
|
||||
fn from(file: EncryptedFile) -> Self {
|
||||
Self { version: file.v, web_key: file.key, iv: file.iv, hashes: file.hashes }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::{AttachmentDecryptor, AttachmentEncryptor, EncryptionInfo};
|
||||
|
||||
const EXAMPLE_DATA: &[u8] = &[
|
||||
179, 154, 118, 127, 186, 127, 110, 33, 203, 33, 33, 134, 67, 100, 173, 46, 235, 27, 215,
|
||||
172, 36, 26, 75, 47, 33, 160,
|
||||
];
|
||||
|
||||
fn example_key() -> EncryptionInfo {
|
||||
let info = json!({
|
||||
"v": "v2",
|
||||
"web_key": {
|
||||
"kty": "oct",
|
||||
"alg": "A256CTR",
|
||||
"ext": true,
|
||||
"k": "Voq2nkPme_x8no5-Tjq_laDAdxE6iDbxnlQXxwFPgE4",
|
||||
"key_ops": ["encrypt", "decrypt"]
|
||||
},
|
||||
"iv": "i0DovxYdJEcAAAAAAAAAAA",
|
||||
"hashes": {
|
||||
"sha256": "ANdt819a8bZl4jKy3Z+jcqtiNICa2y0AW4BBJ/iQRAU"
|
||||
}
|
||||
});
|
||||
|
||||
serde_json::from_value(info).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_cycle() {
|
||||
let data = "Hello world".to_owned();
|
||||
let mut cursor = Cursor::new(data.clone());
|
||||
|
||||
let mut encryptor = AttachmentEncryptor::new(&mut cursor);
|
||||
|
||||
let mut encrypted = Vec::new();
|
||||
|
||||
encryptor.read_to_end(&mut encrypted).unwrap();
|
||||
let key = encryptor.finish();
|
||||
assert_ne!(encrypted.as_slice(), data.as_bytes());
|
||||
|
||||
let mut cursor = Cursor::new(encrypted);
|
||||
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
decryptor.read_to_end(&mut decrypted_data).unwrap();
|
||||
|
||||
let decrypted = String::from_utf8(decrypted_data).unwrap();
|
||||
|
||||
assert_eq!(data, decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_decrypt() {
|
||||
let mut cursor = Cursor::new(EXAMPLE_DATA.to_vec());
|
||||
let key = example_key();
|
||||
|
||||
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
decryptor.read_to_end(&mut decrypted_data).unwrap();
|
||||
let decrypted = String::from_utf8(decrypted_data).unwrap();
|
||||
|
||||
assert_eq!("It's a secret to everybody", decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_invalid_hash() {
|
||||
let mut cursor = Cursor::new("fake message");
|
||||
let key = example_key();
|
||||
|
||||
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
assert!(decryptor.read_to_end(&mut decrypted_data).is_err())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// 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.
|
||||
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
use aes::{
|
||||
cipher::{generic_array::GenericArray, FromBlockCipher, NewBlockCipher, StreamCipher},
|
||||
Aes256, Aes256Ctr,
|
||||
};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use getrandom::getrandom;
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use pbkdf2::pbkdf2;
|
||||
use serde_json::Error as SerdeError;
|
||||
use sha2::{Sha256, Sha512};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
olm::ExportedRoomKey,
|
||||
utilities::{decode, encode, DecodeError},
|
||||
};
|
||||
|
||||
const SALT_SIZE: usize = 16;
|
||||
const IV_SIZE: usize = 16;
|
||||
const MAC_SIZE: usize = 32;
|
||||
const KEY_SIZE: usize = 32;
|
||||
const VERSION: u8 = 1;
|
||||
|
||||
const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----";
|
||||
const FOOTER: &str = "-----END MEGOLM SESSION DATA-----";
|
||||
|
||||
/// Error representing a failure during key export or import.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KeyExportError {
|
||||
/// The key export doesn't contain valid headers.
|
||||
#[error("Invalid or missing key export headers.")]
|
||||
InvalidHeaders,
|
||||
/// The key export has been encrypted with an unsupported version.
|
||||
#[error("The key export has been encrypted with an unsupported version.")]
|
||||
UnsupportedVersion,
|
||||
/// The MAC of the encrypted payload is invalid.
|
||||
#[error("The MAC of the encrypted payload is invalid.")]
|
||||
InvalidMac,
|
||||
/// The decrypted key export isn't valid UTF-8.
|
||||
#[error(transparent)]
|
||||
InvalidUtf8(#[from] std::string::FromUtf8Error),
|
||||
/// The decrypted key export doesn't contain valid JSON.
|
||||
#[error(transparent)]
|
||||
Json(#[from] SerdeError),
|
||||
/// The key export string isn't valid base64.
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
/// The key export doesn't all the required fields.
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Try to decrypt a reader into a list of exported room keys.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that was used to encrypt the exported keys.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// # use std::io::Cursor;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_key_export};
|
||||
/// # use ruma::user_id;
|
||||
/// # use futures::executor::block_on;
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
|
||||
/// # block_on(async {
|
||||
/// # let export = Cursor::new("".to_owned());
|
||||
/// let exported_keys = decrypt_key_export(export, "1234").unwrap();
|
||||
/// machine.import_keys(exported_keys, |_, _| {}).await.unwrap();
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn decrypt_key_export(
|
||||
mut input: impl Read,
|
||||
passphrase: &str,
|
||||
) -> Result<Vec<ExportedRoomKey>, KeyExportError> {
|
||||
let mut x: String = String::new();
|
||||
|
||||
input.read_to_string(&mut x)?;
|
||||
|
||||
if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) {
|
||||
return Err(KeyExportError::InvalidHeaders);
|
||||
}
|
||||
|
||||
let payload: String =
|
||||
x.lines().filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER))).collect();
|
||||
|
||||
Ok(serde_json::from_str(&decrypt_helper(&payload, passphrase)?)?)
|
||||
}
|
||||
|
||||
/// Encrypt the list of exported room keys using the given passphrase.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - A list of sessions that should be encrypted.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that will be used to encrypt the exported
|
||||
/// room keys.
|
||||
///
|
||||
/// * `rounds` - The number of rounds that should be used for the key
|
||||
/// derivation when the passphrase gets turned into an AES key. More rounds are
|
||||
/// increasingly computationally intensive and as such help against brute-force
|
||||
/// attacks. Should be at least `10000`, while values in the `100000` ranges
|
||||
/// should be preferred.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method will panic if it can't get enough randomness from the OS to
|
||||
/// encrypt the exported keys securely.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export};
|
||||
/// # use ruma::{user_id, room_id};
|
||||
/// # use futures::executor::block_on;
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
|
||||
/// # block_on(async {
|
||||
/// let room_id = room_id!("!test:localhost");
|
||||
/// let exported_keys = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap();
|
||||
/// let encrypted_export = encrypt_key_export(&exported_keys, "1234", 1);
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn encrypt_key_export(
|
||||
keys: &[ExportedRoomKey],
|
||||
passphrase: &str,
|
||||
rounds: u32,
|
||||
) -> Result<String, SerdeError> {
|
||||
let mut plaintext = serde_json::to_string(keys)?.into_bytes();
|
||||
let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds);
|
||||
Ok([HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n"))
|
||||
}
|
||||
|
||||
fn encrypt_helper(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String {
|
||||
let mut salt = [0u8; SALT_SIZE];
|
||||
let mut iv = [0u8; IV_SIZE];
|
||||
let mut derived_keys = [0u8; KEY_SIZE * 2];
|
||||
|
||||
getrandom(&mut salt).expect("Can't generate randomness");
|
||||
getrandom(&mut iv).expect("Can't generate randomness");
|
||||
|
||||
let mut iv = u128::from_be_bytes(iv);
|
||||
iv &= !(1 << 63);
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
|
||||
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
|
||||
|
||||
let key = GenericArray::from_slice(key);
|
||||
let iv = iv.to_be_bytes();
|
||||
let iv = GenericArray::from_slice(&iv);
|
||||
|
||||
let aes = Aes256::new(key);
|
||||
let mut aes = Aes256Ctr::from_block_cipher(aes, iv);
|
||||
|
||||
aes.apply_keystream(&mut plaintext);
|
||||
|
||||
let mut payload: Vec<u8> = vec![];
|
||||
|
||||
payload.extend(&VERSION.to_be_bytes());
|
||||
payload.extend(&salt);
|
||||
payload.extend(&*iv);
|
||||
payload.extend(&rounds.to_be_bytes());
|
||||
payload.extend_from_slice(plaintext);
|
||||
|
||||
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("Can't create HMAC object");
|
||||
hmac.update(&payload);
|
||||
let mac = hmac.finalize();
|
||||
|
||||
payload.extend(mac.into_bytes());
|
||||
|
||||
encode(payload)
|
||||
}
|
||||
|
||||
fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExportError> {
|
||||
let decoded = decode(ciphertext)?;
|
||||
|
||||
let mut decoded = Cursor::new(decoded);
|
||||
|
||||
let mut salt = [0u8; SALT_SIZE];
|
||||
let mut iv = [0u8; IV_SIZE];
|
||||
let mut mac = [0u8; MAC_SIZE];
|
||||
let mut derived_keys = [0u8; KEY_SIZE * 2];
|
||||
|
||||
let version = decoded.read_u8()?;
|
||||
decoded.read_exact(&mut salt)?;
|
||||
decoded.read_exact(&mut iv)?;
|
||||
|
||||
let rounds = decoded.read_u32::<BigEndian>()?;
|
||||
let ciphertext_start = decoded.position() as usize;
|
||||
|
||||
decoded.seek(SeekFrom::End(-32))?;
|
||||
let ciphertext_end = decoded.position() as usize;
|
||||
|
||||
decoded.read_exact(&mut mac)?;
|
||||
|
||||
let mut decoded = decoded.into_inner();
|
||||
|
||||
if version != VERSION {
|
||||
return Err(KeyExportError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
|
||||
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
|
||||
|
||||
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("Can't create an HMAC object");
|
||||
hmac.update(&decoded[0..ciphertext_end]);
|
||||
hmac.verify(&mac).map_err(|_| KeyExportError::InvalidMac)?;
|
||||
|
||||
let key = GenericArray::from_slice(key);
|
||||
let iv = GenericArray::from_slice(&iv);
|
||||
|
||||
let mut ciphertext = &mut decoded[ciphertext_start..ciphertext_end];
|
||||
let aes = Aes256::new(key);
|
||||
let mut aes = Aes256Ctr::from_block_cipher(aes, iv);
|
||||
aes.apply_keystream(&mut ciphertext);
|
||||
|
||||
Ok(String::from_utf8(ciphertext.to_owned())?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::io::Cursor;
|
||||
|
||||
use indoc::indoc;
|
||||
use matrix_sdk_test::async_test;
|
||||
use proptest::prelude::*;
|
||||
use ruma::room_id;
|
||||
|
||||
use super::{decode, decrypt_helper, decrypt_key_export, encrypt_helper, encrypt_key_export};
|
||||
use crate::machine::test::get_prepared_machine;
|
||||
|
||||
const PASSPHRASE: &str = "1234";
|
||||
|
||||
const TEST_EXPORT: &str = indoc! {"
|
||||
-----BEGIN MEGOLM SESSION DATA-----
|
||||
Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo
|
||||
Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI
|
||||
CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU
|
||||
bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD
|
||||
ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9
|
||||
41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U
|
||||
XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53
|
||||
uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR
|
||||
RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo
|
||||
SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9
|
||||
MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ
|
||||
GYW19pdjg0qdXNk/eqZsQTsNWVo6A
|
||||
-----END MEGOLM SESSION DATA-----
|
||||
"};
|
||||
|
||||
fn export_wihtout_headers() -> String {
|
||||
TEST_EXPORT.lines().filter(|l| !l.starts_with("-----")).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let export = export_wihtout_headers();
|
||||
assert!(decode(export).is_ok());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) {
|
||||
let mut plaintext_bytes = plaintext.clone().into_bytes();
|
||||
|
||||
let ciphertext = encrypt_helper(&mut plaintext_bytes, "test", 1);
|
||||
let decrypted = decrypt_helper(&ciphertext, "test").unwrap();
|
||||
|
||||
prop_assert!(plaintext == decrypted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let data = "It's a secret to everybody";
|
||||
let mut bytes = data.to_owned().into_bytes();
|
||||
|
||||
let encrypted = encrypt_helper(&mut bytes, PASSPHRASE, 10);
|
||||
let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap();
|
||||
|
||||
assert_eq!(data, decrypted);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_session_encrypt() {
|
||||
let (machine, _) = get_prepared_machine().await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
machine.create_outbound_group_session_with_defaults(&room_id).await.unwrap();
|
||||
let export = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap();
|
||||
|
||||
assert!(!export.is_empty());
|
||||
|
||||
let encrypted = encrypt_key_export(&export, "1234", 1).unwrap();
|
||||
let decrypted = decrypt_key_export(Cursor::new(encrypted), "1234").unwrap();
|
||||
|
||||
assert_eq!(export, decrypted);
|
||||
assert_eq!(machine.import_keys(decrypted, |_, _| {}).await.unwrap(), (0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_decrypt() {
|
||||
let reader = Cursor::new(TEST_EXPORT);
|
||||
let imported = decrypt_key_export(reader, PASSPHRASE).expect("Can't decrypt key export");
|
||||
assert!(!imported.is_empty())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod attachments;
|
||||
mod key_export;
|
||||
|
||||
pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError, EncryptionInfo};
|
||||
pub use key_export::{decrypt_key_export, encrypt_key_export, KeyExportError};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user