Compare commits
2878 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20d82eb92f | |||
| 319e1d1191 | |||
| 107c8c0b1f | |||
| 8c6d9586bf | |||
| 1271fc6bf3 | |||
| c9df03c40c | |||
| 27f6745123 | |||
| 964f448334 | |||
| 20ee03bb44 | |||
| 77bd677182 | |||
| e024d047e3 | |||
| 40943edc06 | |||
| e6699c5424 | |||
| bd8a307e50 | |||
| f71301cafc | |||
| 562bf9331b | |||
| 11e6eb94b5 | |||
| cee3aa2a7a | |||
| 81e3783488 | |||
| fc7f9786f8 | |||
| 0808c0edf1 | |||
| 8de6746efd | |||
| eb9b8ef7c6 | |||
| b09621b915 | |||
| 8d667f9367 | |||
| 56dfe6630f | |||
| 8b3b181a48 | |||
| c952768542 | |||
| 1a368aa996 | |||
| 61449458cf | |||
| 4eb547e535 | |||
| b54acffaef | |||
| 65a1833e1f | |||
| 1ce4f25811 | |||
| 3127105516 | |||
| d59ea4be78 | |||
| f256f04440 | |||
| b444aaa67e | |||
| 745185e689 | |||
| 2bfa891f0a | |||
| 147167bed3 | |||
| 565e18e8a3 | |||
| 55b4595bbf | |||
| eeb2c463dc | |||
| d9bb0e9a52 | |||
| 7d07ab7c7e | |||
| f8ff3aac58 | |||
| 299a7728d1 | |||
| 39dc6a1742 | |||
| f21c5aa7f2 | |||
| e9bc3f26a5 | |||
| 23eaddd6ea | |||
| 8143ce8450 | |||
| 0a487ec43e | |||
| 0edb483802 | |||
| 06a32ce0a1 | |||
| 8cae00407a | |||
| a57ec87c67 | |||
| 4e62491ea4 | |||
| 5758029c1e | |||
| 8f08710c58 | |||
| 90f98105f0 | |||
| 90354aa330 | |||
| aaabebe7f5 | |||
| 80a92dcdc2 | |||
| dc9081e9d4 | |||
| 3c299637b6 | |||
| 07af333943 | |||
| 0bbc781d0c | |||
| 79bf64f079 | |||
| ed67d39456 | |||
| 2f8cc75432 | |||
| 03cccef805 | |||
| 6d5a0c2718 | |||
| 42b359eb5c | |||
| 3071587f11 | |||
| 23159807b0 | |||
| b1ba9f76b8 | |||
| 0e51dfed46 | |||
| 09b00335f8 | |||
| 3d274815d9 | |||
| 70d60b905d | |||
| 3e2ffb25a6 | |||
| 8b9bef5cb3 | |||
| 31e72efc91 | |||
| 60b7252597 | |||
| 3980b62df2 | |||
| b306df726a | |||
| 3d5a79be3b | |||
| ba78d1a9ae | |||
| 241811298f | |||
| 8a0ddc43ab | |||
| 898fa0e41b | |||
| 081ff4dec0 | |||
| 3c69b8511d | |||
| 6843d86ecf | |||
| 2e91200136 | |||
| 852304c417 | |||
| ee752e3885 | |||
| b9480e4302 | |||
| 2ae4d07971 | |||
| 90cac8a118 | |||
| db18274f6e | |||
| 172bad8b55 | |||
| dfe454e18f | |||
| 3d8dd29b4c | |||
| c3ff213ec9 | |||
| e80e5e1f8c | |||
| bba249d5ce | |||
| f57df2bee5 | |||
| b930638156 | |||
| 39c1de19fc | |||
| 17724fc8d3 | |||
| 4c6d11d9ed | |||
| 05d77a85c9 | |||
| e95a133cdd | |||
| c21382d721 | |||
| 8c15125e23 | |||
| 64ddbd97dd | |||
| 9c24bcb7a9 | |||
| 8f016726f0 | |||
| 649fe7a490 | |||
| 35f1cdf89c | |||
| 06adc34fb3 | |||
| 87bf07f95e | |||
| f05bf3f845 | |||
| ab512d087c | |||
| 6799c29921 | |||
| a3f1da1981 | |||
| 3b225651cc | |||
| a40d691159 | |||
| 4ebe60b2ad | |||
| 5a70859593 | |||
| c7be810e65 | |||
| 101217cfb6 | |||
| 5c2aa4677f | |||
| ab9bfa68ae | |||
| aa8c2ca277 | |||
| 84509087ac | |||
| 2450d461fd | |||
| 50c590ae26 | |||
| 516dff06ee | |||
| 9a8af05bfb | |||
| c9bf61c387 | |||
| 4f0f2e8c16 | |||
| 6f042a2142 | |||
| 91416bdbb2 | |||
| 9b093f7569 | |||
| b004d1602d | |||
| 6cca73b999 | |||
| fafd6df13e | |||
| 8f77870526 | |||
| eb0462e89b | |||
| 80748d7d85 | |||
| b694d53b73 | |||
| 8004e82c50 | |||
| 85b5849228 | |||
| 6b86777e96 | |||
| efe64a4817 | |||
| ad777f36b2 | |||
| de77ad867c | |||
| 9b35f86497 | |||
| 84fc8b1931 | |||
| 455c85fb69 | |||
| 2a2fed695b | |||
| c1f28bd410 | |||
| c74e0bb6b3 | |||
| 5b9e158035 | |||
| a8d200dd02 | |||
| 34ae967cb8 | |||
| a67f14825e | |||
| 50c14d0ab8 | |||
| 0edb6e6f6f | |||
| 36d0dacda1 | |||
| 83b74070aa | |||
| f80af68686 | |||
| fe4ac06f43 | |||
| eaaa3e980a | |||
| 07629bfb9a | |||
| 88fdeca2bf | |||
| a3c8eac38b | |||
| bd5380c0b4 | |||
| c3b5767999 | |||
| 9e5c2732c9 | |||
| b8957fa917 | |||
| 52c139dcdc | |||
| 39d4bf1494 | |||
| 4c713e3387 | |||
| bb486f5148 | |||
| 524fea1297 | |||
| e9528ebb98 | |||
| de18283c3b | |||
| cc1c7561a3 | |||
| 5d928f07a0 | |||
| 01b882480f | |||
| 21d52fdbdd | |||
| f9baff2a3a | |||
| 71eca4ffcc | |||
| bc8dca5105 | |||
| 3ae3dffff7 | |||
| 81c6023940 | |||
| 3a0f27fa7e | |||
| 60e339bac0 | |||
| ecb88f45b7 | |||
| 24a869d15b | |||
| 1d427a1ea8 | |||
| 90e25867ad | |||
| 2ae56e61cb | |||
| 56c0830328 | |||
| 093f139d34 | |||
| 1083efc212 | |||
| 69773c2619 | |||
| 2525b5a5d8 | |||
| ff9c84ff94 | |||
| 3aa2bf8a76 | |||
| a229ece693 | |||
| b435137332 | |||
| 2cdbc9f4db | |||
| aa6884e484 | |||
| 4f4d694687 | |||
| 1b47999e80 | |||
| 02427651dd | |||
| 8eeb088e50 | |||
| 3f62581556 | |||
| b53a7f6ee8 | |||
| a4591afba6 | |||
| 6ea4c77dd5 | |||
| dc3d90d696 | |||
| 00de919eb4 | |||
| fdacc2f7ab | |||
| 5a64c29228 | |||
| c5cddf1607 | |||
| 44c7844a4b | |||
| f293da5b34 | |||
| 4f044de79e | |||
| da116e6077 | |||
| 2489180c47 | |||
| 4ec4d330aa | |||
| c75ca1c2d6 | |||
| 67462e9fc4 | |||
| 424b6303ef | |||
| 59c4e2c354 | |||
| ecca8bc86e | |||
| 43ca920b10 | |||
| e1a3f8f053 | |||
| 6e7cb63e7d | |||
| ab1177d987 | |||
| ee0a1d281d | |||
| aef7b9a1dc | |||
| 7cb8de5b69 | |||
| 5c2fb1c42b | |||
| 553857583d | |||
| 6d0923153f | |||
| e34eb48914 | |||
| 05ab6ef3ab | |||
| 81eefc1377 | |||
| 895d854e1c | |||
| e432d4f808 | |||
| 56c0ae294b | |||
| 0b9d68b4f2 | |||
| e2e034f795 | |||
| bb5e3d51b8 | |||
| 70b23614b5 | |||
| 24a75e3765 | |||
| d694ee3ef3 | |||
| efbdf4e1a8 | |||
| 44bfc2e846 | |||
| 0121bdbb75 | |||
| b8ba77a7b5 | |||
| 65dd5cc6ad | |||
| 8aeb994839 | |||
| 64daa444dd | |||
| 26aab4f38d | |||
| 6059df1b67 | |||
| 2a0c85c772 | |||
| 3488fbe64c | |||
| 811a98ad19 | |||
| 4462f4b90e | |||
| 4143a79f7b | |||
| 3ed9b00398 | |||
| b005b75331 | |||
| a9f9e2cf35 | |||
| 5602b94dcb | |||
| 930de640ac | |||
| 6d9fba8191 | |||
| 624c6f0a6e | |||
| 7d2f7fae45 | |||
| 3f917b39c9 | |||
| f1336a5ce7 | |||
| 7a10d504b2 | |||
| 831aec6488 | |||
| 6eb229ac1e | |||
| c58db665dd | |||
| e222fb1783 | |||
| 31a0192c2d | |||
| 53f8091e3a | |||
| 012cbf7995 | |||
| ac26c91cba | |||
| c13162aada | |||
| 9fb6eea8b7 | |||
| 23c4f19cda | |||
| 3b34570749 | |||
| 0412ca5810 | |||
| c80518bf3e | |||
| 61ee6eb8af | |||
| 654e8b41fa | |||
| 7080458f7e | |||
| 08d236f5ec | |||
| e332a7d113 | |||
| 7879709f62 | |||
| 56e030762e | |||
| bac73150ca | |||
| 2e1fb15ada | |||
| ae9bcd6f6c | |||
| c18c679b9b | |||
| d014ee0b72 | |||
| a30ef7250b | |||
| 570ce4f4b7 | |||
| 3c7c9048eb | |||
| 41243757ee | |||
| 2af311bd7d | |||
| 1bc9ee7110 | |||
| 4e040f8e77 | |||
| 26c1c6db3b | |||
| d38da83656 | |||
| 58f163ed5c | |||
| c0c9f0122c | |||
| d33395e46d | |||
| b83c7d3929 | |||
| b5df016b1b | |||
| a8b6be3b38 | |||
| 78cf175f5a | |||
| 1b78856a7d | |||
| 8194287391 | |||
| 2eecea9a07 | |||
| 465032dd4f | |||
| e473315a89 | |||
| 9d34ad5287 | |||
| a532cc5cf9 | |||
| 60c6c5bc41 | |||
| 0cbbbe8503 | |||
| 47a8d3e50a | |||
| 304da09f3b | |||
| acd4dcb56e | |||
| c170456cde | |||
| 4e739e4b06 | |||
| 137c6919f6 | |||
| 842ce30190 | |||
| df1539040c | |||
| 2a04459bb2 | |||
| 6367bf7c75 | |||
| a0456dc430 | |||
| 52ec831b16 | |||
| 8263062fab | |||
| 9a2bf78a8e | |||
| cb16f7a60b | |||
| ad84631ddb | |||
| d78426d708 | |||
| a9543df6db | |||
| aae388be93 | |||
| 4ef970b4da | |||
| b199f133b3 | |||
| 7d1b183a1b | |||
| 49d119e92e | |||
| feed1da570 | |||
| a3ad835d84 | |||
| f8afee8ebd | |||
| 7e955fc312 | |||
| eebf92366f | |||
| 3c23e166a7 | |||
| 26a8439ce4 | |||
| 2c2e8fa1ac | |||
| ddc2fa74b9 | |||
| 93d51b83c3 | |||
| f83eae4a46 | |||
| 87c6d11fca | |||
| e87ac86e48 | |||
| de8063a43a | |||
| a73dabcb67 | |||
| 7782e81101 | |||
| aa70687d9e | |||
| 38d32de06b | |||
| 7720c72b73 | |||
| ff9505073f | |||
| 21ee1c31a7 | |||
| fd01ba1fcf | |||
| fbf53524ed | |||
| 1f2a701ace | |||
| 0b87a573b3 | |||
| 74438716af | |||
| d10b348e74 | |||
| 68e9be47d9 | |||
| bddd03c2fd | |||
| e23ba50dd8 | |||
| 261ab7ae68 | |||
| 21c8c76dc3 | |||
| 266d0f9d05 | |||
| 69d25c1498 | |||
| 3cd2b3925a | |||
| 33e9eb371e | |||
| 07572d1e8d | |||
| dde4f558f3 | |||
| 79d2574ea7 | |||
| 51fb5c4a15 | |||
| 875c6b973b | |||
| 21e1312dd7 | |||
| a722ef3b03 | |||
| 80ba5d29f2 | |||
| a35e6a0f54 | |||
| bdc1958c08 | |||
| 3f2bac71c6 | |||
| 6b9a11b697 | |||
| f17ecba519 | |||
| ce0b014a5a | |||
| ad48d2997e | |||
| 1c1781ce76 | |||
| 5fd001354a | |||
| a18bdad44f | |||
| 600dff62e8 | |||
| db7a402e9b | |||
| 0e53f9052f | |||
| 62e69cacb7 | |||
| 852c88c341 | |||
| 455f52f1f5 | |||
| df6012c58d | |||
| f68a3dde46 | |||
| 25e6b1cac8 | |||
| 4ad20526db | |||
| 18cd017f58 | |||
| 0161664b6c | |||
| 25df31bf96 | |||
| 09438b440e | |||
| 6a5f5b249e | |||
| 3a20114c39 | |||
| f411d50253 | |||
| 00851df25c | |||
| 8822d255b3 | |||
| 4b4ba86167 | |||
| 7ea820f6e1 | |||
| 53d8cf0852 | |||
| 761806c678 | |||
| d6abd639f3 | |||
| 6078bbbe24 | |||
| c1c81df4de | |||
| ee8a4698a9 | |||
| c1956d3f05 | |||
| 56316dc5d9 | |||
| b5c74b5666 | |||
| dc946dffbc | |||
| 937baadb9b | |||
| 8d0c03b4f0 | |||
| a3fba73044 | |||
| 116cf31199 | |||
| cdb78e4c75 | |||
| 0bb9c56e94 | |||
| 821f1c876b | |||
| e9b95f8567 | |||
| 103d811441 | |||
| bb4f5a3fa1 | |||
| f5cbdeac8f | |||
| 56062e8e4e | |||
| 03c85d48e5 | |||
| dd8f0fbdcb | |||
| fac61a76e9 | |||
| 3b09ab3ca1 | |||
| 16bfe79305 | |||
| d668f97c98 | |||
| 18bd10b03d | |||
| 6b1d089caf | |||
| af93401385 | |||
| fa5add3d99 | |||
| fb971580e0 | |||
| dcaea98e33 | |||
| b95079d1c5 | |||
| e59f36cdc0 | |||
| 0f2f041d8b | |||
| 33d2837ec3 | |||
| 7cede221de | |||
| fe47435fc7 | |||
| 491226a916 | |||
| deb7433453 | |||
| 14973a35c2 | |||
| b5779f8654 | |||
| f7d1984257 | |||
| 3fba683090 | |||
| 430da8ac09 | |||
| dcd9b5c382 | |||
| 348c293962 | |||
| 34309da10c | |||
| 6db973f430 | |||
| 9f27bafa62 | |||
| b05136146a | |||
| 13d3be637b | |||
| 44de7fad6f | |||
| b99243406b | |||
| 0e9ec811b0 | |||
| 6979177fb2 | |||
| 3aa8bfa6ca | |||
| 17b356b08e | |||
| f72ae490a8 | |||
| b0c3d0d2e3 | |||
| 9dc344999e | |||
| 663c096400 | |||
| 420b4d119d | |||
| 58b752c63b | |||
| 4740232fa4 | |||
| 0cc9994b8b | |||
| 1e78628b23 | |||
| 00f5ddc93c | |||
| f79f2105fd | |||
| 828c51467f | |||
| 963e271bce | |||
| 0ab41215f0 | |||
| d153c4da07 | |||
| 91aa783c3d | |||
| a54845bf76 | |||
| 8a56a5f1ed | |||
| f585c80491 | |||
| c495b12cef | |||
| 1d6f7f862f | |||
| 0a82c84006 | |||
| a614a02b90 | |||
| 0945e2c5c6 | |||
| b1b49413d0 | |||
| 01af303d63 | |||
| 751060305c | |||
| 389fcfaf3d | |||
| 3eb0c534a5 | |||
| 8a2f84b678 | |||
| dd00735409 | |||
| 6ba7e85e24 | |||
| 941d93c2f4 | |||
| c73e9cbc7c | |||
| ae89e4bf21 | |||
| d1e64d0cfb | |||
| 8f9f9590d9 | |||
| ed68093310 | |||
| 23655e748d | |||
| 98624871bd | |||
| 7b154c0834 | |||
| d9f056242f | |||
| a4268d288e | |||
| f90c91dded | |||
| 5c8890c3c1 | |||
| cd3c6809a9 | |||
| e2c17528c2 | |||
| e00f565f37 | |||
| 085e797c30 | |||
| d753db590b | |||
| eab074a27b | |||
| e2a3e3816f | |||
| 01dd57adab | |||
| 08e674b695 | |||
| 9f70970e61 | |||
| e9ffd5a125 | |||
| 20f4469361 | |||
| feac096dc2 | |||
| 67985d449a | |||
| a6de59c198 | |||
| 01eeb98e35 | |||
| 49a7defbf0 | |||
| c1ba5de686 | |||
| 81428f23d1 | |||
| 8af86bb746 | |||
| 8513f5c413 | |||
| eadec35093 | |||
| d6dbd621b8 | |||
| 7168f76614 | |||
| 1cda95f23c | |||
| bb1cd2bbce | |||
| 3f90ac5712 | |||
| 8d249a843c | |||
| 858b41d835 | |||
| 61aea05af0 | |||
| e7c764d5f5 | |||
| 09a9afe4e7 | |||
| 5a26503da7 | |||
| 5faf5ea1f8 | |||
| 0754c29c22 | |||
| d5c6dcf111 | |||
| 6a57ddd33c | |||
| bd711cdc1f | |||
| e669e493c9 | |||
| 48f290196c | |||
| f8985dbb39 | |||
| ef594d52e4 | |||
| 23bbb2f8c6 | |||
| 42f181cc7b | |||
| b3d2d39b60 | |||
| e323d917a4 | |||
| 73c7733ebc | |||
| 87f7f9443e | |||
| af6bbbc59b | |||
| 5b35a364a9 | |||
| d56ebadbc4 | |||
| 04accdeddc | |||
| 70575f9e33 | |||
| 8e16586d84 | |||
| 6920dfb800 | |||
| 02d93770aa | |||
| cd124231c5 | |||
| cd75848882 | |||
| 1bae15ede9 | |||
| 8c2001adbf | |||
| 79ca235e7c | |||
| 4570fcaa8a | |||
| 90670cf1be | |||
| cc86f427d2 | |||
| 2144791d52 | |||
| 33aabf44e7 | |||
| fc1ea27380 | |||
| 81946294d8 | |||
| 77270fa78c | |||
| 9e29289dcc | |||
| 6198943976 | |||
| 8beb836ccd | |||
| b7c0e39c1a | |||
| 777acae2e5 | |||
| e77389c1ce | |||
| eb24e2e1f1 | |||
| 114244f8bb | |||
| 54769d9136 | |||
| 2f2deb5333 | |||
| 37f106d4af | |||
| 36ee7cdbfc | |||
| 2b564498ee | |||
| 0ddba16fa1 | |||
| 550086eb67 | |||
| 055ce673cd | |||
| 5308595658 | |||
| e726e29f39 | |||
| 33b12fa6b5 | |||
| 829cd05cba | |||
| 4834e12a3a | |||
| bcd4ad130c | |||
| 998d9e010e | |||
| 5480e8e1d5 | |||
| 98fdcabc00 | |||
| 236397816d | |||
| 755c55de3e | |||
| 526da71992 | |||
| 86ef262799 | |||
| 282904d4be | |||
| a1be24307a | |||
| 4b5623691b | |||
| 7bdf1e9b92 | |||
| af1db8a606 | |||
| a99bb3c4c9 | |||
| fd155c15bd | |||
| aaa43631aa | |||
| d2557bc943 | |||
| 33a3506981 | |||
| 03a54353be | |||
| c6328923e6 | |||
| 1ecb820bb0 | |||
| 0be2319288 | |||
| 073a025b83 | |||
| e83836d487 | |||
| 065c61e05c | |||
| 139a6bd903 | |||
| 3fa0ee59d4 | |||
| bd3d26422d | |||
| 370ef9fc69 | |||
| a087fb37a3 | |||
| 68c8fe0fa9 | |||
| 4309749979 | |||
| 1a677804a4 | |||
| a427e2a75c | |||
| 3c735b0ac1 | |||
| 4f446c3909 | |||
| 999ed1b5b3 | |||
| 8fa19f4a0f | |||
| 71a01ec234 | |||
| 32f033a9da | |||
| dade385147 | |||
| 6cf2e54f9a | |||
| fb673b0304 | |||
| 1a425af3f2 | |||
| 9bafed2c26 | |||
| bb2d0b0f62 | |||
| 5e4f10a80c | |||
| 9e12fc4d7d | |||
| 1caf2b7f83 | |||
| 6f17e3e659 | |||
| 17e2cd755d | |||
| a6970d4de8 | |||
| bc99c1f3ce | |||
| 5e3ff7fc27 | |||
| ffe3f966fe | |||
| c60c19a28e | |||
| 4ea785b604 | |||
| 2d4e9d0d3f | |||
| 971d572fbf | |||
| 244e1b84f7 | |||
| e5cdc99a34 | |||
| 9a5768219f | |||
| cee8f57318 | |||
| 1a40e0a83a | |||
| d0072d930f | |||
| 385062c4d7 | |||
| 9245638b25 | |||
| 1865542192 | |||
| 2563abda11 | |||
| 59b80d8fbd | |||
| 68bb8182e4 | |||
| c979ff6696 | |||
| 25681e888c | |||
| 5cfd082b00 | |||
| 0cbced43bd | |||
| b3e8d7e07e | |||
| f4a7395e3a | |||
| 14b42abfa4 | |||
| e8022e985e | |||
| f6c8687dc8 | |||
| 472d8faace | |||
| fc5f3c2fcc | |||
| fb756208d8 | |||
| 6a98e93845 | |||
| 79e155acfb | |||
| 59ae6e3dc0 | |||
| e628ed3ef4 | |||
| 11d40e9daa | |||
| a07f0631b7 | |||
| 790d1dd8f7 | |||
| c92e510a4d | |||
| c48a6c0601 | |||
| 383f3f9834 | |||
| 3c1e9ba6e9 | |||
| 26893b9877 | |||
| 2b734b8e69 | |||
| c5f6f87a6c | |||
| 66cdb62a3d | |||
| f53e33723b | |||
| 06bc6e7568 | |||
| 5e3f42ec5a | |||
| 08b3dfa3b5 | |||
| 6cf9563441 | |||
| fb65c7f4ba | |||
| c4452909e7 | |||
| 2d3669b03b | |||
| 848e6e5897 | |||
| c723b76138 | |||
| 57f6b0af09 | |||
| 1c4082af45 | |||
| eece5d318e | |||
| bb6ade2165 | |||
| 586b010811 | |||
| 84ab0fde51 | |||
| ec18df2c2a | |||
| f50503e7c1 | |||
| c619e5c381 | |||
| e7c4a74ed6 | |||
| f8ea019f02 | |||
| 6db8dd620d | |||
| bdc1fa4c03 | |||
| 4e66a2d436 | |||
| 0fa948448e | |||
| 76c675cd09 | |||
| 85a4a594c5 | |||
| f70746c50f | |||
| 712490b671 | |||
| b580e68469 | |||
| bd2cf18fbc | |||
| 092f4217b0 | |||
| abd2ac7168 | |||
| eeea70640e | |||
| 6047838f53 | |||
| cb51799246 | |||
| 44d99277fe | |||
| 5b8e643541 | |||
| ae85c209ab | |||
| 2306caa62f | |||
| 17c11ae23f | |||
| 5b51096e37 | |||
| ac79d6bcee | |||
| b6e056f832 | |||
| d99a22d68d | |||
| 2602c155d0 | |||
| 80f562643f | |||
| 578cb4e268 | |||
| c53c6a94d7 | |||
| 907cf19f05 | |||
| 88682e1c3b | |||
| 20a4edf899 | |||
| 3222b11346 | |||
| fc9d6a6d47 | |||
| c9917e4079 | |||
| b233ab87bb | |||
| 73c3a709de | |||
| 6ce7b30b72 | |||
| 980d55a2f3 | |||
| 988be62804 | |||
| 23efd0850d | |||
| 17e0f1d9ab | |||
| 3c85bd55d3 | |||
| 2298d72ab9 | |||
| 408407b33d | |||
| ab426384e1 | |||
| 5bc68c0c6d | |||
| ebf20d5b2c | |||
| 93d9c40323 | |||
| c6ea976d7f | |||
| 5f24915300 | |||
| fbe174fb64 | |||
| 977d5331c0 | |||
| d40d7e18f5 | |||
| 11be68ad49 | |||
| b0d0782a72 | |||
| dbb6d8ac71 | |||
| a30845f9ce | |||
| 379f290b8b | |||
| 6c413bba48 | |||
| e17a39d446 | |||
| fcadf6ec4a | |||
| 231fde219c | |||
| 2774bd238b | |||
| fed67192bc | |||
| 16db970558 | |||
| c9a79bf32e | |||
| d74ed508f9 | |||
| eafba9c7ef | |||
| 610923af89 | |||
| 23dfeb13df | |||
| f4abd7d027 | |||
| b716e71784 | |||
| 094598196a | |||
| db1d1c49a0 | |||
| ff4125c11e | |||
| a0d51803ed | |||
| 3aabd63975 | |||
| 394e37f9ea | |||
| 369b88d6f8 | |||
| ec8b3ae515 | |||
| c94382b46c | |||
| 2a6a67c6cc | |||
| 37f0a9ad7b | |||
| 28540ad50a | |||
| 29d92d3e81 | |||
| 0477f354c9 | |||
| c7a0c1402c | |||
| 2af5643243 | |||
| 5e9885946f | |||
| e89879d8a6 | |||
| 2f219f83db | |||
| 63e9f794c7 | |||
| 7c0b910d7a | |||
| c77ecad9a5 | |||
| db2897cf1e | |||
| 5c5ce0dfe3 | |||
| 6de213483c | |||
| f5846b89ea | |||
| c5e7bedb37 | |||
| 2b46c560c7 | |||
| c6ad0665b5 | |||
| 8ab84dee16 | |||
| 62b2c07be2 | |||
| 2fb29ae8fd | |||
| b57e858ad1 | |||
| 054aac17aa | |||
| 68b65dd357 | |||
| f2881126cd | |||
| 11968a5888 | |||
| ad279dc566 | |||
| 2814932845 | |||
| a2430dbc53 | |||
| e51d2dd36a | |||
| 604af1ac8c | |||
| 68c6393eb2 | |||
| 4cbf9c7f47 | |||
| 8bb3b75b1d | |||
| a76f0c7cb4 | |||
| 01e31afcbd | |||
| 3b2f2f922e | |||
| 64b83b3245 | |||
| 563e6b3cdd | |||
| 6518bff2ac | |||
| 0e26247b53 | |||
| e69f7dbc5f | |||
| 4d0f6df89a | |||
| 6b184363a1 | |||
| b519069634 | |||
| 1bd44a7427 | |||
| 568ff5a3f5 | |||
| a6bf40d4e2 | |||
| b3bb99d76a | |||
| 243bab7036 | |||
| 88b39f4b67 | |||
| 5e8061f846 | |||
| 870e96a1df | |||
| 59070c2af6 | |||
| cec8936728 | |||
| 14071b0d31 | |||
| 57173e4385 | |||
| 997caad985 | |||
| 2b752c0c02 | |||
| 2cccb8b450 | |||
| 0c540ac8de | |||
| 6033b7b886 | |||
| b67f8d1389 | |||
| ae645ad9f0 | |||
| 5b72509dac | |||
| 3ce42a096b | |||
| 8331c2f267 | |||
| b3c9570b0f | |||
| 9d5c877df9 | |||
| a8e2727473 | |||
| 4b9c6e6bd2 | |||
| d29ac088c0 | |||
| 3a316de9ef | |||
| 40cb37e824 | |||
| f165b55a1d | |||
| 84b91d4575 | |||
| f5832423f4 | |||
| 73dd07aadf | |||
| 0f39a45734 | |||
| f41060c39a | |||
| bbb8e12bac | |||
| d0e1471c91 | |||
| 322ef1fd63 | |||
| 47cb97bc60 | |||
| 8d35bea830 | |||
| d8bcc4e3f1 | |||
| 434ac86090 | |||
| 1061026f96 | |||
| e638c49160 | |||
| 5d84db9fb7 | |||
| 874bdea634 | |||
| 68497d3a1f | |||
| b9e198c172 | |||
| 40d0a82342 | |||
| d49c0a1bcb | |||
| 91fb7b0a7c | |||
| 9b12c22823 | |||
| 3957006fae | |||
| 874029dff0 | |||
| 6aff3ed407 | |||
| c0ae78ae82 | |||
| 8b22f01ecd | |||
| 2ed694b041 | |||
| a0ef6ab811 | |||
| d098b39024 | |||
| 3cf23f8a5c | |||
| fc59bc2992 | |||
| da65f43983 | |||
| 72e77d237a | |||
| ecc3e18e85 | |||
| dea70af889 | |||
| 30362091e5 | |||
| ada4b6ef16 | |||
| 59e6066579 | |||
| 0aa3362671 | |||
| 5873db7331 | |||
| b3fe05ec81 | |||
| 92fbf58b13 | |||
| a4b2cc84c7 | |||
| 89c3f6fa0e | |||
| 1395da694e | |||
| 264b20535e | |||
| caba350b33 | |||
| d9fe194111 | |||
| 258adda67c | |||
| 40dc13b2e2 | |||
| 4cda54ca1c | |||
| 8116c5b3f7 | |||
| 35d584c67b | |||
| 9504cbcc4f | |||
| 1dcc5127d0 | |||
| 6790699279 | |||
| 85e3d7083c | |||
| 262ace1773 | |||
| 7cd101d8cb | |||
| ce2058aea9 | |||
| e9b0acaa8e | |||
| bd2da08c4e | |||
| 0a88d419c6 | |||
| 80c190db36 | |||
| 550cf00ee7 | |||
| fbca7951fc | |||
| 1e1358fcef | |||
| fd1b3329f5 | |||
| 9c9d8468a5 | |||
| 55ca03f100 | |||
| 83708725b2 | |||
| 6f59d62e5c | |||
| 1c348f0cdb | |||
| 634596257d | |||
| 5e4973a1dc | |||
| 19f023e0ee | |||
| 090c15fe19 | |||
| e8b307dc4f | |||
| 056479d450 | |||
| e5ebe2f888 | |||
| e8e1b431ad | |||
| 847d40e567 | |||
| cf6c555e6a | |||
| b508aa9ebc | |||
| 5e7634506e | |||
| ba39b64ced | |||
| 33ad65a105 | |||
| fcebe89f6c | |||
| 2d5eb920b8 | |||
| 26de2c86ed | |||
| cba1e95d0a | |||
| 78a5a88638 | |||
| b7b9c67259 | |||
| 54bff81470 | |||
| fe21972f4a | |||
| 58e3c72446 | |||
| 6dd5c6c317 | |||
| 4e0af3eafe | |||
| 1d0791142c | |||
| 2560ba2980 | |||
| 19be3dd852 | |||
| 40f2a6978b | |||
| 1fd8c43d94 | |||
| 63cc3fd890 | |||
| c556ca40b1 | |||
| 3e32f47903 | |||
| 8f2824186a | |||
| b0dbb20e22 | |||
| 0519c4c6b1 | |||
| 28184b4a29 | |||
| 76175abea2 | |||
| d28f829b1c | |||
| 184c3dce15 | |||
| a08a3078da | |||
| c2100d7622 | |||
| a91fa59174 | |||
| 574a6b68ae | |||
| 2f4c1dfcc4 | |||
| 1b62a21dbd | |||
| a78825eff9 | |||
| 0bad7b213e | |||
| e4bb37b1a8 | |||
| 54c443ac68 | |||
| 3af9af96ea | |||
| f75d188131 | |||
| fc3a00054f | |||
| 84e41c2ade | |||
| 4630733b55 | |||
| eb690e14e4 | |||
| 009430e829 | |||
| 073fb73ff3 | |||
| e789747834 | |||
| 833002f846 | |||
| 3838fab788 | |||
| 907e9fc476 | |||
| b829a39cd2 | |||
| daa7af0605 | |||
| 47a1adc864 | |||
| 98e448acdd | |||
| 72bd51f26e | |||
| 29db856322 | |||
| cd5cda916f | |||
| 33a1139772 | |||
| c5b62903f3 | |||
| 387fd16b40 | |||
| c91b67d370 | |||
| b809d1c263 | |||
| 7bb6675abf | |||
| b91bea94f4 | |||
| 96e21700bd | |||
| 7e8f25bce3 | |||
| 9e02049b05 | |||
| affdfccd60 | |||
| 402f8c27a9 | |||
| ba4dc6c60a | |||
| 6b8dd42547 | |||
| 1511a27f4c | |||
| 7ae6c147fa | |||
| f51630eb07 | |||
| e3586411e0 | |||
| a0639a32c7 | |||
| 759c6e77a7 | |||
| 04ad3d7c3c | |||
| 49badd9a2f | |||
| 8b00083bca | |||
| 3d98e324b5 | |||
| 768c66313f | |||
| 3561fd1c05 | |||
| a6c055b6d1 | |||
| 0d24c18fed | |||
| dd8b2a79fb | |||
| f0095611bc | |||
| a3567f0918 | |||
| f0d3d0d74e | |||
| 3e32bc0d5d | |||
| 632e4aa120 | |||
| 2391ce198d | |||
| e5e2bbd482 | |||
| 0b6632123b | |||
| b1801fc953 | |||
| ca1a1c4f28 | |||
| 3363cc4f1d | |||
| f84684982f | |||
| 3bed5969bf | |||
| ebc162e3d8 | |||
| f30136dba3 | |||
| 9b1926f902 | |||
| d29524ba3f | |||
| 7258fe4e5c | |||
| f8ea1702f8 | |||
| 7582c28c1a | |||
| 5042eb87e7 | |||
| e8e80732ef | |||
| b8744a79ae | |||
| 414b153d28 | |||
| 8e160dda8e | |||
| 9b54c9b807 | |||
| 1bb608cdb6 | |||
| 1239485b30 | |||
| 0d23d047fc | |||
| d837ae64ac | |||
| d72a70396a | |||
| 938772b86a | |||
| 3e88593a81 | |||
| 60c01d7869 | |||
| 1cbcc61bd6 | |||
| 7f5a2974ce | |||
| 3de3ea38b9 | |||
| 3659e86d57 | |||
| c335a6b3de | |||
| 267b831bc4 | |||
| 7ee93cb910 | |||
| ae95a49618 | |||
| 8f98504183 | |||
| 1b77ee0ef4 | |||
| a6de395cde | |||
| fcd6dd34b2 | |||
| a6ebfe4215 | |||
| 6a9158aa62 | |||
| b0b0291bc7 | |||
| 85f1da1f10 | |||
| c47445ca98 | |||
| 4e25867548 | |||
| ad71bb30ac | |||
| 362bf1895d | |||
| 7d00c0bd5a | |||
| 5e5994f166 | |||
| 7247762b60 | |||
| 21e0c79f7d | |||
| 78b08bfef2 | |||
| ae7e90dc2f | |||
| bf873bde42 | |||
| 017f81e430 | |||
| 0028bfbfc7 | |||
| 60c9c403bd | |||
| ec5fff2046 | |||
| a7199a3d0d | |||
| 5cf2ebea4f | |||
| 580e95605e | |||
| 8c3d1df3cf | |||
| 7c66f91429 | |||
| 73e294b1bd | |||
| e5ec479923 | |||
| 75107f99b2 | |||
| fb8efe368a | |||
| 1faf477537 | |||
| e3d108454c | |||
| 806b40727d | |||
| fa702efe8f | |||
| 344e3e18ab | |||
| aea9eaa307 | |||
| dffe0b39b6 | |||
| 267d660527 | |||
| 4c3046f917 | |||
| 71444b638b | |||
| 962ec7bb53 | |||
| 52fad6aec2 | |||
| 5b830f0b6a | |||
| 3d24c8768f | |||
| 977b8625f8 | |||
| df7dc04a1d | |||
| 269d3cb086 | |||
| 4d310cd461 | |||
| 88c5c39fcb | |||
| 79ca68300c | |||
| baca20b225 | |||
| 0e3cb1977f | |||
| 8b1fa72877 | |||
| e8610a35b4 | |||
| 77e6442f73 | |||
| eeddfd4919 | |||
| fa16da86b3 | |||
| 372a628cab | |||
| 2f4d8c3530 | |||
| 482eab0e2a | |||
| 03c63d9b12 | |||
| 07e87915ba | |||
| 91f2bf99c0 | |||
| 535d59db4d | |||
| 9739c3355a | |||
| 827db37eef | |||
| 0c6e47a5bc | |||
| 864ea749e5 | |||
| 5d92ec3b7b | |||
| 733a3ed102 | |||
| b14be026b7 | |||
| b4afe97289 | |||
| 43a7a607b2 | |||
| a8bf66d8af | |||
| 3616a07dbb | |||
| 6609dfd410 | |||
| e3913bd397 | |||
| 8c01ed1469 | |||
| 7aa0dcc89f | |||
| 5285b22a76 | |||
| 0fa49bc2cd | |||
| 01d8730850 | |||
| 52149ce74a | |||
| bffc20612d | |||
| 2c0eb19a27 | |||
| 748c4737f6 | |||
| 769d5113f7 | |||
| 157be6da05 | |||
| 1dc4b8bb63 | |||
| f261599435 | |||
| 2862b49057 | |||
| a8d0d8f33d | |||
| f55a2079bf | |||
| c0f706a2a2 | |||
| b034f67a0f | |||
| 977b9eb686 | |||
| 5e11bf735e | |||
| a8c73f7a4d | |||
| 86105611fc | |||
| 0364af7337 | |||
| c618ce4625 | |||
| 2b9c834476 | |||
| d366ec9c48 | |||
| ca3981fba8 | |||
| bb490faefe | |||
| d8f673ed51 | |||
| 6ce7170cf4 | |||
| 1d71e7243f | |||
| cf08901d02 | |||
| 230a9311a0 | |||
| 576f7142c1 | |||
| 20b4285849 | |||
| d67bdbf088 | |||
| 3a389793ff | |||
| f5ff5dc3e0 | |||
| 00bf5bdf69 | |||
| 9541aa7dbf | |||
| e61c6b89c8 | |||
| a5b3869e9f | |||
| fbdce27db2 | |||
| 148876f597 | |||
| 0cb533beca | |||
| 5811ebd6f3 | |||
| 8fa87f8ba5 | |||
| 21ba4f71f6 | |||
| 097e7df7c9 | |||
| 83c6615d6e | |||
| f6fafeaafb | |||
| 420a88c776 | |||
| 5fcf9481b3 | |||
| 48c3dcc08a | |||
| 62333b3e2c | |||
| df758b31b7 | |||
| 9f08bfaa6f | |||
| 198d2c780d | |||
| ab1c0dabae | |||
| 0234f11914 | |||
| 79fcc9f343 | |||
| 031f722540 | |||
| 531ccf1819 | |||
| 0d2ac42dc4 | |||
| 9ec6ea3bdf | |||
| 1ce580bba3 | |||
| 8ad2a94a90 | |||
| de3f75bc83 | |||
| 008d85ed32 | |||
| 5e30aff418 | |||
| 5de0d39553 | |||
| d95d44dc94 | |||
| 6061deac37 | |||
| ba34a766e7 | |||
| 1c81a17298 | |||
| 2097b31d4f | |||
| 2155dd0552 | |||
| 8733654094 | |||
| abd15748ce | |||
| 9a796f1383 | |||
| 88f2f62945 | |||
| 30adefed07 | |||
| 20a1828fa5 | |||
| 809674ca2b | |||
| 0ca3475878 | |||
| 32b741e205 | |||
| c917c4a468 | |||
| 6c584d2b4c | |||
| 759d415d40 | |||
| 45d86fa270 | |||
| 2c5cad71ee | |||
| 2b5925b893 | |||
| f012ada2c4 | |||
| af1b26ae95 | |||
| f72f5b43e1 | |||
| d55618921b | |||
| c7e1e07262 | |||
| 24a1bec23d | |||
| 89ad104423 | |||
| c2f3324302 | |||
| 04a969b997 | |||
| 630dfa9499 | |||
| 95668950c2 | |||
| 3012501e4b | |||
| 0e81dfb004 | |||
| 35b7f358b6 | |||
| e3e48944e0 | |||
| 94bbba72f5 | |||
| b34716f7e9 | |||
| c429ca67b9 | |||
| bce2ba0785 | |||
| 2613690064 | |||
| 7283076bc8 | |||
| f43d05b54e | |||
| c6b500bc09 | |||
| c3972015c7 | |||
| 3c18c57857 | |||
| f562a06707 | |||
| 7f50dd205f | |||
| c4fe15400c | |||
| 6e3e8f7310 | |||
| 5ae2c26130 | |||
| d8d35f4022 | |||
| fadb4d9219 | |||
| b63149b36a | |||
| 79f92abcfa | |||
| 0137fb468b | |||
| 70ef8760cc | |||
| c74d2d831b | |||
| 0415f821eb | |||
| 779fe35255 | |||
| aec7ef6f9c | |||
| 329f09ce0a | |||
| 68c23af5ae | |||
| a54f30c02f | |||
| fde00b1c62 | |||
| 11382d2cd7 | |||
| ef31131a5d | |||
| 8dd425f8ff | |||
| 76feabe32b | |||
| 7fe3e2f90a | |||
| c0b2151929 | |||
| 5e3b1bf6b0 | |||
| fdf4523c2a | |||
| c6cf76f345 | |||
| f16e544691 | |||
| df101217fc | |||
| 5d90dc16cc | |||
| bbfc1a1cd6 | |||
| b7208c12ac | |||
| eaa2fdec44 | |||
| 7099adfe79 | |||
| cb17a2bcb0 | |||
| 172044a1cb | |||
| 69204d4fb3 | |||
| 4b203b6b63 | |||
| 67876bab4c | |||
| 6ce691e40f | |||
| 9cd44b09f9 | |||
| 73a2704126 | |||
| a50dd785b8 | |||
| bafbe5cbec | |||
| 9cdcbf6bf8 | |||
| 9596087959 | |||
| 6570402b95 | |||
| 4153845346 | |||
| 548713ed98 | |||
| 1bf1ce7070 | |||
| 21dc0fbf2f | |||
| 7b29de9698 | |||
| 7d468ee148 | |||
| 260e7b529f | |||
| b027089fc1 | |||
| 8f318528f8 | |||
| 9696e70817 | |||
| 85580acbff | |||
| c45a5f7e84 | |||
| df74b0b038 | |||
| 749e5b8458 | |||
| 1255566324 | |||
| a25da4ebf4 | |||
| 72835f3c5f | |||
| c4f52f6256 | |||
| 3eb9505633 | |||
| 543a0be1e4 | |||
| 5ef21c5075 | |||
| 5c412cc0bd | |||
| 346deb4b0d | |||
| 6f8f30b018 | |||
| b808757012 | |||
| 5e491bb89a | |||
| fc3cb0dd45 | |||
| 0846cb04af | |||
| 9708394846 | |||
| 92e4396602 | |||
| a5870b73a9 | |||
| 77ff6b9088 | |||
| 425e74f297 | |||
| ffa184464e | |||
| eca68e9e7f | |||
| dd1bc8ec9a | |||
| 24dc1d6991 | |||
| 9f510b7eee | |||
| 730ca9b60b | |||
| 1afaf903f9 | |||
| d53cd41aa6 | |||
| 3e5ea745d2 | |||
| c7052f7dc7 | |||
| 56b01df85b | |||
| aa18eeb7d6 | |||
| c18264c615 | |||
| 11746290a9 | |||
| 16c062c069 | |||
| 64396de0dc | |||
| 8ff78c5d60 | |||
| 349297e495 | |||
| dc3ffb3b30 | |||
| 5c2dfb138a | |||
| 0be679de42 | |||
| 8bd68e0f10 | |||
| df70e16b4d | |||
| b246545da5 | |||
| 3280cb648f | |||
| 8798bf42e6 | |||
| 5a23927e56 | |||
| beafd597dd | |||
| fbc43b0d58 | |||
| facfcf679d | |||
| 68b230a78f | |||
| 3d1fcc6f83 | |||
| a0578efeb9 | |||
| 727ad5755e | |||
| 4f17352858 | |||
| 0eb72122ce | |||
| 66e2b3bb70 | |||
| 5f12d858eb | |||
| e258d6ca8d | |||
| 2d25dedbcc | |||
| bdf6fcb222 | |||
| 7e1cea1ef6 | |||
| d98c803b54 | |||
| 95238466b5 | |||
| 71652043a0 | |||
| c9cbaf254b | |||
| f9cc5cbd33 | |||
| bcb9405793 | |||
| 30cb6f196f | |||
| 856ef01632 | |||
| 6f95554655 | |||
| a72f915646 | |||
| 94605417f6 | |||
| 1e017df128 | |||
| ec27bb5131 | |||
| 9637fc098a | |||
| 874020ced7 | |||
| a7beedcfb6 | |||
| 4351c4dd6f | |||
| 1ddf7fb96c | |||
| ec5cfe4ee9 | |||
| 4fed2ea7bf | |||
| ae14cf4740 | |||
| 8aa68b3dc1 | |||
| e810ee7750 | |||
| 9a08194597 | |||
| c77277b60c | |||
| 21a324558f | |||
| 7a31751564 | |||
| b11bacc2e2 | |||
| 8c02e7ba67 | |||
| 275eb8d434 | |||
| 736d0df38d | |||
| 89d5d41015 | |||
| 0e1444c84b | |||
| 104f8b093d | |||
| 1e638c376b | |||
| a2e1a6ca8f | |||
| 337331ff1b | |||
| 1dfde7cd80 | |||
| 7df2bfe7bc | |||
| 4ec90a4b99 | |||
| d4e8f9039c | |||
| 4c6c00f16d | |||
| fd30b25596 | |||
| 032b7cab5d | |||
| 47dfb4b8cd | |||
| 51e782b671 | |||
| cf195262bf | |||
| cf72052e46 | |||
| 6f50c39b2a | |||
| b6cd826dd7 | |||
| f1194b1fbe | |||
| c0ca85fb3a | |||
| 022df1b143 | |||
| fdf987f081 | |||
| f1e874cd18 | |||
| c3bede58aa | |||
| 38915eb7fc | |||
| 470bd23b3b | |||
| d007eefe2e | |||
| 362f442a98 | |||
| 8c2645c5dd | |||
| 1ba0e4809e | |||
| 341371b613 | |||
| d856285271 | |||
| e4ffc93463 | |||
| 3149958319 | |||
| ac659e8df1 | |||
| 81d54c7558 | |||
| e4de333d83 | |||
| e72096328a | |||
| 88a082a533 | |||
| 4fbf4f1069 | |||
| c360dd11ed | |||
| 95d582ccee | |||
| e0c9b990e7 | |||
| 074cfb7c58 | |||
| 8b649cec8d | |||
| ea6974fc89 | |||
| fb29da4e40 | |||
| 461acbcc81 | |||
| 5bab8647b6 | |||
| f2d1222de7 | |||
| 585ea14a23 | |||
| 5a0997ded5 | |||
| 0174c5674f | |||
| 9de8653936 | |||
| 27d28b8247 | |||
| 110f43a246 | |||
| 56612751f9 | |||
| 751fe7349a | |||
| fb1b554b86 | |||
| 36d7d33afc | |||
| a94f3c720e | |||
| 6c1087e429 | |||
| 2cdb010cff | |||
| 60052f59a0 | |||
| 29d44f809e | |||
| 83e4aa2755 | |||
| eeb97f5b66 | |||
| b6f26ae6a5 | |||
| f40435654a | |||
| 67d471ea3d | |||
| 4946c5e687 | |||
| af4f05c29e | |||
| 145c76095a | |||
| 90045b6faa | |||
| aef27d811a | |||
| e2e5f80298 | |||
| ca0ed50172 | |||
| d44d63c1d6 | |||
| 8403042297 | |||
| 1568bb014d | |||
| 7a069c4018 | |||
| c856eb931f | |||
| b290ce795f | |||
| 6f3d279165 | |||
| 124ab30f98 | |||
| fee90bab66 | |||
| e26ade0e62 | |||
| c43ccb860b | |||
| 81de2b3afc | |||
| 9a53fa3876 | |||
| f2b7e8b038 | |||
| 1863f1311b | |||
| e5086f22d6 | |||
| da90a3ca78 | |||
| d397c5a251 | |||
| d26b4434b8 | |||
| 7188f17f9a | |||
| b0365f8b0e | |||
| a1ddeea00e | |||
| 1414c24caf | |||
| 0748c864cd | |||
| f80626a0fa | |||
| 0fffd64a7f | |||
| 8da48211d9 | |||
| 0dd8ffa3a0 | |||
| 0362e61f36 | |||
| d92a77f695 | |||
| e6dd573e8a | |||
| b6330c3a4f | |||
| 10bc714f5c | |||
| 8f57723b88 | |||
| 3d71bee85e | |||
| 30e00d5fa7 | |||
| bc99a9d792 | |||
| 61df41d21f | |||
| 0b4ef8dcbb | |||
| 9d7d48b9b5 | |||
| fb37150dfd | |||
| bd08ed898d | |||
| d3bc525713 | |||
| ae27c553ac | |||
| 27030ae1e9 | |||
| fd083e1e66 | |||
| 5e4149ae76 | |||
| 859d462629 | |||
| 8bfa81df42 | |||
| 6782d53e28 | |||
| 4796721d5c | |||
| 55bbc71a17 | |||
| 5372575b24 | |||
| d995019c6e | |||
| aa70da5659 | |||
| 020b293068 | |||
| 678ff23bcb | |||
| f93d50dcd0 | |||
| aa3201ebb0 | |||
| 4ad153c425 | |||
| 082683bf0e | |||
| 7f590af0b5 | |||
| ecc1c86600 | |||
| fece506cdd | |||
| f11a58e2cb | |||
| 0238ecebed | |||
| 0d6ffa3935 | |||
| 143632e635 | |||
| 9ec33a97bb | |||
| 7e2c236582 | |||
| 6ebfd175bc | |||
| defaa918a6 | |||
| c4e70be0a5 | |||
| 57d425fae6 | |||
| 6024163af8 | |||
| 44b35cdb3d | |||
| 36ff0ad019 | |||
| 9218e518f1 | |||
| c80bde1f60 | |||
| 4b7157b987 | |||
| a90f592224 | |||
| 59f228dab7 | |||
| bae3f5ceb7 | |||
| a5c5da5b8a | |||
| 7ecf313132 | |||
| 313cfacfa1 | |||
| fb991503a9 | |||
| c31ce641a1 | |||
| 5b1a5b7dd0 | |||
| f25324fb1c | |||
| 2a7f35a633 | |||
| eb2d5484b8 | |||
| 40cbd5ec9d | |||
| 10680ace17 | |||
| 26e28ed687 | |||
| 8bf92d84db | |||
| 9e2bb5b37b | |||
| a48a88c312 | |||
| 76b2fc2a6c | |||
| 4438d716b9 | |||
| 8fcf55d761 | |||
| 3091a76702 | |||
| 117e2370d7 | |||
| a35d70e995 | |||
| ec68000105 | |||
| c707d3db00 | |||
| d3572836bd | |||
| a5dac751b0 | |||
| 7a59579dcd | |||
| a60e4efe6d | |||
| 4c0ebeab58 | |||
| 0143ac2a86 | |||
| f24b02cae4 | |||
| 497a2bd057 | |||
| 995f796a5d | |||
| bf462e2840 | |||
| fb75134179 | |||
| 68d67b7fc2 | |||
| bbf412f3ad | |||
| 52b575296c | |||
| c1652f4898 | |||
| d40cc795f7 | |||
| 4035d933ad | |||
| d255348762 | |||
| 978db89deb | |||
| b867afe772 | |||
| e34fd89bb6 | |||
| 51883b8f11 | |||
| ff5f95227a | |||
| dc929236a4 | |||
| b09e20747a | |||
| e8fc857dbc | |||
| b42a13cc3b | |||
| 37ed9800c5 | |||
| 99f4968888 | |||
| 34428a811c | |||
| da7104b00d | |||
| b373601e13 | |||
| fcc90e884b | |||
| 7a90096077 | |||
| e7886a55fe | |||
| e33b786f65 | |||
| b0b50f4ef9 | |||
| 37c8e6dc07 | |||
| 675441fe93 | |||
| 2495d97862 | |||
| 70c998dd55 | |||
| ec60e46611 | |||
| bb2dc618f7 | |||
| d9addf84ef | |||
| d89163110e | |||
| 7c851faba6 | |||
| b70c219a05 | |||
| 93b722af3c | |||
| ee0e036aa5 | |||
| 24ef004adc | |||
| dcab4eb70b | |||
| 8f252992e4 | |||
| a3a3e32e21 | |||
| f5f8867326 | |||
| 5494a9dd3b | |||
| 89f7857c84 | |||
| 3140c6526a | |||
| 96d828d22b | |||
| a8a50384b8 | |||
| 787af6448d | |||
| 4633135322 | |||
| 050d522735 | |||
| 809b202e70 | |||
| f1555dbb5d | |||
| d1d0266a10 | |||
| ee37ed0697 | |||
| e3972dee2c | |||
| 7a727e7eda | |||
| d24c822f68 | |||
| 8d804013f3 | |||
| d73a115436 | |||
| 0b4ff731e8 | |||
| a58ec3f192 | |||
| 868c20b161 | |||
| c2cd050419 | |||
| 7d5c107fb8 | |||
| d855a6ea0f | |||
| 464f84d8cd | |||
| fe0ee6402a | |||
| 068939f790 | |||
| 35f48d1c8e | |||
| 52adde2501 | |||
| 0ddc4eceaf | |||
| b0ab8c750d | |||
| b17dd8351f | |||
| 0ceb8d159a | |||
| 402b943ddb | |||
| be55451c90 | |||
| c51c1a2ae6 | |||
| 845c796b96 | |||
| b0918ef293 | |||
| 102572b088 | |||
| 63076e77f5 | |||
| 8e48ee5f66 | |||
| 1a55f550c0 | |||
| ae8fc64394 | |||
| 5e8e56caf9 | |||
| c075c161c2 | |||
| 237a553d15 | |||
| ca8674e0de | |||
| 0511a1172f | |||
| e07b304914 | |||
| 17364e72ec | |||
| 22b213ae26 | |||
| 7d5936a9e9 | |||
| ab8f466f53 | |||
| 201177e7f0 | |||
| ec5f9a2892 | |||
| ee5b8748b5 | |||
| 8d04f8b8b5 | |||
| 033babfbfc | |||
| 15b77861ea | |||
| c4721850ce | |||
| b325aad5c9 | |||
| 92e616f18e | |||
| f7fee29c76 | |||
| eccea7411f | |||
| 01f93e0970 | |||
| 2d82a7bc2e | |||
| ca91fba071 | |||
| 9f2fce4d87 | |||
| e1942267c5 | |||
| 12212409c7 | |||
| e5565c6bdb | |||
| f00558d840 | |||
| da0dc5ed11 | |||
| b417492fad | |||
| d3ee532624 | |||
| e8be38ce5a | |||
| 38c9a05a0c | |||
| 110bd332f4 | |||
| 8a0f73bf81 | |||
| 337c9cbea3 | |||
| cfd61096d9 | |||
| 2894e253a2 | |||
| e52985e082 | |||
| 7d2bc12bb7 | |||
| a5f397b26d | |||
| 5b93d5210e | |||
| e943a6e09c | |||
| 8f527a6212 | |||
| f2f8ad6b65 | |||
| c870930bc0 | |||
| b26b1caa86 | |||
| 6613ee6b0d | |||
| 9550bca099 | |||
| 92a75aaa08 | |||
| 906bf88450 | |||
| d7157843f4 | |||
| d317c1ff08 | |||
| ef889963d9 | |||
| a2d7b221ee | |||
| aff32afefa | |||
| 0943e0c60f | |||
| 18f75ec61c | |||
| d821082843 | |||
| 366a88cc5c | |||
| 951df61aa0 | |||
| 3e79575602 | |||
| 92e24777c0 | |||
| ab8d06bb86 | |||
| 8563dd5860 | |||
| 1f6153fa82 | |||
| 23d66b9746 | |||
| 25ccd6bc6d | |||
| 14ad32bcd2 | |||
| 9ab9b9d75a | |||
| b7a3c4557f | |||
| bdb90b4b33 | |||
| 85d0935e97 | |||
| 86ad75d27b | |||
| b40473aa3b | |||
| 3bd5ffc5cd | |||
| 10aafd3738 | |||
| c055765bfe | |||
| d8f486fc0d | |||
| 06eea71a37 | |||
| 3effb9ec29 | |||
| 6603a2300b | |||
| ed029fe348 | |||
| b497bc5eb9 | |||
| 8a4a1dfadf | |||
| 8bbf14acbf | |||
| 86f2c86440 | |||
| cfb29f1339 | |||
| 63a28d8e34 | |||
| d37cbb10a5 | |||
| 055590c0c6 | |||
| 6aaac45468 | |||
| 34adaae5af | |||
| 9c6f004f7f | |||
| ff685e33d5 | |||
| 2999603b28 | |||
| 32d8f4b084 | |||
| 0fb0c1b71b | |||
| 2ac34dbab0 | |||
| 986fb12543 | |||
| e686eb750f | |||
| 8ac15068ee | |||
| 5e4cd6cf11 | |||
| 39d694de8c | |||
| 342f5c01e0 | |||
| f91293c6c5 | |||
| 1ce4977a70 | |||
| f4b25b59e5 | |||
| b33a47e253 | |||
| ccd4d4263d | |||
| 2ff9a36eed | |||
| d1e91cd702 | |||
| e2599071c5 | |||
| fc38b89aee | |||
| 5688286a79 | |||
| 7eb10ab7ac | |||
| 34b31865c5 | |||
| f1c5b632cc | |||
| 04ca0ac2b5 | |||
| 8b2fdf3a75 | |||
| adca75b7d8 | |||
| 504fa2a1d3 | |||
| 266a062a5d | |||
| 652a9452c2 | |||
| 503b6ea6c8 | |||
| 547501ba81 | |||
| cfffbc4a09 | |||
| b58d84fba1 | |||
| a5d3dd942e | |||
| b96062b6de | |||
| 04b71c11e1 | |||
| 651baefb1d | |||
| ff7e845615 | |||
| f0612a1407 | |||
| 83bd24adf8 | |||
| b5a8e6bbdf | |||
| 9798fcf839 | |||
| 15556b6797 | |||
| 0ca4d728d8 | |||
| b2c7804032 | |||
| e091dc0294 | |||
| 3bfb4595cf | |||
| 8955d8de23 | |||
| 1372b298bb | |||
| 9558845e6e | |||
| 5ab0930de8 | |||
| 3294f4858a | |||
| eea9a3ba59 | |||
| 753974d663 | |||
| 527cd0a6e5 | |||
| 24f70387d2 | |||
| adc2070ac1 | |||
| a8642682d0 | |||
| d66e6db480 | |||
| dc66bbc3dc | |||
| 6e7f5feea5 | |||
| f21ea6c065 | |||
| 6af56b56bc | |||
| 598d40b0b7 | |||
| 6ae714f51f | |||
| b0661bb586 | |||
| b6a165f1f8 | |||
| 8fe4a36b68 | |||
| 0d24f2d4c1 | |||
| dd0ff3eeb5 | |||
| 07868f701a | |||
| f4f0e4b60f | |||
| ae950a2ff4 | |||
| 5f6e4bdfe9 | |||
| c6d2d4ccda | |||
| 59160a5d42 | |||
| 5da6423fd6 | |||
| d36b8721ca | |||
| 539abffe0e | |||
| 9b24e66441 | |||
| cc16cb9281 | |||
| 45fe4846f2 | |||
| 3ca2779d9c | |||
| 967341b127 | |||
| 4e7f9fb805 | |||
| f3eb661aad | |||
| 1abf8e23a4 | |||
| 9f1f476f43 | |||
| 1a9d61c92a | |||
| 6ad465e3c0 | |||
| 8ef947722f | |||
| 6e6b5c95a3 | |||
| fa593a7a37 | |||
| 7fcccad0ae | |||
| e8ce94ade2 | |||
| 6055f038ee | |||
| 6a1f40eeab | |||
| ca01589e50 | |||
| cca891644d | |||
| cd19578d80 | |||
| c96f7e5a13 | |||
| d7f92b4f72 | |||
| 70a5208fcc | |||
| 8c9150db66 | |||
| 1f86dbd12f | |||
| cfa871c076 | |||
| f355661522 | |||
| be3fb0f917 | |||
| e2f4c0ffd1 | |||
| 210a53a3a5 | |||
| 5049919855 | |||
| ce187786cb | |||
| e6b35a9237 | |||
| 82e5e9cf4a | |||
| d7e1910076 | |||
| 009c28ae50 | |||
| db66023102 | |||
| 4d8dc1a0c4 | |||
| e0a5edeb04 | |||
| ffd9a01e2f | |||
| 25a8c79951 | |||
| c8674ff104 | |||
| a40b10f53c | |||
| 79fa944402 | |||
| ed3cdeec74 | |||
| 05d50d457c | |||
| 2531db84a6 | |||
| 96c1126fe5 | |||
| bb5038b8b2 | |||
| 0c65162349 | |||
| 17cc12844d | |||
| 6cfcf92a28 | |||
| 6ed9a85dca | |||
| 0371265fea | |||
| de257b34c0 | |||
| 4b6575d94a | |||
| 2c54d76085 | |||
| 70f39ed760 | |||
| 1c6652483b | |||
| ab7e0a9266 | |||
| ff323d00af | |||
| ea2a04135f | |||
| 6d88c76464 | |||
| 9b188ca87d | |||
| 1664312c80 | |||
| 38baa42ebb | |||
| 654322e896 | |||
| 3f70f532b7 | |||
| 6ba214a259 | |||
| caf73f387f | |||
| 9a81ca9fab | |||
| 0edf19a871 | |||
| 6989f6c835 | |||
| 2daa39520a | |||
| c8eca50f43 | |||
| de844f1a32 | |||
| 97951e1c1a | |||
| 2edbed8528 | |||
| 24937910c7 | |||
| 5cd441fb48 | |||
| 06b956bd75 | |||
| 41864d46c3 | |||
| f6622e0bcd | |||
| 0f30d21fa2 | |||
| 4257c8c9f5 | |||
| 331859d383 | |||
| ef03b708a8 | |||
| 716d098361 | |||
| d887057660 | |||
| 7efbfebb4d | |||
| 4c7afe5af0 | |||
| 676515cf27 | |||
| 0eb5b0fdfa | |||
| 2feba4787f | |||
| 516dc1043e | |||
| b26c1c57dc | |||
| 0945ba9e90 | |||
| 69ed6f283d | |||
| 9eef850d0c | |||
| cf1574d690 | |||
| d6913e41a0 | |||
| 3c81c295c7 | |||
| 56dfa0c755 | |||
| 43989be768 | |||
| 822380ac38 | |||
| 8c37d9ac9a | |||
| e40b8461f7 | |||
| a3f45b466a | |||
| 672ad68c64 | |||
| 4ccec13739 | |||
| 09529a1aa8 | |||
| d182fd6bb7 | |||
| 36bf123e2b | |||
| 92cfbf655f | |||
| fbef701179 | |||
| 0415b9cf4c | |||
| cb9a9e8d50 | |||
| 6021c1c6b1 | |||
| 655be2fa2e | |||
| 98491a63a7 | |||
| acd7f15c83 | |||
| 5020d4e99f | |||
| 9693c30209 | |||
| 2b6f8adc64 | |||
| 822f5927e5 | |||
| 0f6e9d7b9d | |||
| 99f3e3f09e | |||
| aa81c96a98 | |||
| 9d532b6c72 | |||
| 4c63906b8f | |||
| dd2a870227 | |||
| 88948c3cfd | |||
| b33dcfe6ff | |||
| 2c15bdae04 | |||
| 2f45633312 | |||
| fdd42fbc6d | |||
| 54a6f5d425 | |||
| 68d9662fe5 | |||
| 4f0987da01 | |||
| 625697e097 | |||
| 92b14f20d2 | |||
| dd069647d1 | |||
| 4523ae7d29 | |||
| 19e5eda773 | |||
| 070d58ac0e | |||
| 2d70f69857 | |||
| 7a3acfa6a7 | |||
| 8ac0d12d1e | |||
| 86164103f0 | |||
| b9c71ef03f | |||
| 7ffff761d5 | |||
| 7d4366473d | |||
| e63c660162 | |||
| 1762f9d68e | |||
| 76287eed2c | |||
| 5a764bbaa2 | |||
| ce9e69c9e0 | |||
| 3a74e1f154 | |||
| 6df4a36da9 | |||
| 4e38b51958 | |||
| b6c036af25 | |||
| dd789a8dcc | |||
| ca83b858c0 | |||
| 0f29952a1c | |||
| e2d7b465ae | |||
| 8985dc2f7e | |||
| cf1731792c | |||
| 4c200cdd49 | |||
| 5c8eacddde | |||
| 2668177210 | |||
| 62be08f063 | |||
| ab2a67a012 | |||
| 3ceeee7298 | |||
| 2d7576f29b | |||
| 6e25a17afb | |||
| 0715682a8b | |||
| cfff30c314 | |||
| b53318ecb7 | |||
| 039a3e258b | |||
| 18806e5524 | |||
| 0594a8d03a | |||
| 5a575d61b6 | |||
| 42c3cf2545 | |||
| bf6490739d | |||
| 9815c0a866 | |||
| f5f05a9a91 | |||
| b392656d60 | |||
| 6737d091fd | |||
| 9f924c3510 | |||
| 2ea66d2e81 | |||
| d18c238938 | |||
| 8c92e221a3 | |||
| bafe9c06d4 | |||
| cad6ec854e | |||
| 47a0398b62 | |||
| 11f5ae3c20 | |||
| 61cf853eb5 | |||
| f72884ac19 | |||
| b72b38b0a3 | |||
| 6a2465329a | |||
| 4cb80588e9 | |||
| 753f11e0e9 | |||
| c0bd2c8945 | |||
| aebbe4f254 | |||
| 68948dbaeb | |||
| a38917f920 | |||
| f52e198b17 | |||
| dec734346b | |||
| a73f10edd4 | |||
| 59a7232016 | |||
| d9e6aed9da | |||
| e4f52dd1c7 | |||
| 2c1e3416e3 | |||
| 62090ef119 | |||
| 52ef8a635f | |||
| bf26ccd0a5 | |||
| 5a55b98650 | |||
| 547333c946 | |||
| 1ed105cb79 | |||
| 2ce2928170 | |||
| 14727d75ac | |||
| ccbc0b79b8 | |||
| 5bee0004b2 | |||
| 86fd42dcb5 | |||
| 1e05e0d6f8 | |||
| 821e0ed6ce | |||
| 66ce31f6d6 | |||
| cf486aedbd | |||
| 1b0f22c4ae | |||
| 55acf21aa6 | |||
| dc8a2670ab | |||
| b666ec1f4d | |||
| 999fc07683 | |||
| 37a186696a | |||
| 107ef27f69 | |||
| 0c1c10a0e0 | |||
| 89de1f9a01 | |||
| 602e91da40 | |||
| dec4e67135 | |||
| c30670000d | |||
| 9d8e81d79c | |||
| 421a35c201 | |||
| fcfc7b6cec | |||
| 2f5da3851b | |||
| 6c2e8eba1c | |||
| c9c3937f4b | |||
| 7777cbf6da | |||
| a8a7d327ff | |||
| 571fcbe98d | |||
| 8b4b0e0d39 | |||
| c8868a393b | |||
| 49be37dcf9 | |||
| 2cd5fe2fec | |||
| b52a674c1a | |||
| 8f790d406f | |||
| 7e2a256229 | |||
| c7a0a560d8 | |||
| 1d591034ff | |||
| f5ceaffc5c | |||
| 3c246a97e8 | |||
| ca395541b6 | |||
| 30d9dec438 | |||
| ff3606478c | |||
| 72caf1886d | |||
| bdae9521cb | |||
| cb7fb6c7be | |||
| a33e4477af | |||
| d435192e22 | |||
| 9480447637 | |||
| 7dbe852606 | |||
| d7216f44f5 | |||
| fdf09d46af | |||
| 32360e7473 | |||
| 90482377b7 | |||
| 08a3aea1c7 | |||
| 6eebd1e957 | |||
| 033bd9bbdc | |||
| af634d3a7d | |||
| d42ce3935b | |||
| 7a239c81f7 | |||
| 2b96cd7059 | |||
| 36b8b2c679 | |||
| 69b3be2419 | |||
| bbe74e6987 | |||
| 98d606fca4 | |||
| 5c4472492a | |||
| a5c726eef9 | |||
| d23d5b50a3 | |||
| 37e507707d | |||
| 44d889b99a | |||
| 491092d040 | |||
| b296a1dabe | |||
| 63b8d45ef2 | |||
| 8220fad855 | |||
| 5aa146f0a6 | |||
| f8d2661426 | |||
| 25b8027bd6 | |||
| c93c8a79b7 | |||
| 29336e260c | |||
| 29942e5109 | |||
| 926fee8493 | |||
| eedaacd256 | |||
| e926aa1bf8 | |||
| 597f981fec | |||
| 777fdfbcfa | |||
| aafb587085 | |||
| 1eb2576dbe | |||
| 9a9646d012 | |||
| 8ecb05a094 | |||
| 8c8ca0584f | |||
| b02ba08abc | |||
| 75d213f6b3 | |||
| 9fdeb7a8e3 | |||
| 635b3dbccb | |||
| e42af06609 | |||
| 0416816329 | |||
| 7f8f216263 | |||
| 69ebcf08fc | |||
| cbdb007b26 | |||
| a28c03a2f9 | |||
| 94a8915f6c | |||
| be7192082a | |||
| 54297cacd1 | |||
| 82f5997e61 | |||
| 4d2bc88305 | |||
| cd8dfa331a | |||
| 45d22c6196 | |||
| eeed11e283 | |||
| 476333b3fc | |||
| c7fdbd1c64 | |||
| 0b9bc2a7a7 | |||
| 6949b6666e | |||
| a380b6803a | |||
| 6696c712d3 | |||
| 0d4833d6e3 | |||
| 207bce61ad | |||
| 0f1d367b80 | |||
| bf2e6a33c2 | |||
| b66fed9ae9 | |||
| 4897287fda | |||
| cc1daa5a54 | |||
| 6e7b9472be | |||
| e13ed6436e | |||
| db24690d9b | |||
| 3a39fd23c4 | |||
| d471277031 | |||
| ef57b88343 | |||
| 1e3fcdc109 | |||
| 606b9718a3 | |||
| ab6e30da28 | |||
| 0baea5c1a6 | |||
| bd07310e15 | |||
| bf227508ce | |||
| 1c1ba58579 | |||
| a73f898bb9 | |||
| ee8a52d0c0 | |||
| d5e87a537c | |||
| 18d3786676 | |||
| c906dbad75 | |||
| db20fc7831 | |||
| bd226d94d8 | |||
| 8a487ca1bc | |||
| 7b43a34860 | |||
| 6a60585123 | |||
| 0f7ab32777 | |||
| ffeaf2dec0 | |||
| 3277820381 | |||
| 80d0aadbd0 | |||
| d744cecbfa | |||
| d453813db4 | |||
| c06ebd1e4a | |||
| 10af2d0560 | |||
| 42f2dafb40 | |||
| 6ca7661510 | |||
| 4388cc207f | |||
| 07f14538e3 | |||
| 3d9173a877 | |||
| cb88f53587 | |||
| d76e8be4ff | |||
| e8c6002d08 | |||
| d9033812a2 | |||
| 2e6b93f886 | |||
| afc4e145b6 | |||
| cee243a2a2 | |||
| 5fd74109ff | |||
| a3cc8eb1f6 | |||
| bd4de4832c | |||
| 9e74c934a1 | |||
| a056d4916a | |||
| 31630859a2 | |||
| 8cb41f6797 | |||
| c3a8aeca42 | |||
| eaa95fb1e5 | |||
| 8e6bca34d7 | |||
| 1cdffa2c81 | |||
| 0e4eebbb39 | |||
| 8441589ce6 | |||
| b52ba89cec | |||
| b99e1205c4 | |||
| 8d502743a5 | |||
| 732a764ec6 | |||
| 9975786bac | |||
| 89ef4aa6e7 | |||
| 7e82ac3620 | |||
| c3440c506c | |||
| f16ef93cc3 | |||
| b6f3fc5466 | |||
| 6690f59410 | |||
| 65e08b9633 | |||
| 2e916e63f5 | |||
| f531b9fb21 | |||
| 9581e48bcb | |||
| ad9d58ebc2 | |||
| 896fc5a3f0 | |||
| 94addb6315 | |||
| 2c6d4c5a5c | |||
| f81d6b6157 | |||
| ef17214ae2 | |||
| 5d544c773d | |||
| 721b9df35d | |||
| 526fbfa8f1 | |||
| 0317830b12 | |||
| dfd8c56838 | |||
| b7e33b237b | |||
| 025cb8bd91 | |||
| fa89f2be77 | |||
| bf008a1bee | |||
| 8656ad584b | |||
| e316a9a5b3 | |||
| 7e3a146240 | |||
| 2395d2856b | |||
| 34ae473e6f | |||
| 656c54ead9 | |||
| c6e21c9c5c | |||
| e71e32122c | |||
| 522105a858 | |||
| e3b008e19a | |||
| 776cfed2b3 | |||
| 85cf2a3692 | |||
| c9b700ef6a | |||
| 34fde7d16d | |||
| 5911c4d2db | |||
| dfae72e9af | |||
| 085493d580 | |||
| 5245c7f2ab | |||
| 4ccd649358 | |||
| 32f42d59b1 | |||
| ca618f2bf2 | |||
| a0ae0b3922 | |||
| a09329949a | |||
| b67e360302 | |||
| 3d30ad843f | |||
| d37935dd78 | |||
| 512d5882c9 | |||
| 247deacbb7 | |||
| e79926db6c | |||
| 34a0bd4c38 | |||
| fb820fa9a7 | |||
| 423175f539 | |||
| 007848c42e | |||
| 49e6fd3c60 | |||
| 80129e7483 | |||
| dc74a2326f | |||
| fc0117869d | |||
| 7bca05af64 | |||
| 9b354ba99e | |||
| 194fad7445 | |||
| 18001dc539 | |||
| 78031f2c04 | |||
| 07261fa5d9 | |||
| aa4ffc7bda | |||
| cee7f7a280 | |||
| c7f173bbb2 | |||
| 118bd75a68 | |||
| 9a593f147f | |||
| dfcbdeb002 | |||
| df20365a6d | |||
| 9c6973a46a | |||
| b439be8fb5 | |||
| edaf84a78f | |||
| 056ed3b3c4 | |||
| 317898d41c | |||
| c8b26eeac4 | |||
| 766d8f0ba4 | |||
| e159e504fa | |||
| 8bcb048f53 | |||
| 0fa9f7c609 | |||
| c24b580165 | |||
| cc39ede920 | |||
| 7a4ef7b257 | |||
| 8c52870e07 | |||
| 922dd6cf9c | |||
| 9633532dd4 | |||
| 5abf6b9f20 | |||
| 478550ec93 | |||
| bb7c9b2227 | |||
| 8538d7881a | |||
| 5f28bc4468 | |||
| 7ed65407e6 | |||
| 97e421306b | |||
| 1ce9e7c6bb | |||
| df4ae597a5 | |||
| 8c512bce9e | |||
| 4775010452 | |||
| 03f4b15c61 | |||
| d8c23c0dcb | |||
| 4ab261b89f | |||
| e057956ede | |||
| 543b9cf0ce | |||
| 591b56d794 | |||
| 7f8375d864 | |||
| 31af4bbeb5 | |||
| 0a11404be2 | |||
| ff723980ac | |||
| 0dfd60ad5e | |||
| 18f57a2100 | |||
| 9b5cb3a631 | |||
| 09e4e4709f | |||
| 00895f00e6 | |||
| c57be7b966 | |||
| 51d94e63f4 | |||
| 6daeec838f | |||
| 53f23939c1 | |||
| 0bbec9e182 | |||
| 101970dcd9 | |||
| 6644151d19 | |||
| 548ffdced1 | |||
| cba4b24b23 | |||
| 3e922c2d41 | |||
| ae6a409cc2 | |||
| 94d79edbd0 | |||
| 4dc331d629 | |||
| 18131735d7 | |||
| 2a51e7a665 | |||
| df7ac77113 | |||
| 1b222249c4 | |||
| 126967cb90 | |||
| 2afa381cae | |||
| 05cbc217a0 | |||
| 54eec40d20 | |||
| 3ab34f911b | |||
| d6e4d0a417 | |||
| fac40f5183 | |||
| ce684a6628 | |||
| 14fac241f7 | |||
| 335579e250 | |||
| c8565be3a5 | |||
| 76e76269cf | |||
| 3c43e2718d | |||
| f2676772c8 | |||
| c9bf4270fc | |||
| 41ddb7660b | |||
| b3e93ffadf | |||
| 582576b1c3 | |||
| 456135a6ec | |||
| c7357952ec | |||
| b796246d9d | |||
| 598e48e39b | |||
| 2e3c349c5e | |||
| 7f4ff352e8 | |||
| b11bff5a5b | |||
| 223bd459f6 | |||
| ed1673c66c | |||
| 0fda43b603 | |||
| d3b63c592e | |||
| a32af7d77c | |||
| 742d942baa | |||
| 73e86bfc5d | |||
| 0a4c41c958 | |||
| 79a699f0be | |||
| 4a2e6a826b | |||
| 95f56f95ec | |||
| 67e75fb7af | |||
| ebda89d1ae | |||
| 5ce5299651 | |||
| 41bd518182 | |||
| 739e94302d | |||
| e54541aecf | |||
| 338c707579 | |||
| 301ab01911 | |||
| 99089c0f5f | |||
| ec124847d7 | |||
| 89ced19874 | |||
| f997b4a1d5 | |||
| def99728e7 | |||
| fab234dccb | |||
| 0c0572948e | |||
| a4e281265f | |||
| ce71de0d78 | |||
| c5afcaeaf7 | |||
| 889bfce65d | |||
| 1f33d76e87 | |||
| d8de23228f | |||
| 579218aafc | |||
| aefdacc566 | |||
| d619495136 | |||
| 036d1da013 | |||
| 4ba8e7e072 | |||
| f6830992ea | |||
| 769a0cb76f | |||
| 766e837775 | |||
| 749f53a22b | |||
| 851b33aac2 | |||
| fc958a3922 | |||
| 2c31b72c52 | |||
| 8decb02027 | |||
| a0fd87c032 | |||
| c0d862c9f0 | |||
| 8143abc9e7 | |||
| af95dcaef6 | |||
| 5b4aedd4be | |||
| d8c0b16d7e | |||
| 909b56d48e | |||
| a5d857945a | |||
| 1a03e534bd | |||
| e623b539c4 | |||
| 2ff6f5f958 | |||
| 1532188d95 | |||
| 04093692c9 | |||
| a96389a3e4 | |||
| 00e7c84a93 | |||
| e0c924870d | |||
| 1a27ad22a7 | |||
| 7029083266 | |||
| a5f0ec7c7d | |||
| e7dcc06855 | |||
| 867ac49b50 | |||
| bfffbea4a0 | |||
| f8b1c124df | |||
| bc9e290c11 | |||
| 777ef83378 | |||
| 24283dcbd5 | |||
| 2113c83679 | |||
| 77508f38bb | |||
| 6c3eb19b74 | |||
| e173d822e8 | |||
| e2d3ace476 | |||
| 6f79a3107b | |||
| e6a3b2aa28 | |||
| e5dcfdf115 | |||
| 47e12fcc3e | |||
| d51b2884da | |||
| 28e9c10ded | |||
| e27bf04ced | |||
| 65f1b3c976 | |||
| 4529578cd6 | |||
| 6769c96942 | |||
| dcb987732c | |||
| 4f4eba16d6 | |||
| 9da913f5a6 | |||
| a15aa0f7a4 | |||
| efa1eee6e2 | |||
| 55179f0a1a | |||
| 01593d1a69 | |||
| 40e22cfa86 | |||
| 97aeaec8d2 | |||
| 0b9f85d97b | |||
| d266486581 | |||
| c47d2fc750 | |||
| 66c4c8882f | |||
| 72d7dd7690 | |||
| fff354669c | |||
| 07ae4b0be6 | |||
| cc51805c39 | |||
| c61ac2a845 | |||
| 6d67de06a2 | |||
| ec1273893f | |||
| 1e26077d58 | |||
| ad67f002e3 | |||
| 572df32dca | |||
| 6b8181c06f | |||
| 5900542cfb | |||
| a28b825c4d | |||
| d105854619 | |||
| a4f192bc88 | |||
| db925d7fde | |||
| 16b4865035 | |||
| 20b310484b | |||
| 611a191b0e | |||
| 8b856b9d15 | |||
| 3f7df0d15c | |||
| e0917d3c47 | |||
| 19c257703c | |||
| 62b6262534 | |||
| 55bd3ac302 | |||
| 7a7f345f28 | |||
| ff2282a41a | |||
| b5c7c700d5 | |||
| de6330fb80 | |||
| aafb1ffdef | |||
| c5d738d25c | |||
| 15d8252909 | |||
| 8189c58fc3 | |||
| b3e7f4ea21 | |||
| 9c9ae562ec | |||
| f93eea095e | |||
| 09255a52f7 | |||
| 6f9c8c3007 | |||
| 2e99d5da64 | |||
| 72c8586fad | |||
| d98867b810 | |||
| de7061184b | |||
| 4e2483b41a | |||
| d3db4ee63d | |||
| 5d049cc5e8 | |||
| 6218bad00f | |||
| 2968e9c0c7 | |||
| e73051b230 | |||
| acad3e69dd | |||
| 4794dfc17b | |||
| d505ab9eeb | |||
| 631eeb9bc0 | |||
| 892ca56808 | |||
| 828c7ba451 | |||
| a3d86c03b1 | |||
| 74d6cb802f | |||
| 1b83f66536 | |||
| e5d5cd901a | |||
| 92ae4dda72 | |||
| cd5a88c718 | |||
| 1c744a66e6 | |||
| 57cf7e1f7d | |||
| 86ea00cfee | |||
| 02f8e7da3d | |||
| a245b735b3 | |||
| 0f71983cb9 | |||
| 7db6b9e490 | |||
| 0021b21170 | |||
| 3080dc018a | |||
| f5d0ec32e5 | |||
| 6b3a06a8ed | |||
| 9e89e71e0e | |||
| dcedc78fc2 | |||
| faff057592 | |||
| 56be271b0a | |||
| fa557eb0cc | |||
| b784d1a5e7 | |||
| f6614ac0e4 | |||
| e4aea701ab | |||
| 352f79e9fd | |||
| 3a17ef983e | |||
| 91e571fb68 | |||
| 1a3ee28d01 | |||
| 669aecf4e6 | |||
| ea23db6450 | |||
| 69e4bdd421 | |||
| 86a4fd687c | |||
| 14bc4af90c | |||
| 0cd2b2c0e2 | |||
| 832559926f | |||
| 59411353b1 | |||
| 83bd420cd5 | |||
| 78a0aa5d47 | |||
| 6e31319294 | |||
| cd0b19f93f | |||
| 4f22610499 | |||
| 9e57a9352a | |||
| 4e0d7b56d8 | |||
| f2e10e030d | |||
| 266b7afc72 | |||
| a15dffbb3a | |||
| a30c816cb6 | |||
| e65fe483e1 | |||
| 0d51fad805 | |||
| 17ed38ad05 | |||
| 8259f08882 | |||
| 55d6cf7ab0 | |||
| 425f862cf8 | |||
| 5d6256bede | |||
| ff5b923e6f | |||
| af7a9a68b8 | |||
| 905059d6da | |||
| 3bc56cf3f8 | |||
| 1feb7fc0ba | |||
| c2a40572a5 | |||
| ee7d4d0521 | |||
| 6ab410ef6a | |||
| 8235d966d6 | |||
| c7b83f6ee6 | |||
| 460f20a4ce | |||
| da408f975e | |||
| 9a98c3991a | |||
| 6e0b2de99f | |||
| 0633d7d3f6 | |||
| 2765720b76 | |||
| 71f23ffce1 | |||
| 1863af147d | |||
| 0d5d74674e | |||
| 45ed0884df | |||
| 45e9f59fdc | |||
| bde6a171f6 | |||
| 49a74755a8 | |||
| 2fbef8638f | |||
| eb4166afe3 | |||
| b3beaacec7 | |||
| 355b728a57 | |||
| 577b0e8f1b | |||
| 35d99564c1 | |||
| 6f9bb38232 | |||
| d02c205910 | |||
| 38681202dc | |||
| 0d20a0acf0 | |||
| 9277a86403 | |||
| 5ec8688cf6 | |||
| 6ae82a9cb4 | |||
| 72a4b92022 | |||
| 0cc68bc125 | |||
| 6ca917f4db | |||
| 8a848deddc | |||
| 2ebd4b15a4 | |||
| f0274f3f26 | |||
| 85b2e5d758 | |||
| eef03882ad | |||
| f7e5d962c0 | |||
| 87c6a40b3f | |||
| e614e17a71 | |||
| b4dc5e620b | |||
| 0713e65fc5 | |||
| b69f6cf70a | |||
| 2c6409a67a | |||
| ad7db78829 | |||
| bd9e3e5794 | |||
| bd32ed5598 | |||
| 5a5257a598 | |||
| 75b6ebf287 | |||
| a9d3ae4ef8 | |||
| d480b6cf3e | |||
| fdb640e361 | |||
| 924a8533f1 | |||
| 72b4f270ff | |||
| 946539e32d | |||
| 9882fed6d7 | |||
| 93f45c0a94 | |||
| c6d358a6f3 | |||
| 2e4c362ccd | |||
| f959e1a134 | |||
| 7dfc4a404c | |||
| 2af349eb72 | |||
| 43f3a1e8b3 | |||
| 13c186dfbe | |||
| 4d88736d13 | |||
| 1da633e28a | |||
| 879da47f0e | |||
| cacafb461d | |||
| 15e285c6b4 | |||
| 71c33420f6 | |||
| e7f70bba5c | |||
| e4ec2aa55f | |||
| fc495a5f1e | |||
| 6fe4dfcad0 | |||
| dac820f957 | |||
| 91f8df8d19 | |||
| 9b507f6c6c | |||
| 5e583d3c50 | |||
| d706b57fe9 | |||
| 1063a16013 | |||
| 46a2073427 | |||
| d7bb9574e7 | |||
| 9c18893ae5 | |||
| 4503c320e5 | |||
| c4995bd153 | |||
| af0f5b37d8 | |||
| b9ba4671b4 | |||
| 50b8f13037 | |||
| 4aa9dca608 | |||
| 98dc5328a0 | |||
| 408671b58a | |||
| 1bda527e3d | |||
| e0f1b9ebf0 | |||
| 55127aa43f | |||
| 888fbe3549 | |||
| ed5c061566 | |||
| df6b1d1471 | |||
| 5e0f09075d | |||
| 2daa1b6007 | |||
| 4ff2ad9fac | |||
| ba06e8091f | |||
| 692b3107ac | |||
| 6baf9e1c37 | |||
| c07e662b90 | |||
| aca8b32e5b | |||
| 0ec8a6e0af | |||
| 41af7c8883 | |||
| 7f2070f7b7 | |||
| 1cc74ec116 | |||
| 627f662384 | |||
| c791881c87 | |||
| b0782885d5 | |||
| d25d60f0f0 | |||
| d356e722da | |||
| 84e2fc91ae | |||
| 9768fb020c | |||
| e25112ad35 | |||
| e18b446190 | |||
| 0848d4ed10 | |||
| 7514aea813 | |||
| 58031ab21d | |||
| c1c2ca3ec1 | |||
| b863a363da | |||
| b42db46abd | |||
| d46863e199 | |||
| 751ce421cd | |||
| 74e1dccf50 | |||
| bf3eaa9eb7 | |||
| b4f22310ea | |||
| 2da70ca024 | |||
| 42babbc595 | |||
| ba4735d4a8 | |||
| 31e7addf2f | |||
| ba339ffdad | |||
| a05cbab7c6 | |||
| e708e59b15 | |||
| dd5878015a | |||
| c72f613afc | |||
| 1159e0911f | |||
| 9f180179d5 | |||
| 238700cbdb | |||
| df43b19510 | |||
| e4bfb3ca32 | |||
| 0234410b43 | |||
| 4877edb79b | |||
| 1c4ee62397 | |||
| 7ea7e5ac6c | |||
| 32fa51818b | |||
| e0bd05a8c4 | |||
| a25315a994 | |||
| 03e493453b | |||
| 4d6f9da578 | |||
| 783b1feb70 | |||
| 9925b327b4 | |||
| f75287b6b9 | |||
| cc72d35c6b | |||
| 89d8133ad2 | |||
| e56833c7b2 | |||
| 2c9f8ba598 | |||
| 1f16bba342 | |||
| 4cde51b3ce | |||
| 267e009ae3 | |||
| 0ba1a1dabc | |||
| 6739da5acb | |||
| b0d5e1d844 | |||
| ea6f526ef9 | |||
| 3a7b1c6dd4 | |||
| b98e421b8a | |||
| 1bd5d12665 | |||
| 02de5e96ba | |||
| bc56213010 |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
|
||||
// this transforms async functions into generator functions, which
|
||||
// are then made to use the regenerator module by babel's
|
||||
// transform-regnerator plugin (which is enabled by es2015).
|
||||
"transform-async-to-bluebird",
|
||||
|
||||
// This makes sure that the regenerator runtime is available to
|
||||
// the transpiled code.
|
||||
"transform-runtime",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
steps:
|
||||
- label: ":eslint: Lint"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn lint"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: ":karma: Tests"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn test"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: "📃 Docs"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn gendoc"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- wait
|
||||
|
||||
- label: "🐴 Trigger matrix-react-sdk"
|
||||
trigger: "matrix-react-sdk"
|
||||
branches: "develop"
|
||||
build:
|
||||
branch: "develop"
|
||||
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
|
||||
async: true
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright 2017 Aviral Dasgupta
|
||||
#
|
||||
# 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.
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
@@ -0,0 +1,86 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint", // now needed for class properties
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
||||
// babel's transform-runtime converts references to ES6 globals such as
|
||||
// Promise and Map to core-js polyfills, so we can use ES6 globals.
|
||||
es6: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
plugins: [
|
||||
"babel",
|
||||
],
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
"max-len": ["error", {
|
||||
code: 90,
|
||||
ignoreComments: true,
|
||||
}],
|
||||
curly: ["error", "multi-line"],
|
||||
"prefer-const": ["error"],
|
||||
"comma-dangle": ["error", {
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "always-multiline",
|
||||
}],
|
||||
|
||||
// loosen jsdoc requirements a little
|
||||
"require-jsdoc": ["error", {
|
||||
require: {
|
||||
FunctionDeclaration: false,
|
||||
}
|
||||
}],
|
||||
"valid-jsdoc": ["error", {
|
||||
requireParamDescription: false,
|
||||
requireReturn: false,
|
||||
requireReturnDescription: false,
|
||||
}],
|
||||
|
||||
// rules we do not want from eslint-recommended
|
||||
"no-console": ["off"],
|
||||
"no-constant-condition": ["off"],
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }],
|
||||
|
||||
// rules we do not want from the google styleguide
|
||||
"object-curly-spacing": ["off"],
|
||||
"spaced-comment": ["off"],
|
||||
"guard-for-in": ["off"],
|
||||
|
||||
// in principle we prefer single quotes, but life is too short
|
||||
quotes: ["off"],
|
||||
|
||||
// rules we'd ideally like to adhere to, but the current
|
||||
// code does not (in most cases because it's still ES5)
|
||||
// we set these to warnings, and assert that the number
|
||||
// of warnings doesn't exceed a given threshold
|
||||
"no-var": ["warn"],
|
||||
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
"padded-blocks": ["warn"],
|
||||
"no-extend-native": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
|
||||
// eslint's built in no-invalid-this rule breaks with class properties
|
||||
"no-invalid-this": "off",
|
||||
// so we replace it with a version that is class property aware
|
||||
"babel/no-invalid-this": "error",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
patreon: matrixdotorg
|
||||
liberapay: matrixdotorg
|
||||
+9
-4
@@ -1,15 +1,20 @@
|
||||
# Keep this file in sync with .npmignore.
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
/.npmrc
|
||||
/*.log
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
# version file and tarball created by 'npm pack'
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"node": true,
|
||||
"jasmine": true,
|
||||
|
||||
"nonew": true,
|
||||
"curly": true,
|
||||
"forin": true,
|
||||
"freeze": false,
|
||||
"undef": true,
|
||||
"unused": "vars"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
# Keep this file in sync with .gitignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
|
||||
# tarball created by 'npm pack'.
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
+1957
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,124 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
|
||||
|
||||
How to contribute
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
**The single biggest thing you need to know is: please base your changes on
|
||||
the develop branch - /not/ master.**
|
||||
|
||||
We use the master branch to track the most recent release, so that folks who
|
||||
blindly clone the repo and automatically check out master get something that
|
||||
works. Develop is the unstable branch where all the development actually
|
||||
happens: the workflow is that contributors should fork the develop branch to
|
||||
make a 'feature' branch for a particular contribution, and then make a pull
|
||||
request to merge this back into the matrix.org 'official' develop branch. We
|
||||
use GitHub's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
We use Travis for continuous integration, and all pull requests get
|
||||
automatically tested by Travis: if your change breaks the build, then the PR
|
||||
will show that there are failed checks, so please check back after a few
|
||||
minutes.
|
||||
|
||||
Code style
|
||||
~~~~~~~~~~
|
||||
|
||||
The code-style for matrix-js-sdk is not formally documented, but contributors
|
||||
are encouraged to read the code style document for matrix-react-sdk
|
||||
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
|
||||
and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and **never** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
Attribution
|
||||
~~~~~~~~~~~
|
||||
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
Sign off
|
||||
~~~~~~~~
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix::
|
||||
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment::
|
||||
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the ``-s`` flag to
|
||||
``git commit``, which uses the name and email set in your ``user.name`` and
|
||||
``user.email`` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase::
|
||||
|
||||
git rebase --signoff origin/develop
|
||||
@@ -1,8 +1,7 @@
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
[](http://matrix.org/jenkins/job/JavascriptSDK/)
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for
|
||||
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
|
||||
JavaScript. This SDK can be run in a browser or in Node.js.
|
||||
|
||||
Quickstart
|
||||
@@ -10,16 +9,22 @@ Quickstart
|
||||
|
||||
In a browser
|
||||
------------
|
||||
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
|
||||
your page. There will be a global variable ``matrixcs`` attached to
|
||||
``window`` through which you can access the SDK.
|
||||
Download either the full or minified version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
``<script>`` to your page. There will be a global variable ``matrixcs``
|
||||
attached to ``window`` through which you can access the SDK. See below for how to
|
||||
include libolm to enable end-to-end-encryption.
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
|
||||
In Node.js
|
||||
----------
|
||||
|
||||
``npm install matrix-js-sdk``
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
|
||||
```javascript
|
||||
var sdk = require("matrix-js-sdk");
|
||||
@@ -29,7 +34,60 @@ In Node.js
|
||||
});
|
||||
```
|
||||
|
||||
Please check [the Node.js terminal app](examples/node) for a more complex example.
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
```javascript
|
||||
await client.startClient({initialSyncLimit: 10});
|
||||
```
|
||||
|
||||
You can perform a call to `/sync` to get the current state of the client:
|
||||
|
||||
```javascript
|
||||
client.once('sync', function(state, prevState, res) {
|
||||
if(state === 'PREPARED') {
|
||||
console.log("prepared");
|
||||
} else {
|
||||
console.log(state);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
To send a message:
|
||||
|
||||
```javascript
|
||||
var content = {
|
||||
"body": "message text",
|
||||
"msgtype": "m.text"
|
||||
};
|
||||
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
console.log(err);
|
||||
});
|
||||
```
|
||||
|
||||
To listen for message events:
|
||||
|
||||
```javascript
|
||||
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
});
|
||||
```
|
||||
|
||||
By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
|
||||
|
||||
```javascript
|
||||
Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
client.getRoom(roomId).timeline.forEach(t => {
|
||||
console.log(t.event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
What does this SDK do?
|
||||
----------------------
|
||||
@@ -64,6 +122,7 @@ Later versions of the SDK will:
|
||||
Usage
|
||||
=====
|
||||
|
||||
|
||||
Conventions
|
||||
-----------
|
||||
|
||||
@@ -78,7 +137,7 @@ are updated.
|
||||
client.on("event", function(event) {
|
||||
console.log(event.getType());
|
||||
});
|
||||
|
||||
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
if (member.typing) {
|
||||
@@ -88,38 +147,43 @@ are updated.
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
### Promises or Callbacks
|
||||
### Promises and Callbacks
|
||||
|
||||
The SDK supports *both* callbacks and Promises (Q). The convention
|
||||
you'll see used is:
|
||||
Most of the methods in the SDK are asynchronous: they do not directly return a
|
||||
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
|
||||
which will be fulfilled in the future.
|
||||
|
||||
The typical usage is something like:
|
||||
|
||||
```javascript
|
||||
var promise = matrixClient.someMethod(arg1, arg2, callback);
|
||||
```
|
||||
|
||||
The ``callback`` parameter is optional, so you could do:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).then(function(err, result) {
|
||||
matrixClient.someMethod(arg1, arg2).done(function(result) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, you could do:
|
||||
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2, function(result) {
|
||||
...
|
||||
});
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
Methods which support this will be clearly marked as returning
|
||||
``Promises``.
|
||||
|
||||
The main thing to note is that it is an error to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved. If
|
||||
you have nothing better to do with the result, just call ``.done()`` on it. See
|
||||
http://documentup.com/kriskowal/q/#the-end for more information.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
|
||||
via an optional ``callback`` argument. The callback support is now deprecated:
|
||||
new methods do not include a ``callback`` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -147,10 +211,10 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
|
||||
### Print out messages for all rooms
|
||||
|
||||
```javascript
|
||||
@@ -166,7 +230,7 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
@@ -198,10 +262,10 @@ Output:
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
```
|
||||
My Room
|
||||
@@ -211,7 +275,7 @@ Output:
|
||||
(join) Bob
|
||||
(invite) @charlie:localhost
|
||||
```
|
||||
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
@@ -222,13 +286,53 @@ This SDK uses JSDoc3 style comments. You can manually build and
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ npm run gendoc
|
||||
$ yarn gendoc
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
```
|
||||
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
If the ``Olm`` global is not available, the SDK will show a warning, as shown
|
||||
below; ``initCrypto()`` will also fail.
|
||||
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
```
|
||||
|
||||
If the crypto layer is not (successfully) initialised, the SDK will continue to
|
||||
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
|
||||
specification.
|
||||
|
||||
To provide the Olm library in a browser application:
|
||||
|
||||
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://packages.matrix.org/npm/olm/)
|
||||
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
|
||||
application also works without e2e crypto enabled, add ``--optional`` to mark it
|
||||
as an optional dependency.
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
*This section is for people who want to modify the SDK. If you just
|
||||
@@ -236,7 +340,7 @@ want to use this SDK, skip this section.*
|
||||
|
||||
First, you need to pull in the right build tools:
|
||||
```
|
||||
$ npm install
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
Building
|
||||
@@ -244,20 +348,20 @@ Building
|
||||
|
||||
To build a browser version from scratch when developing::
|
||||
```
|
||||
$ npm run build
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
To constantly do builds when files are modified (using ``watchify``)::
|
||||
```
|
||||
$ npm run watch
|
||||
$ yarn watch
|
||||
```
|
||||
|
||||
To run tests (Jasmine)::
|
||||
```
|
||||
$ npm test
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
|
||||
To run linting:
|
||||
```
|
||||
$ npm run lint
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
There is a script `release.sh` which does the following, but if you need to do
|
||||
a release manually, here are the steps:
|
||||
|
||||
- `git checkout -b release-v0.x.x`
|
||||
- Update `CHANGELOG.md`
|
||||
- `npm version 0.x.x`
|
||||
- Merge `release-v0.x.x` onto `master`.
|
||||
- Push `master`.
|
||||
- Push the tag: `git push --tags`
|
||||
- `npm publish`
|
||||
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
|
||||
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
|
||||
- Merge `master` onto `develop`.
|
||||
- Push `develop`.
|
||||
+32
-1
@@ -1,4 +1,35 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("browser-request"));
|
||||
const request = require('browser-request');
|
||||
const queryString = require('qs');
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
// we mimic `request`'s query string interface to make it all work
|
||||
// as expected.
|
||||
// browser-request will happily take the constructed string as the
|
||||
// query string without trying to modify it further.
|
||||
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
|
||||
return request(opts, fn);
|
||||
});
|
||||
|
||||
// just *accessing* indexedDB throws an exception in firefox with
|
||||
// indexeddb disabled.
|
||||
var indexedDB;
|
||||
try {
|
||||
indexedDB = global.indexedDB;
|
||||
} catch(e) {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
matrixcs.setCryptoStoreFactory(
|
||||
function() {
|
||||
return new matrixcs.IndexedDBCryptoStore(
|
||||
indexedDB, "matrix-js-sdk:crypto"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = matrixcs; // keep export for browserify package deps
|
||||
global.matrixcs = matrixcs;
|
||||
|
||||
Vendored
-5826
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-6490
File diff suppressed because it is too large
Load Diff
-2
File diff suppressed because one or more lines are too long
Vendored
-9900
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-10023
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-10864
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15420
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15577
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-15785
File diff suppressed because it is too large
Load Diff
-6
File diff suppressed because one or more lines are too long
Vendored
-15832
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-16305
File diff suppressed because it is too large
Load Diff
-8
File diff suppressed because one or more lines are too long
Vendored
-16327
File diff suppressed because it is too large
Load Diff
-8
File diff suppressed because one or more lines are too long
Vendored
-37345
File diff suppressed because one or more lines are too long
-38
File diff suppressed because one or more lines are too long
Vendored
-1
@@ -1 +0,0 @@
|
||||
Release builds and development builds will reside here.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Browser Storage Notes
|
||||
|
||||
## Overview
|
||||
|
||||
Browsers examined: Firefox 67, Chrome 75
|
||||
|
||||
The examination below applies to the default, non-persistent storage policy.
|
||||
|
||||
## Quota Measurement
|
||||
|
||||
Browsers appear to enforce and measure the quota in terms of space on disk, not
|
||||
data stored, so you may be able to store more data than the simple sum of all
|
||||
input data depending on how compressible your data is.
|
||||
|
||||
## Quota Limit
|
||||
|
||||
Specs and documentation suggest we should consistently receive
|
||||
`QuotaExceededError` when we're near space limits, but the reality is a bit
|
||||
blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
* Chrome
|
||||
* Log database may fail to start with AbortError
|
||||
* IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
* When near the quota, QuotaExceededError is used more consistently
|
||||
* Firefox
|
||||
* The first error will be QuotaExceededError
|
||||
* Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
* Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
* Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
While the Storage Standard says all storage for an origin group should be
|
||||
limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
* Chrome, Firefox
|
||||
* IndexedDB for origin deleted
|
||||
* Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
* Chrome
|
||||
* Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
* Firefox
|
||||
* Firefox 67 shows a prompt to grant
|
||||
* Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
* Chrome, Firefox
|
||||
* Can run this at any time to request an estimate of space remaining
|
||||
* Firefox
|
||||
* Returns `0` for `usage` if a site is persisted
|
||||
@@ -0,0 +1,31 @@
|
||||
Random notes from Matthew on the two possible approaches for warning users about unexpected
|
||||
unverified devices popping up in their rooms....
|
||||
|
||||
Original idea...
|
||||
================
|
||||
|
||||
Warn when an existing user adds an unknown device to a room.
|
||||
|
||||
Warn when a user joins the room with unverified or unknown devices.
|
||||
|
||||
Warn when you initial sync if the room has any unverified devices in it.
|
||||
^ this is good enough if we're doing local storage.
|
||||
OR, better:
|
||||
Warn when you initial sync if the room has any new undefined devices since you were last there.
|
||||
=> This means persisting the rooms that devices are in, across initial syncs.
|
||||
|
||||
|
||||
Updated idea...
|
||||
===============
|
||||
|
||||
Warn when the user tries to send a message:
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
|
||||
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
|
||||
@@ -1 +1 @@
|
||||
../../../dist/browser-matrix-dev.js
|
||||
../../../dist/browser-matrix.js
|
||||
+12
-8
@@ -135,11 +135,15 @@ rl.on('line', function(line) {
|
||||
// ==== END User input
|
||||
|
||||
// show the room list after syncing.
|
||||
matrixClient.on("syncComplete", function() {
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
matrixClient.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on("Room", function() {
|
||||
@@ -198,9 +202,9 @@ function printRoomList() {
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(
|
||||
/T/, ' ').replace(/\..+/, '');
|
||||
}
|
||||
var me = roomList[i].getMember(myUserId);
|
||||
if (me) {
|
||||
fmt = fmts[me.membership];
|
||||
var myMembership = roomList[i].getMyMembership();
|
||||
if (myMembership) {
|
||||
fmt = fmts[myMembership];
|
||||
}
|
||||
var roomName = fixWidth(roomList[i].name, 25);
|
||||
print(
|
||||
|
||||
@@ -44,7 +44,15 @@ window.onload = function() {
|
||||
disableButtons(true, true, true);
|
||||
};
|
||||
|
||||
client.on("syncComplete", function () {
|
||||
client.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
|
||||
disableButtons(false, true, true);
|
||||
|
||||
@@ -85,5 +93,5 @@ client.on("syncComplete", function () {
|
||||
call = c;
|
||||
addListeners(call);
|
||||
});
|
||||
});
|
||||
}
|
||||
client.startClient();
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../../dist/browser-matrix-dev.js
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
npm run lint
|
||||
yarn lint
|
||||
|
||||
+17
-12
@@ -1,9 +1,12 @@
|
||||
#!/bin/bash -l
|
||||
|
||||
export NVM_DIR="/home/jenkins/.nvm"
|
||||
set -x
|
||||
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
nvm use 0.10
|
||||
npm install
|
||||
|
||||
nvm use 10 || exit $?
|
||||
yarn install || exit $?
|
||||
|
||||
RC=0
|
||||
|
||||
@@ -12,20 +15,22 @@ function fail {
|
||||
RC=1
|
||||
}
|
||||
|
||||
npm test || fail "npm test finished with return code $?"
|
||||
# don't use last time's test reports
|
||||
rm -rf reports coverage || exit $?
|
||||
|
||||
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml ||
|
||||
fail "jshint finished with return code $?"
|
||||
yarn test || fail "yarn test finished with return code $?"
|
||||
|
||||
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 \
|
||||
--max_line_length 90 \
|
||||
-r lib/ -r spec/ > gjslint.log ||
|
||||
fail "gjslint finished with return code $?"
|
||||
yarn -s lint -f checkstyle > eslint.xml ||
|
||||
fail "eslint finished with return code $?"
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-js-sdk-*.tgz
|
||||
|
||||
npm pack ||
|
||||
fail "npm pack finished with return code $?"
|
||||
# `yarn pack` doesn't seem to run scripts, however that seems okay here as we
|
||||
# just built as part of `install` above.
|
||||
yarn pack ||
|
||||
fail "yarn pack finished with return code $?"
|
||||
|
||||
yarn gendoc || fail "JSDoc failed with code $?"
|
||||
|
||||
exit $RC
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
var Olm = require("olm");
|
||||
var utils = require("./utils");
|
||||
|
||||
/**
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
* OlmAccount and a number of OlmSessions.
|
||||
*
|
||||
* Accounts and sessions are kept pickled in a sessionStore.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} sessionStore A store to be used for data in end-to-end
|
||||
* crypto
|
||||
*
|
||||
* @property {string} deviceCurve25519Key Curve25519 key for the account
|
||||
* @property {string} deviceEd25519Key Ed25519 key for the account
|
||||
*/
|
||||
function OlmDevice(sessionStore) {
|
||||
this._sessionStore = sessionStore;
|
||||
this._pickleKey = "DEFAULT_KEY";
|
||||
|
||||
var e2eKeys;
|
||||
var account = new Olm.Account();
|
||||
try {
|
||||
var e2eAccount = this._sessionStore.getEndToEndAccount();
|
||||
if (e2eAccount === null) {
|
||||
account.create();
|
||||
var pickled = account.pickle(this._pickleKey);
|
||||
this._sessionStore.storeEndToEndAccount(pickled);
|
||||
} else {
|
||||
account.unpickle(this._pickleKey, e2eAccount);
|
||||
}
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
|
||||
this.deviceCurve25519Key = e2eKeys.curve25519;
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* extract our OlmAccount from the session store and call the given function
|
||||
*
|
||||
* @param {function} func
|
||||
* @return {object} result of func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getAccount = function(func) {
|
||||
var account = new Olm.Account();
|
||||
try {
|
||||
var pickledAccount = this._sessionStore.getEndToEndAccount();
|
||||
account.unpickle(this._pickleKey, pickledAccount);
|
||||
return func(account);
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* store our OlmAccount in the session store
|
||||
*
|
||||
* @param {OlmAccount} account
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveAccount = function(account) {
|
||||
var pickledAccount = account.pickle(this._pickleKey);
|
||||
this._sessionStore.storeEndToEndAccount(pickledAccount);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* extract an OlmSession from the session store and call the given function
|
||||
*
|
||||
* @param {string} deviceKey
|
||||
* @param {string} sessionId
|
||||
* @param {function} func
|
||||
* @return {object} result of func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
|
||||
var sessions = this._sessionStore.getEndToEndSessions(deviceKey);
|
||||
var pickledSession = sessions[sessionId];
|
||||
|
||||
var session = new Olm.Session();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, pickledSession);
|
||||
return func(session);
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* store our OlmSession in the session store
|
||||
*
|
||||
* @param {string} deviceKey
|
||||
* @param {OlmSession} session
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveSession = function(deviceKey, session) {
|
||||
var pickledSession = session.pickle(this._pickleKey);
|
||||
this._sessionStore.storeEndToEndSession(
|
||||
deviceKey, session.session_id(), pickledSession
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get an OlmUtility and call the given function
|
||||
*
|
||||
* @param {function} func
|
||||
* @return {object} result of func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getUtility = function(func) {
|
||||
var utility = new Olm.Utility();
|
||||
try {
|
||||
return func(utility);
|
||||
} finally {
|
||||
utility.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Signs a message with the ed25519 key for this account.
|
||||
*
|
||||
* @param {string} message message to be signed
|
||||
* @return {string} base64-encoded signature
|
||||
*/
|
||||
OlmDevice.prototype.sign = function(message) {
|
||||
return this._getAccount(function(account) {
|
||||
return account.sign(message);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current (unused, unpublished) one-time keys for this account.
|
||||
*
|
||||
* @return {object} one time keys; an object with the single property
|
||||
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
|
||||
* key.
|
||||
*/
|
||||
OlmDevice.prototype.getOneTimeKeys = function() {
|
||||
return this._getAccount(function(account) {
|
||||
return JSON.parse(account.one_time_keys());
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the maximum number of one-time keys we can store.
|
||||
*
|
||||
* @return {number} number of keys
|
||||
*/
|
||||
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
|
||||
return this._getAccount(function(account) {
|
||||
return account.max_number_of_one_time_keys();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks all of the one-time keys as published.
|
||||
*/
|
||||
OlmDevice.prototype.markKeysAsPublished = function() {
|
||||
var self = this;
|
||||
this._getAccount(function(account) {
|
||||
account.mark_keys_as_published();
|
||||
self._saveAccount(account);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate some new one-time keys
|
||||
*
|
||||
* @param {number} numKeys number of keys to generate
|
||||
*/
|
||||
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
|
||||
var self = this;
|
||||
this._getAccount(function(account) {
|
||||
account.generate_one_time_keys(numKeys);
|
||||
self._saveAccount(account);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new outbound session
|
||||
*
|
||||
* The new session will be stored in the sessionStore.
|
||||
*
|
||||
* @param {string} theirIdentityKey remote user's Curve25519 identity key
|
||||
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
|
||||
* @return {string} sessionId for the outbound session.
|
||||
*/
|
||||
OlmDevice.prototype.createOutboundSession = function(
|
||||
theirIdentityKey, theirOneTimeKey
|
||||
) {
|
||||
var self = this;
|
||||
return this._getAccount(function(account) {
|
||||
var session = new Olm.Session();
|
||||
try {
|
||||
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
|
||||
self._saveSession(theirIdentityKey, session);
|
||||
return session.session_id();
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new inbound session, given an incoming message
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
|
||||
* @param {number} message_type message_type field from the received message (must be 0)
|
||||
* @param {string} ciphertext base64-encoded body from the received message
|
||||
*
|
||||
* @return {string} decrypted payload
|
||||
*
|
||||
* @raises {Error} if the received message was not valid (for instance, it
|
||||
* didn't use a valid one-time key).
|
||||
*/
|
||||
OlmDevice.prototype.createInboundSession = function(
|
||||
theirDeviceIdentityKey, message_type, ciphertext
|
||||
) {
|
||||
if (message_type !== 0) {
|
||||
throw new Error("Need message_type == 0 to create inbound session");
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return this._getAccount(function(account) {
|
||||
var session = new Olm.Session();
|
||||
try {
|
||||
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
|
||||
account.remove_one_time_keys(session);
|
||||
self._saveAccount(account);
|
||||
|
||||
var payloadString = session.decrypt(message_type, ciphertext);
|
||||
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
|
||||
return payloadString;
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of known session IDs for the given device
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @return {string[]} a list of known session ids for the device
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
|
||||
var sessions = this._sessionStore.getEndToEndSessions(
|
||||
theirDeviceIdentityKey
|
||||
);
|
||||
return utils.keys(sessions);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message using an existing session
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @param {string} sessionId the id of the active session
|
||||
* @param {string} payloadString payload to be encrypted and sent
|
||||
*
|
||||
* @return {string} ciphertext
|
||||
*/
|
||||
OlmDevice.prototype.encryptMessage = function(
|
||||
theirDeviceIdentityKey, sessionId, payloadString
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrypt an incoming message using an existing session
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @param {string} sessionId the id of the active session
|
||||
* @param {number} message_type message_type field from the received message
|
||||
* @param {string} ciphertext base64-encoded body from the received message
|
||||
*
|
||||
* @return {object} Result, with keys <ul>
|
||||
* <li><tt>matchesInbound</tt>: (boolean): true if the message was a pre-key
|
||||
* message which matched an existing inbound session.</li>
|
||||
* <li><tt>payload</tt>: (string): decrypted payload; null if the received
|
||||
* message was not valid (for instance, it did not match this session).</li>
|
||||
* </ul>
|
||||
*/
|
||||
OlmDevice.prototype.decryptMessage = function(
|
||||
theirDeviceIdentityKey, sessionId, message_type, ciphertext
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var matchesInbound = (message_type === 0 &&
|
||||
session.matches_inbound(ciphertext));
|
||||
|
||||
var payloadString = null;
|
||||
try {
|
||||
payloadString = session.decrypt(message_type, ciphertext);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"Failed to decrypt with an existing session: " + e.message
|
||||
);
|
||||
// return null as the payload
|
||||
}
|
||||
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
|
||||
return {
|
||||
matchesInbound: matchesInbound,
|
||||
payload: payloadString,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Verify an ed25519 signature.
|
||||
*
|
||||
* @param {string} key ed25519 key
|
||||
* @param {string} message message which was signed
|
||||
* @param {string} signature base64-encoded signature to be checked
|
||||
*
|
||||
* @raises {Error} if there is a problem with the verification. If the key was
|
||||
* too small then the message will be "OLM.INVALID_BASE64". If the signature
|
||||
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
|
||||
*/
|
||||
OlmDevice.prototype.verifySignature = function(
|
||||
key, message, signature
|
||||
) {
|
||||
this._getUtility(function(util) {
|
||||
util.ed25519_verify(key, message, signature);
|
||||
});
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = OlmDevice;
|
||||
@@ -1,846 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
/**
|
||||
* This is an internal module. MatrixBaseApis is currently only meant to be used
|
||||
* by {@link client~MatrixClient}.
|
||||
*
|
||||
* @module base-apis
|
||||
*/
|
||||
|
||||
var httpApi = require("./http-api");
|
||||
var utils = require("./utils");
|
||||
|
||||
/**
|
||||
* Low-level wrappers for the Matrix APIs
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {Object} opts Configuration options
|
||||
*
|
||||
* @param {string} opts.baseUrl Required. The base URL to the client-server
|
||||
* HTTP API.
|
||||
*
|
||||
* @param {string} opts.idBaseUrl Optional. The base identity server URL for
|
||||
* identity server requests.
|
||||
*
|
||||
* @param {Function} opts.request Required. The function to invoke for HTTP
|
||||
* requests. The value of this property is typically <code>require("request")
|
||||
* </code> as it returns a function which meets the required interface. See
|
||||
* {@link requestFunction} for more information.
|
||||
*
|
||||
* @param {string} opts.accessToken The access_token for this user.
|
||||
*
|
||||
* @param {Object} opts.queryParams Optional. Extra query parameters to append
|
||||
* to all requests with this client. Useful for application services which require
|
||||
* <code>?user_id=</code>.
|
||||
*
|
||||
*/
|
||||
function MatrixBaseApis(opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
|
||||
|
||||
this.baseUrl = opts.baseUrl;
|
||||
this.idBaseUrl = opts.idBaseUrl;
|
||||
|
||||
var httpOpts = {
|
||||
baseUrl: opts.baseUrl,
|
||||
idBaseUrl: opts.idBaseUrl,
|
||||
accessToken: opts.accessToken,
|
||||
request: opts.request,
|
||||
prefix: httpApi.PREFIX_R0,
|
||||
onlyData: true,
|
||||
extraParams: opts.queryParams
|
||||
};
|
||||
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Homeserver URL of this client
|
||||
* @return {string} Homeserver URL of this client
|
||||
*/
|
||||
MatrixBaseApis.prototype.getHomeserverUrl = function() {
|
||||
return this.baseUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Identity Server URL of this client
|
||||
* @return {string} Identity Server URL of this client
|
||||
*/
|
||||
MatrixBaseApis.prototype.getIdentityServerUrl = function() {
|
||||
return this.idBaseUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the access token associated with this account.
|
||||
* @return {?String} The access_token or null
|
||||
*/
|
||||
MatrixBaseApis.prototype.getAccessToken = function() {
|
||||
return this._http.opts.accessToken || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {boolean} true if there is a valid access_token for this client.
|
||||
*/
|
||||
MatrixBaseApis.prototype.isLoggedIn = function() {
|
||||
return this._http.opts.accessToken !== undefined;
|
||||
};
|
||||
|
||||
|
||||
// Registration/Login operations
|
||||
// =============================
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.loginFlows = function(callback) {
|
||||
return this._http.request(callback, "GET", "/login");
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs out the current session.
|
||||
* Obviously, further calls that require authorisation should fail after this
|
||||
* method is called. The state of the MatrixClient object is not affected:
|
||||
* it is up to the caller to either reset or destroy the MatrixClient after
|
||||
* this method succeeds.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: On success, the empty object
|
||||
*/
|
||||
MatrixBaseApis.prototype.logout = function(callback) {
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", '/logout'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deactivates the logged-in account.
|
||||
* Obviously, further calls that require authorisation should fail after this
|
||||
* method is called. The state of the MatrixClient object is not affected:
|
||||
* it is up to the caller to either reset or destroy the MatrixClient after
|
||||
* this method succeeds.
|
||||
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: On success, the empty object
|
||||
*/
|
||||
MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
|
||||
var body = {};
|
||||
if (auth) {
|
||||
body = {
|
||||
auth: auth,
|
||||
};
|
||||
}
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Room operations
|
||||
// ===============
|
||||
|
||||
/**
|
||||
* Create a new room.
|
||||
* @param {Object} options a list of options to pass to the /createRoom API.
|
||||
* @param {string} options.room_alias_name The alias localpart to assign to
|
||||
* this room.
|
||||
* @param {string} options.visibility Either 'public' or 'private'.
|
||||
* @param {string[]} options.invite A list of user IDs to invite to this room.
|
||||
* @param {string} options.name The name to give this room.
|
||||
* @param {string} options.topic The topic to give this room.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: <code>{room_id: {string},
|
||||
* room_alias: {string(opt)}}</code>
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createRoom = function(options, callback) {
|
||||
// valid options include: room_alias_name, visibility, invite
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/createRoom", undefined, options
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.roomState = function(roomId, callback) {
|
||||
var path = utils.encodeUri("/rooms/$roomId/state", {$roomId: roomId});
|
||||
return this._http.authedRequest(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a state event.
|
||||
* @param {string} roomId
|
||||
* @param {string} eventType
|
||||
* @param {string} stateKey
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, callback) {
|
||||
var pathParams = {
|
||||
$roomId: roomId,
|
||||
$eventType: eventType,
|
||||
$stateKey: stateKey
|
||||
};
|
||||
var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
|
||||
if (stateKey !== undefined) {
|
||||
path = utils.encodeUri(path + "/$stateKey", pathParams);
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} eventType
|
||||
* @param {Object} content
|
||||
* @param {string} stateKey
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
|
||||
callback) {
|
||||
var pathParams = {
|
||||
$roomId: roomId,
|
||||
$eventType: eventType,
|
||||
$stateKey: stateKey
|
||||
};
|
||||
var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
|
||||
if (stateKey !== undefined) {
|
||||
path = utils.encodeUri(path + "/$stateKey", pathParams);
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, content
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} eventId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
|
||||
var path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
|
||||
$roomId: roomId,
|
||||
$eventId: eventId
|
||||
});
|
||||
return this._http.authedRequest(callback, "POST", path, undefined, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {Number} limit
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
|
||||
if (utils.isFunction(limit)) { callback = limit; limit = undefined; }
|
||||
var path = utils.encodeUri("/rooms/$roomId/initialSync",
|
||||
{$roomId: roomId}
|
||||
);
|
||||
if (!limit) {
|
||||
limit = 30;
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path, { limit: limit }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Room Directory operations
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.publicRooms = function(callback) {
|
||||
return this._http.authedRequest(callback, "GET", "/publicRooms");
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an alias to room ID mapping.
|
||||
* @param {string} alias The room alias to create.
|
||||
* @param {string} roomId The room ID to link the alias to.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
|
||||
var path = utils.encodeUri("/directory/room/$alias", {
|
||||
$alias: alias
|
||||
});
|
||||
var data = {
|
||||
room_id: roomId
|
||||
};
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, data
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an alias to room ID mapping. This alias must be on your local server
|
||||
* and you must have sufficient access to do this operation.
|
||||
* @param {string} alias The room alias to delete.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
|
||||
var path = utils.encodeUri("/directory/room/$alias", {
|
||||
$alias: alias
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "DELETE", path, undefined, undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get room info for the given alias.
|
||||
* @param {string} alias The room alias to resolve.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Object with room_id and servers.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
|
||||
// TODO: deprecate this or resolveRoomAlias
|
||||
var path = utils.encodeUri("/directory/room/$alias", {
|
||||
$alias: alias
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomAlias
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
|
||||
// TODO: deprecate this or getRoomIdForAlias
|
||||
var path = utils.encodeUri("/directory/room/$alias", {$alias: roomAlias});
|
||||
return this._http.request(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the visibility of a room in the current HS's room directory
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getRoomDirectoryVisibility =
|
||||
function(roomId, callback) {
|
||||
var path = utils.encodeUri("/directory/list/room/$roomId", {
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(callback, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the visbility of a room in the current HS's room directory
|
||||
* @param {string} roomId
|
||||
* @param {string} visibility "public" to make the room visible
|
||||
* in the public directory, or "private" to make
|
||||
* it invisible.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomDirectoryVisibility =
|
||||
function(roomId, visibility, callback) {
|
||||
var path = utils.encodeUri("/directory/list/room/$roomId", {
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, { "visibility": visibility }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Media operations
|
||||
// ================
|
||||
|
||||
/**
|
||||
* Upload a file to the media repository on the home server.
|
||||
* @param {File} file object
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.uploadContent = function(file, callback) {
|
||||
return this._http.uploadContent(file, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel a file upload in progress
|
||||
* @param {module:client.Promise} promise The promise returned from uploadContent
|
||||
* @return {boolean} true if canceled, otherwise false
|
||||
*/
|
||||
MatrixBaseApis.prototype.cancelUpload = function(promise) {
|
||||
return this._http.cancelUpload(promise);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of all file uploads in progress
|
||||
* @return {array} Array of objects representing current uploads.
|
||||
* Currently in progress is element 0. Keys:
|
||||
* - promise: The promise associated with the upload
|
||||
* - loaded: Number of bytes uploaded
|
||||
* - total: Total number of bytes to upload
|
||||
*/
|
||||
MatrixBaseApis.prototype.getCurrentUploads = function() {
|
||||
return this._http.getCurrentUploads();
|
||||
};
|
||||
|
||||
|
||||
// Profile operations
|
||||
// ==================
|
||||
|
||||
/**
|
||||
* @param {string} userId
|
||||
* @param {string} info The kind of info to retrieve (e.g. 'displayname',
|
||||
* 'avatar_url').
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
|
||||
if (utils.isFunction(info)) { callback = info; info = undefined; }
|
||||
|
||||
var path = info ?
|
||||
utils.encodeUri("/profile/$userId/$info",
|
||||
{ $userId: userId, $info: info }) :
|
||||
utils.encodeUri("/profile/$userId",
|
||||
{ $userId: userId });
|
||||
return this._http.authedRequest(callback, "GET", path);
|
||||
};
|
||||
|
||||
|
||||
// Account operations
|
||||
// ==================
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getThreePids = function(callback) {
|
||||
var path = "/account/3pid";
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path, undefined, undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} creds
|
||||
* @param {boolean} bind
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
|
||||
var path = "/account/3pid";
|
||||
var data = {
|
||||
'threePidCreds': creds,
|
||||
'bind': bind
|
||||
};
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", path, null, data
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a request to change your password.
|
||||
* @param {Object} authDict
|
||||
* @param {string} newPassword The new desired password.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback) {
|
||||
var path = "/account/password";
|
||||
var data = {
|
||||
'auth': authDict,
|
||||
'new_password': newPassword
|
||||
};
|
||||
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", path, null, data
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Device operations
|
||||
// =================
|
||||
|
||||
/**
|
||||
* Gets all devices recorded for the logged-in user
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getDevices = function() {
|
||||
var path = "/devices";
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "GET", path, undefined, undefined,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the given device
|
||||
*
|
||||
* @param {string} device_id device to update
|
||||
* @param {Object} body body of request
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
|
||||
var path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: device_id,
|
||||
});
|
||||
|
||||
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "PUT", path, undefined, body,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the given device
|
||||
*
|
||||
* @param {string} device_id device to delete
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteDevice = function(device_id) {
|
||||
var path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: device_id,
|
||||
});
|
||||
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "DELETE", path, undefined, undefined,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Push operations
|
||||
// ===============
|
||||
|
||||
/**
|
||||
* Gets all pushers registered for the logged-in user
|
||||
*
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Array of objects representing pushers
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getPushers = function(callback) {
|
||||
var path = "/pushers";
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path, undefined, undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a new pusher or updates an existing pusher
|
||||
*
|
||||
* @param {Object} pusher Object representing a pusher
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: Empty json object on success
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
|
||||
var path = "/pushers/set";
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", path, null, pusher
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getPushRules = function(callback) {
|
||||
return this._http.authedRequest(callback, "GET", "/pushrules/");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {Object} body
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callback) {
|
||||
// NB. Scope not uri encoded because devices need the '/'
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, body
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback) {
|
||||
// NB. Scope not uri encoded because devices need the '/'
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(callback, "DELETE", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable or disable a push notification rule.
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {boolean} enabled
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
|
||||
ruleId, enabled, callback) {
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, {"enabled": enabled}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the actions for a push notification rule.
|
||||
* @param {string} scope
|
||||
* @param {string} kind
|
||||
* @param {string} ruleId
|
||||
* @param {array} actions
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
|
||||
ruleId, actions, callback) {
|
||||
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
|
||||
$kind: kind,
|
||||
$ruleId: ruleId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, {"actions": actions}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Search
|
||||
// ======
|
||||
|
||||
/**
|
||||
* Perform a server-side search.
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.next_batch the batch token to pass in the query string
|
||||
* @param {Object} opts.body the JSON object to pass to the request body.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.search = function(opts, callback) {
|
||||
var queryparams = {};
|
||||
if (opts.next_batch) {
|
||||
queryparams.next_batch = opts.next_batch;
|
||||
}
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/search", queryparams, opts.body
|
||||
);
|
||||
};
|
||||
|
||||
// Crypto
|
||||
// ======
|
||||
|
||||
/**
|
||||
* Upload keys
|
||||
*
|
||||
* @param {Object} content body of upload request
|
||||
*
|
||||
* @param {Object=} opts
|
||||
*
|
||||
* @param {string=} opts.device_id explicit device_id to use for upload
|
||||
* (default is to use the same as that used during auth).
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
|
||||
opts = opts || {};
|
||||
var deviceId = opts.device_id;
|
||||
var path;
|
||||
if (deviceId) {
|
||||
path = utils.encodeUri("/keys/upload/$deviceId", {
|
||||
$deviceId: deviceId,
|
||||
});
|
||||
} else {
|
||||
path = "/keys/upload";
|
||||
}
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Download device keys
|
||||
*
|
||||
* @param {string[]} userIds list of users to get keys for
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
|
||||
var downloadQuery = {};
|
||||
|
||||
for (var i = 0; i < userIds.length; ++i) {
|
||||
downloadQuery[userIds[i]] = {};
|
||||
}
|
||||
var content = {device_keys: downloadQuery};
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", "/keys/query", undefined, content,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Claim one-time keys
|
||||
*
|
||||
* @param {string[][]} devices a list of [userId, deviceId] pairs
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, callback) {
|
||||
var queries = {};
|
||||
|
||||
for (var i = 0; i < devices.length; ++i) {
|
||||
var userId = devices[i][0];
|
||||
var deviceId = devices[i][1];
|
||||
var query = queries[userId] || {};
|
||||
queries[userId] = query;
|
||||
query[deviceId] = "curve25519";
|
||||
}
|
||||
var content = {one_time_keys: queries};
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", "/keys/claim", undefined, content,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Identity Server Operations
|
||||
// ==========================
|
||||
|
||||
/**
|
||||
* Requests an email verification token directly from an Identity Server.
|
||||
*
|
||||
* Note that the Home Server offers APIs to proxy this API for specific
|
||||
* situations, allowing for better feedback to the user.
|
||||
*
|
||||
* @param {string} email The email address to request a token for
|
||||
* @param {string} clientSecret A secret binary string generated by the client.
|
||||
* It is recommended this be around 16 ASCII characters.
|
||||
* @param {number} sendAttempt If an identity server sees a duplicate request
|
||||
* with the same sendAttempt, it will not send another email.
|
||||
* To request another email to be sent, use a larger value for
|
||||
* the sendAttempt param as was used in the previous request.
|
||||
* @param {string} nextLink Optional If specified, the client will be redirected
|
||||
* to this link after validation.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @throws Error if No ID server is set
|
||||
*/
|
||||
MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
|
||||
sendAttempt, nextLink, callback) {
|
||||
var params = {
|
||||
client_secret: clientSecret,
|
||||
email: email,
|
||||
send_attempt: sendAttempt,
|
||||
next_link: nextLink
|
||||
};
|
||||
return this._http.idServerRequest(
|
||||
callback, "POST", "/validate/email/requestToken",
|
||||
params, httpApi.PREFIX_IDENTITY_V1
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up the public Matrix ID mapping for a given 3rd party
|
||||
* identifier from the Identity Server
|
||||
* @param {string} medium The medium of the threepid, eg. 'email'
|
||||
* @param {string} address The textual address of the threepid
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: A threepid mapping
|
||||
* object or the empty object if no mapping
|
||||
* exists
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
|
||||
var params = {
|
||||
medium: medium,
|
||||
address: address,
|
||||
};
|
||||
return this._http.idServerRequest(
|
||||
callback, "GET", "/lookup",
|
||||
params, httpApi.PREFIX_IDENTITY_V1
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* MatrixBaseApis object
|
||||
*/
|
||||
module.exports = MatrixBaseApis;
|
||||
-3033
File diff suppressed because it is too large
Load Diff
-806
@@ -1,806 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
/**
|
||||
* Internal module
|
||||
*
|
||||
* @module crypto
|
||||
*/
|
||||
|
||||
var anotherjson = require('another-json');
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("./utils");
|
||||
var OlmDevice = require("./OlmDevice");
|
||||
|
||||
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
|
||||
var DeviceVerification = {
|
||||
VERIFIED: 1,
|
||||
UNVERIFIED: 0,
|
||||
BLOCKED: -1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Stored information about a user's device
|
||||
*
|
||||
* @typedef {Object} DeviceInfo
|
||||
*
|
||||
* @property {string[]} altorithms list of algorithms supported by this device
|
||||
*
|
||||
* @property {Object} keys a map from <key type>:<id> -> key
|
||||
*
|
||||
* @property {DeviceVerification} verified whether the device has been
|
||||
* verified by the user
|
||||
*
|
||||
* @property {Object} unsigned additional data from the homeserver
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
*
|
||||
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
|
||||
* Store to be used for end-to-end crypto session data
|
||||
*
|
||||
* @param {string} userId The user ID for the local user
|
||||
*
|
||||
* @param {string} deviceId The identifier for this device.
|
||||
*/
|
||||
function Crypto(baseApis, sessionStore, userId, deviceId) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._userId = userId;
|
||||
this._deviceId = deviceId;
|
||||
|
||||
this._cryptoAlgorithms = [];
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore);
|
||||
this._cryptoAlgorithms = [OLM_ALGORITHM];
|
||||
|
||||
// build our device keys: these will later be uploaded
|
||||
this._deviceKeys = {};
|
||||
this._deviceKeys["ed25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceEd25519Key;
|
||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceCurve25519Key;
|
||||
|
||||
// add our own deviceinfo to the sessionstore
|
||||
var deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._cryptoAlgorithms,
|
||||
verified: DeviceVerification.VERIFIED,
|
||||
};
|
||||
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||
this._userId
|
||||
) || {};
|
||||
myDevices[this._deviceId] = deviceInfo;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(
|
||||
this._userId, myDevices
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Ed25519 key for this device
|
||||
*
|
||||
* @return {string} base64-encoded ed25519 key.
|
||||
*/
|
||||
Crypto.prototype.getDeviceEd25519Key = function() {
|
||||
return this._olmDevice.deviceEd25519Key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload the device keys to the homeserver and ensure that the
|
||||
* homeserver has enough one-time keys.
|
||||
* @param {number} maxKeys The maximum number of keys to generate
|
||||
* @return {object} A promise that will resolve when the keys are uploaded.
|
||||
*/
|
||||
Crypto.prototype.uploadKeys = function(maxKeys) {
|
||||
var self = this;
|
||||
return _uploadDeviceKeys(this).then(function(res) {
|
||||
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
||||
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
||||
var keyLimit = Math.floor(maxOneTimeKeys / 2);
|
||||
var numberToGenerate = Math.max(keyLimit - keyCount, 0);
|
||||
if (maxKeys !== undefined) {
|
||||
numberToGenerate = Math.min(numberToGenerate, maxKeys);
|
||||
}
|
||||
|
||||
if (numberToGenerate <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._olmDevice.generateOneTimeKeys(numberToGenerate);
|
||||
return _uploadOneTimeKeys(self);
|
||||
});
|
||||
};
|
||||
|
||||
// returns a promise which resolves to the response
|
||||
function _uploadDeviceKeys(crypto) {
|
||||
var userId = crypto._userId;
|
||||
var deviceId = crypto._deviceId;
|
||||
|
||||
var deviceKeys = {
|
||||
algorithms: crypto._cryptoAlgorithms,
|
||||
device_id: deviceId,
|
||||
keys: crypto._deviceKeys,
|
||||
user_id: userId,
|
||||
};
|
||||
|
||||
var sig = crypto._olmDevice.sign(anotherjson.stringify(deviceKeys));
|
||||
deviceKeys.signatures = {};
|
||||
deviceKeys.signatures[userId] = {};
|
||||
deviceKeys.signatures[userId]["ed25519:" + deviceId] = sig;
|
||||
|
||||
return crypto._baseApis.uploadKeysRequest({
|
||||
device_keys: deviceKeys,
|
||||
}, {
|
||||
// for now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
device_id: deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
// returns a promise which resolves to the response
|
||||
function _uploadOneTimeKeys(crypto) {
|
||||
var oneTimeKeys = crypto._olmDevice.getOneTimeKeys();
|
||||
var oneTimeJson = {};
|
||||
|
||||
for (var keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
|
||||
}
|
||||
}
|
||||
return crypto._baseApis.uploadKeysRequest({
|
||||
one_time_keys: oneTimeJson
|
||||
}, {
|
||||
// for now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
device_id: crypto._deviceId,
|
||||
}).then(function(res) {
|
||||
crypto._olmDevice.markKeysAsPublished();
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the keys for a list of users and stores the keys in the session
|
||||
* store.
|
||||
* @param {Array} userIds The users to fetch.
|
||||
* @param {bool} forceDownload Always download the keys even if cached.
|
||||
*
|
||||
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
||||
* module:crypto~DeviceInfo|DeviceInfo}.
|
||||
*/
|
||||
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
var self = this;
|
||||
var stored = {};
|
||||
var downloadUsers = [];
|
||||
|
||||
for (var i = 0; i < userIds.length; ++i) {
|
||||
var userId = userIds[i];
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
|
||||
stored[userId] = devices || {};
|
||||
if (devices && !forceDownload) {
|
||||
continue;
|
||||
}
|
||||
downloadUsers.push(userId);
|
||||
}
|
||||
|
||||
if (downloadUsers.length === 0) {
|
||||
return q(stored);
|
||||
}
|
||||
|
||||
return this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers
|
||||
).then(function(res) {
|
||||
for (var userId in res.device_keys) {
|
||||
if (!stored.hasOwnProperty(userId)) {
|
||||
// spurious result
|
||||
continue;
|
||||
}
|
||||
|
||||
var userStore = stored[userId];
|
||||
var updated = _updateStoredDeviceKeysForUser(
|
||||
self._olmDevice, userId, userStore, res.device_keys[userId]
|
||||
);
|
||||
|
||||
if (updated) {
|
||||
self._sessionStore.storeEndToEndDevicesForUser(
|
||||
userId, userStore
|
||||
);
|
||||
}
|
||||
}
|
||||
return stored;
|
||||
});
|
||||
};
|
||||
|
||||
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
userResult) {
|
||||
var updated = false;
|
||||
|
||||
// remove any devices in the store which aren't in the response
|
||||
for (var deviceId in userStore) {
|
||||
if (!userStore.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(deviceId in userResult)) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
" has been removed");
|
||||
delete userStore[deviceId];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (deviceId in userResult) {
|
||||
if (!userResult.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_storeDeviceKeys(
|
||||
_olmDevice, userId, deviceId, userStore, userResult[deviceId]
|
||||
)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/*
|
||||
* Process a device in a /query response, and add it to the userStore
|
||||
*
|
||||
* returns true if a change was made, else false
|
||||
*/
|
||||
function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult) {
|
||||
if (!deviceResult.keys) {
|
||||
// no keys?
|
||||
return false;
|
||||
}
|
||||
|
||||
var signKeyId = "ed25519:" + deviceId;
|
||||
var signKey = deviceResult.keys[signKeyId];
|
||||
if (!signKey) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
" has no ed25519 key");
|
||||
return false;
|
||||
}
|
||||
|
||||
var unsigned = deviceResult.unsigned;
|
||||
var signatures = deviceResult.signatures || {};
|
||||
var userSigs = signatures[userId] || {};
|
||||
var signature = userSigs[signKeyId];
|
||||
if (!signature) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
" is not signed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare the canonical json: remove 'unsigned' and sigxsnatures, and
|
||||
// stringify with anotherjson
|
||||
delete deviceResult.unsigned;
|
||||
delete deviceResult.signatures;
|
||||
var json = anotherjson.stringify(deviceResult);
|
||||
|
||||
console.log("msg:", json);
|
||||
try {
|
||||
_olmDevice.verifySignature(signKey, json, signature);
|
||||
} catch (e) {
|
||||
console.log("Unable to verify signature on device " +
|
||||
userId + ":" + deviceId + ":", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
var deviceStore;
|
||||
if (deviceId in userStore) {
|
||||
// already have this device.
|
||||
deviceStore = userStore[deviceId];
|
||||
|
||||
if (deviceStore.keys["ed25519:" + deviceId] != signKey) {
|
||||
// this should only happen if the list has been MITMed; we are
|
||||
// best off sticking with the original keys.
|
||||
//
|
||||
// Should we warn the user about it somehow?
|
||||
console.warn("Ed25519 key for device" + userId + ": " +
|
||||
deviceId + " has changed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
userStore[deviceId] = deviceStore = {
|
||||
verified: DeviceVerification.UNVERIFIED
|
||||
};
|
||||
}
|
||||
|
||||
deviceStore.keys = deviceResult.keys;
|
||||
deviceStore.algorithms = deviceResult.algorithms;
|
||||
deviceStore.unsigned = unsigned;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* List the stored device keys for a user id
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
*
|
||||
* @return {object[]} list of devices with "id", "verified", "blocked",
|
||||
* "key", and "display_name" parameters.
|
||||
*/
|
||||
Crypto.prototype.listDeviceKeys = function(userId) {
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
var result = [];
|
||||
if (devices) {
|
||||
var deviceId;
|
||||
var deviceIds = [];
|
||||
for (deviceId in devices) {
|
||||
if (devices.hasOwnProperty(deviceId)) {
|
||||
deviceIds.push(deviceId);
|
||||
}
|
||||
}
|
||||
deviceIds.sort();
|
||||
for (var i = 0; i < deviceIds.length; ++i) {
|
||||
deviceId = deviceIds[i];
|
||||
var device = devices[deviceId];
|
||||
var ed25519Key = device.keys["ed25519:" + deviceId];
|
||||
var unsigned = device.unsigned || {};
|
||||
if (ed25519Key) {
|
||||
result.push({
|
||||
id: deviceId,
|
||||
key: ed25519Key,
|
||||
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
|
||||
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
|
||||
display_name: unsigned.device_display_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a device by curve25519 identity key
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} algorithm encryption algorithm
|
||||
* @param {string} sender_key curve25519 key to match
|
||||
*
|
||||
* @return {module:crypto~DeviceInfo?}
|
||||
*/
|
||||
Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key) {
|
||||
if (algorithm !== OLM_ALGORITHM) {
|
||||
// we only deal in olm keys
|
||||
return null;
|
||||
}
|
||||
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devices) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var deviceId in devices) {
|
||||
if (!devices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var device = devices[deviceId];
|
||||
for (var keyId in device.keys) {
|
||||
if (!device.keys.hasOwnProperty(keyId)) {
|
||||
continue;
|
||||
}
|
||||
if (keyId.indexOf("curve25519:") !== 0) {
|
||||
continue;
|
||||
}
|
||||
var deviceKey = device.keys[keyId];
|
||||
if (deviceKey == sender_key) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doesn't match a known device
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Update the blocked/verified state of the given device
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} deviceId unique identifier for the device
|
||||
*
|
||||
* @param {?boolean} verified whether to mark the device as verified. Null to
|
||||
* leave unchanged.
|
||||
*
|
||||
* @param {?boolean} blocked whether to mark the device as blocked. Null to
|
||||
* leave unchanged.
|
||||
*/
|
||||
Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, blocked) {
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devices || !devices[deviceId]) {
|
||||
throw new Error("Unknown device " + userId + ":" + deviceId);
|
||||
}
|
||||
|
||||
var dev = devices[deviceId];
|
||||
var verificationStatus = dev.verified;
|
||||
|
||||
if (verified) {
|
||||
verificationStatus = DeviceVerification.VERIFIED;
|
||||
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||
}
|
||||
|
||||
if (blocked) {
|
||||
verificationStatus = DeviceVerification.BLOCKED;
|
||||
} else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) {
|
||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||
}
|
||||
|
||||
if (dev.verified === verificationStatus) {
|
||||
return;
|
||||
}
|
||||
dev.verified = verificationStatus;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(userId, devices);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Identify a device by curve25519 identity key and determine its verification state
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} algorithm encryption algorithm
|
||||
* @param {string} sender_key curve25519 key to match
|
||||
*
|
||||
* @return {boolean} true if the device is verified
|
||||
*/
|
||||
Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
|
||||
var device = this.getDeviceByIdentityKey(userId, algorithm, sender_key);
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
return device.verified == DeviceVerification.VERIFIED;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Configure a room to use encryption (ie, save a flag in the sessionstore).
|
||||
*
|
||||
* @param {string} roomId The room ID to enable encryption in.
|
||||
* @param {object} config The encryption config for the room.
|
||||
* @param {string[]} roomMembers userIds of room members to start sessions with
|
||||
*
|
||||
* @return {Object} A promise that will resolve when encryption is setup.
|
||||
*/
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
|
||||
var self = this;
|
||||
|
||||
// if we already have encryption in this room, we should ignore this event
|
||||
// (for now at least. maybe we should alert the user somehow?)
|
||||
var existingConfig = this._sessionStore.getEndToEndRoom(roomId);
|
||||
if (existingConfig) {
|
||||
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
|
||||
console.error("Ignoring m.room.encryption event which requests " +
|
||||
"a change of config in " + roomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.algorithm !== OLM_ALGORITHM) {
|
||||
throw new Error("Unknown algorithm: " + config.algorithm);
|
||||
}
|
||||
|
||||
// remove spurious keys
|
||||
config = {
|
||||
algorithm: OLM_ALGORITHM,
|
||||
};
|
||||
this._sessionStore.storeEndToEndRoom(roomId, config);
|
||||
|
||||
return self.downloadKeys(roomMembers, true).then(function(res) {
|
||||
return self._ensureOlmSessionsForUsers(roomMembers);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given users.
|
||||
*
|
||||
* @param {string[]} users list of user ids
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* an object with keys <tt>missingUsers</tt> (a list of users with no known
|
||||
* olm devices), and <tt>missingDevices</tt> a list of olm devices with no
|
||||
* known one-time keys.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
|
||||
var devicesWithoutSession = [];
|
||||
var userWithoutDevices = [];
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devices) {
|
||||
userWithoutDevices.push(userId);
|
||||
} else {
|
||||
for (var deviceId in devices) {
|
||||
if (devices.hasOwnProperty(deviceId)) {
|
||||
var keys = devices[deviceId];
|
||||
var key = keys.keys["curve25519:" + deviceId];
|
||||
if (key == this._olmDevice.deviceCurve25519Key) {
|
||||
continue;
|
||||
}
|
||||
if (!this._sessionStore.getEndToEndSessions(key)) {
|
||||
devicesWithoutSession.push([userId, deviceId, key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesWithoutSession.length === 0) {
|
||||
return q({
|
||||
missingUsers: userWithoutDevices,
|
||||
missingDevices: []
|
||||
});
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return this._baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession
|
||||
).then(function(res) {
|
||||
var missing = {};
|
||||
for (i = 0; i < devicesWithoutSession.length; ++i) {
|
||||
var device = devicesWithoutSession[i];
|
||||
var userRes = res.one_time_keys[device[0]] || {};
|
||||
var deviceRes = userRes[device[1]];
|
||||
var oneTimeKey;
|
||||
for (var keyId in deviceRes) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
}
|
||||
}
|
||||
if (oneTimeKey) {
|
||||
var sid = self._olmDevice.createOutboundSession(
|
||||
device[2], oneTimeKey
|
||||
);
|
||||
console.log("Started new sessionid " + sid +
|
||||
" for device " + device[2]);
|
||||
} else {
|
||||
missing[device[0]] = missing[device[0]] || [];
|
||||
missing[device[0]].push([device[1]]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
missingUsers: userWithoutDevices,
|
||||
missingDevices: missing
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether encryption is enabled for a room.
|
||||
* @param {string} roomId the room id to query.
|
||||
* @return {bool} whether encryption is enabled.
|
||||
*/
|
||||
Crypto.prototype.isRoomEncrypted = function(roomId) {
|
||||
return (this._sessionStore.getEndToEndRoom(roomId) && true) || false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room, if necessary.
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event to be sent
|
||||
* @param {module:models/room.Room} room destination room
|
||||
*/
|
||||
Crypto.prototype.encryptEventIfNeeded = function(event, room) {
|
||||
if (event.isEncrypted()) {
|
||||
// this event has already been encrypted; this happens if the
|
||||
// encryption step succeeded, but the send step failed on the first
|
||||
// attempt.
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getType() !== "m.room.message") {
|
||||
// we only encrypt m.room.message
|
||||
return;
|
||||
}
|
||||
|
||||
var roomId = event.getRoomId();
|
||||
|
||||
var e2eRoomInfo = this._sessionStore.getEndToEndRoom(roomId);
|
||||
if (!e2eRoomInfo || !e2eRoomInfo.algorithm) {
|
||||
// not encrypting messages in this room
|
||||
return;
|
||||
}
|
||||
|
||||
var encryptedContent = this._encryptMessage(
|
||||
room, e2eRoomInfo, event.getType(), event.getContent()
|
||||
);
|
||||
event.makeEncrypted("m.room.encrypted", encryptedContent);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {module:models/room.Room} room
|
||||
* @param {object} e2eRoomInfo
|
||||
* @param {string} eventType
|
||||
* @param {object} content
|
||||
*
|
||||
* @return {object} new event body
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Crypto.prototype._encryptMessage = function(room, e2eRoomInfo, eventType, content) {
|
||||
if (e2eRoomInfo.algorithm !== OLM_ALGORITHM) {
|
||||
throw new Error("Unknown end-to-end algorithm: " + e2eRoomInfo.algorithm);
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
}
|
||||
|
||||
// pick the list of recipients based on the membership list.
|
||||
//
|
||||
// TODO: there is a race condition here! What if a new user turns up
|
||||
// just as you are sending a secret message?
|
||||
|
||||
var users = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
var participantKeys = [];
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
for (var deviceId in devices) {
|
||||
if (devices.hasOwnProperty(deviceId)) {
|
||||
var dev = devices[deviceId];
|
||||
if (dev.verified === DeviceVerification.BLOCKED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var keyId in dev.keys) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
participantKeys.push(dev.keys[keyId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
participantKeys.sort();
|
||||
var participantHash = ""; // Olm.sha256(participantKeys.join());
|
||||
var payloadJson = {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
fingerprint: participantHash,
|
||||
sender_device: this._deviceId,
|
||||
content: content
|
||||
};
|
||||
var ciphertext = {};
|
||||
var payloadString = JSON.stringify(payloadJson);
|
||||
for (i = 0; i < participantKeys.length; ++i) {
|
||||
var deviceKey = participantKeys[i];
|
||||
if (deviceKey == this._olmDevice.deviceCurve25519Key) {
|
||||
continue;
|
||||
}
|
||||
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
|
||||
// Use the session with the lowest ID.
|
||||
sessionIds.sort();
|
||||
if (sessionIds.length === 0) {
|
||||
// If we don't have a session for a device then
|
||||
// we can't encrypt a message for it.
|
||||
continue;
|
||||
}
|
||||
var sessionId = sessionIds[0];
|
||||
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
|
||||
ciphertext[deviceKey] = this._olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, payloadString
|
||||
);
|
||||
}
|
||||
var encryptedContent = {
|
||||
algorithm: e2eRoomInfo.algorithm,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext
|
||||
};
|
||||
return encryptedContent;
|
||||
};
|
||||
|
||||
function DecryptionError(msg) {
|
||||
this.message = msg;
|
||||
}
|
||||
utils.inherits(DecryptionError, Error);
|
||||
|
||||
/**
|
||||
* Exception thrown when decryption fails
|
||||
*/
|
||||
Crypto.DecryptionError = DecryptionError;
|
||||
|
||||
/**
|
||||
* Decrypt a received event
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {object} decrypted payload (with properties 'type', 'content')
|
||||
*
|
||||
* @raises {DecryptionError} if there is a problem decrypting the event
|
||||
*/
|
||||
Crypto.prototype.decryptEvent = function(event) {
|
||||
var content = event.content;
|
||||
if (content.algorithm !== OLM_ALGORITHM) {
|
||||
throw new DecryptionError("Unknown algorithm");
|
||||
}
|
||||
|
||||
var deviceKey = content.sender_key;
|
||||
var ciphertext = content.ciphertext;
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new DecryptionError("Missing ciphertext");
|
||||
}
|
||||
|
||||
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
|
||||
throw new DecryptionError("Not included in recipients");
|
||||
}
|
||||
|
||||
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
|
||||
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
|
||||
var payloadString = null;
|
||||
var foundSession = false;
|
||||
for (var i = 0; i < sessionIds.length; i++) {
|
||||
var sessionId = sessionIds[i];
|
||||
var res = this._olmDevice.decryptMessage(
|
||||
deviceKey, sessionId, message.type, message.body
|
||||
);
|
||||
payloadString = res.payload;
|
||||
if (payloadString) {
|
||||
console.log("decrypted with sessionId " + sessionId);
|
||||
break;
|
||||
}
|
||||
|
||||
if (res.matchesInbound) {
|
||||
// this was a prekey message which matched this session; don't
|
||||
// create a new session.
|
||||
foundSession = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === 0 && !foundSession && payloadString === null) {
|
||||
try {
|
||||
payloadString = this._olmDevice.createInboundSession(
|
||||
deviceKey, message.type, message.body
|
||||
);
|
||||
console.log("created new inbound sesion");
|
||||
} catch (e) {
|
||||
// Failed to decrypt with a new session.
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check the sender user id matches the sender key.
|
||||
if (payloadString !== null) {
|
||||
return JSON.parse(payloadString);
|
||||
} else {
|
||||
throw new DecryptionError("Bad Encrypted Message");
|
||||
}
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = Crypto;
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
/**
|
||||
* @module filter
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {string} keyNesting
|
||||
* @param {*} val
|
||||
*/
|
||||
function setProp(obj, keyNesting, val) {
|
||||
var nestedKeys = keyNesting.split(".");
|
||||
var currentObj = obj;
|
||||
for (var i = 0; i < (nestedKeys.length - 1); i++) {
|
||||
if (!currentObj[nestedKeys[i]]) {
|
||||
currentObj[nestedKeys[i]] = {};
|
||||
}
|
||||
currentObj = currentObj[nestedKeys[i]];
|
||||
}
|
||||
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new Filter.
|
||||
* @constructor
|
||||
* @param {string} userId The user ID for this filter.
|
||||
* @param {string=} filterId The filter ID if known.
|
||||
* @prop {string} userId The user ID of the filter
|
||||
* @prop {?string} filterId The filter ID
|
||||
*/
|
||||
function Filter(userId, filterId) {
|
||||
this.userId = userId;
|
||||
this.filterId = filterId;
|
||||
this.definition = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON body of the filter.
|
||||
* @return {Object} The filter definition
|
||||
*/
|
||||
Filter.prototype.getDefinition = function() {
|
||||
return this.definition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the JSON body of the filter
|
||||
* @param {Object} definition The filter definition
|
||||
*/
|
||||
Filter.prototype.setDefinition = function(definition) {
|
||||
this.definition = definition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the max number of events to return for each room's timeline.
|
||||
* @param {Number} limit The max number of events to return for each room.
|
||||
*/
|
||||
Filter.prototype.setTimelineLimit = function(limit) {
|
||||
setProp(this.definition, "room.timeline.limit", limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Control whether left rooms should be included in responses.
|
||||
* @param {boolean} includeLeave True to make rooms the user has left appear
|
||||
* in responses.
|
||||
*/
|
||||
Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
|
||||
setProp(this.definition, "room.include_leave", includeLeave);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a filter from existing data.
|
||||
* @static
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @param {Object} jsonObj
|
||||
* @return {Filter}
|
||||
*/
|
||||
Filter.fromJson = function(userId, filterId, jsonObj) {
|
||||
var filter = new Filter(userId, filterId);
|
||||
filter.setDefinition(jsonObj);
|
||||
return filter;
|
||||
};
|
||||
|
||||
/** The Filter class */
|
||||
module.exports = Filter;
|
||||
-573
@@ -1,573 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixHttpApi} for the public class.
|
||||
* @module http-api
|
||||
*/
|
||||
var q = require("q");
|
||||
var utils = require("./utils");
|
||||
|
||||
// we use our own implementation of setTimeout, so that if we get suspended in
|
||||
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
|
||||
// waiting for the delay to elapse.
|
||||
var callbacks = require("./realtime-callbacks");
|
||||
|
||||
/*
|
||||
TODO:
|
||||
- CS: complete register function (doing stages)
|
||||
- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
|
||||
*/
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
|
||||
*/
|
||||
module.exports.PREFIX_R0 = "/_matrix/client/r0";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
|
||||
*/
|
||||
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
|
||||
|
||||
/**
|
||||
* URI path for the identity API
|
||||
*/
|
||||
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
|
||||
|
||||
/**
|
||||
* URI path for the media repo API
|
||||
*/
|
||||
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
|
||||
/**
|
||||
* Construct a MatrixHttpApi.
|
||||
* @constructor
|
||||
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
|
||||
* @param {Object} opts The options to use for this HTTP API.
|
||||
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
|
||||
* 'http://localhost:8008'.
|
||||
* @param {Function} opts.request Required. The function to call for HTTP
|
||||
* requests. This function must look like function(opts, callback){ ... }.
|
||||
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
|
||||
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
|
||||
* @param {bool} opts.onlyData True to return only the 'data' component of the
|
||||
* response (e.g. the parsed HTTP body). If false, requests will return status
|
||||
* codes and headers in addition to data. Default: false.
|
||||
* @param {string} opts.accessToken The access_token to send with requests. Can be
|
||||
* null to not send an access token.
|
||||
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
|
||||
* requests.
|
||||
*/
|
||||
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = opts.onlyData || false;
|
||||
this.event_emitter = event_emitter;
|
||||
this.opts = opts;
|
||||
this.uploads = [];
|
||||
};
|
||||
|
||||
module.exports.MatrixHttpApi.prototype = {
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
|
||||
* path and query parameters respectively.
|
||||
*/
|
||||
getContentUri: function() {
|
||||
var params = {
|
||||
access_token: this.opts.accessToken
|
||||
};
|
||||
return {
|
||||
base: this.opts.baseUrl,
|
||||
path: "/_matrix/media/v1/upload",
|
||||
params: params
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload content to the Home Server
|
||||
* @param {File} file A File object (in a browser) or in Node,
|
||||
an object with properties:
|
||||
name: The file's name
|
||||
stream: A read stream
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
*/
|
||||
uploadContent: function(file, callback) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
}
|
||||
var defer = q.defer();
|
||||
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||||
// browser-request doesn't support File objects because it deep-copies
|
||||
// the options using JSON.parse(JSON.stringify(options)). Instead of
|
||||
// loading the whole file into memory as a string and letting
|
||||
// browser-request base64 encode and then decode it again, we just
|
||||
// use XMLHttpRequest directly.
|
||||
// (browser-request doesn't support progress either, which is also kind
|
||||
// of important here)
|
||||
|
||||
var upload = { loaded: 0, total: 0 };
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
var xhr = new global.XMLHttpRequest();
|
||||
upload.xhr = xhr;
|
||||
var cb = requestCallback(defer, callback, this.opts.onlyData);
|
||||
|
||||
var timeout_fn = function() {
|
||||
xhr.abort();
|
||||
cb(new Error('Timeout'));
|
||||
};
|
||||
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
var err;
|
||||
if (!xhr.responseText) {
|
||||
err = new Error('No response body.');
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.content_uri === undefined) {
|
||||
err = Error('Bad response');
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(undefined, xhr, resp.content_uri);
|
||||
break;
|
||||
}
|
||||
};
|
||||
xhr.upload.addEventListener("progress", function(ev) {
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
defer.notify(ev);
|
||||
});
|
||||
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
|
||||
url += "&filename=" + encodeURIComponent(file.name);
|
||||
|
||||
xhr.open("POST", url);
|
||||
if (file.type) {
|
||||
xhr.setRequestHeader("Content-Type", file.type);
|
||||
} else {
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
|
||||
}
|
||||
xhr.send(file);
|
||||
} else {
|
||||
var queryParams = {
|
||||
filename: file.name,
|
||||
access_token: this.opts.accessToken
|
||||
};
|
||||
upload.request = this.opts.request({
|
||||
uri: url,
|
||||
qs: queryParams,
|
||||
method: "POST",
|
||||
headers: {"Content-Type": file.type},
|
||||
body: file.stream
|
||||
}, requestCallback(defer, callback, this.opts.onlyData));
|
||||
}
|
||||
|
||||
this.uploads.push(upload);
|
||||
|
||||
var self = this;
|
||||
upload.promise = defer.promise.finally(function() {
|
||||
var uploadsKeys = Object.keys(self.uploads);
|
||||
for (var i = 0; i < uploadsKeys.length; ++i) {
|
||||
if (self.uploads[uploadsKeys[i]].promise === defer.promise) {
|
||||
self.uploads.splice(uploadsKeys[i], 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
return upload.promise;
|
||||
},
|
||||
|
||||
cancelUpload: function(promise) {
|
||||
var uploadsKeys = Object.keys(this.uploads);
|
||||
for (var i = 0; i < uploadsKeys.length; ++i) {
|
||||
var upload = this.uploads[uploadsKeys[i]];
|
||||
if (upload.promise === promise) {
|
||||
if (upload.xhr !== undefined) {
|
||||
upload.xhr.abort();
|
||||
return true;
|
||||
} else if (upload.request !== undefined) {
|
||||
upload.request.abort();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getCurrentUploads: function() {
|
||||
return this.uploads;
|
||||
},
|
||||
|
||||
idServerRequest: function(callback, method, path, params, prefix) {
|
||||
var fullUri = this.opts.idBaseUrl + prefix + path;
|
||||
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
}
|
||||
|
||||
var opts = {
|
||||
uri: fullUri,
|
||||
method: method,
|
||||
withCredentials: false,
|
||||
json: false,
|
||||
_matrix_opts: this.opts
|
||||
};
|
||||
if (method == 'GET') {
|
||||
opts.qs = params;
|
||||
} else {
|
||||
opts.form = params;
|
||||
}
|
||||
|
||||
var defer = q.defer();
|
||||
this.opts.request(
|
||||
opts,
|
||||
requestCallback(defer, callback, this.opts.onlyData)
|
||||
);
|
||||
// ID server does not always take JSON, so we can't use requests' 'json'
|
||||
// option as we do with the home server, but it does return JSON, so
|
||||
// parse it manually
|
||||
return defer.promise.then(function(response) {
|
||||
return JSON.parse(response);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an authorised request to the homeserver.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) {
|
||||
if (!queryParams) { queryParams = {}; }
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
var self = this;
|
||||
var request_promise = this.request(
|
||||
callback, method, path, queryParams, data, localTimeoutMs
|
||||
);
|
||||
request_promise.catch(function(err) {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN') {
|
||||
self.event_emitter.emit("Session.logged_out");
|
||||
}
|
||||
});
|
||||
// return the original promise, otherwise tests break due to it having to
|
||||
// go around the event loop one more time to process the result of the request
|
||||
return request_promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a request to the homeserver without any credentials.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
request: function(callback, method, path, queryParams, data, localTimeoutMs) {
|
||||
return this.requestWithPrefix(
|
||||
callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an authorised request to the homeserver with a specific path
|
||||
* prefix which overrides the default for this call only. Useful for hitting
|
||||
* different Matrix Client-Server versions.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {string} prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
|
||||
prefix, localTimeoutMs) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
return this._request(
|
||||
callback, method, fullUri, queryParams, data, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a request to the homeserver without any credentials but with a
|
||||
* specific path prefix which overrides the default for this call only.
|
||||
* Useful for hitting different Matrix Client-Server versions.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {string} prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
|
||||
localTimeoutMs) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
return this._request(
|
||||
callback, method, fullUri, queryParams, data, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a request to an arbitrary URL.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} uri The HTTP URI
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
requestOtherUrl: function(callback, method, uri, queryParams, data,
|
||||
localTimeoutMs) {
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
return this._request(
|
||||
callback, method, uri, queryParams, data, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Form and return a homeserver request URL based on the given path
|
||||
* params and prefix.
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {string} prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @return {string} URL
|
||||
*/
|
||||
getUrl: function(path, queryParams, prefix) {
|
||||
var queryString = "";
|
||||
if (queryParams) {
|
||||
queryString = "?" + utils.encodeParams(queryParams);
|
||||
}
|
||||
return this.opts.baseUrl + prefix + path + queryString;
|
||||
},
|
||||
|
||||
_request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
}
|
||||
var self = this;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
if (this.opts.extraParams) {
|
||||
for (var key in this.opts.extraParams) {
|
||||
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
|
||||
queryParams[key] = this.opts.extraParams[key];
|
||||
}
|
||||
}
|
||||
var defer = q.defer();
|
||||
|
||||
var timeoutId;
|
||||
var timedOut = false;
|
||||
var req;
|
||||
if (localTimeoutMs) {
|
||||
timeoutId = callbacks.setTimeout(function() {
|
||||
timedOut = true;
|
||||
if (req && req.abort) {
|
||||
req.abort();
|
||||
}
|
||||
defer.reject(new module.exports.MatrixError({
|
||||
error: "Locally timed out waiting for a response",
|
||||
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
|
||||
timeout: localTimeoutMs
|
||||
}));
|
||||
}, localTimeoutMs);
|
||||
}
|
||||
|
||||
var reqPromise = defer.promise;
|
||||
|
||||
try {
|
||||
req = this.opts.request(
|
||||
{
|
||||
uri: uri,
|
||||
method: method,
|
||||
withCredentials: false,
|
||||
qs: queryParams,
|
||||
body: data,
|
||||
json: true,
|
||||
timeout: localTimeoutMs,
|
||||
_matrix_opts: this.opts
|
||||
},
|
||||
function(err, response, body) {
|
||||
if (localTimeoutMs) {
|
||||
callbacks.clearTimeout(timeoutId);
|
||||
if (timedOut) {
|
||||
return; // already rejected promise
|
||||
}
|
||||
}
|
||||
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
|
||||
handlerFn(err, response, body);
|
||||
}
|
||||
);
|
||||
if (req && req.abort) {
|
||||
// FIXME: This is EVIL, but I can't think of a better way to expose
|
||||
// abort() operations on underlying HTTP requests :(
|
||||
reqPromise.abort = req.abort.bind(req);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
defer.reject(ex);
|
||||
if (callback) {
|
||||
callback(ex);
|
||||
}
|
||||
}
|
||||
return reqPromise;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns a callback that can be invoked by an HTTP request on completion,
|
||||
* that will either resolve or reject the given defer as well as invoke the
|
||||
* given userDefinedCallback (if any).
|
||||
*
|
||||
* If onlyData is true, the defer/callback is invoked with the body of the
|
||||
* response, otherwise the result code.
|
||||
*/
|
||||
var requestCallback = function(defer, userDefinedCallback, onlyData) {
|
||||
userDefinedCallback = userDefinedCallback || function() {};
|
||||
|
||||
return function(err, response, body) {
|
||||
if (!err && response.statusCode >= 400) {
|
||||
err = new module.exports.MatrixError(body);
|
||||
err.httpStatus = response.statusCode;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
defer.reject(err);
|
||||
userDefinedCallback(err);
|
||||
}
|
||||
else {
|
||||
var res = {
|
||||
code: response.statusCode,
|
||||
headers: response.headers,
|
||||
data: body
|
||||
};
|
||||
defer.resolve(onlyData ? body : res);
|
||||
userDefinedCallback(null, onlyData ? body : res);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix error. This is a JavaScript Error with additional
|
||||
* information specific to the standard Matrix error response.
|
||||
* @constructor
|
||||
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
|
||||
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
|
||||
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
|
||||
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
|
||||
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
||||
* @prop {integer} httpStatus The numeric HTTP status code given
|
||||
*/
|
||||
module.exports.MatrixError = function MatrixError(errorJson) {
|
||||
errorJson = errorJson || {};
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
this.message = errorJson.error || "Unknown message";
|
||||
this.data = errorJson;
|
||||
};
|
||||
module.exports.MatrixError.prototype = Object.create(Error.prototype);
|
||||
/** */
|
||||
module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
|
||||
@@ -1,313 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
|
||||
* the public classes.
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
module.exports.EventStatus = {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT: "not_sent",
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING: "sending",
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED: "queued",
|
||||
/** The event has been sent to the server, but we have not yet received the
|
||||
* echo. */
|
||||
SENT: "sent",
|
||||
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED: "cancelled",
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix Event object
|
||||
* @constructor
|
||||
*
|
||||
* @param {Object} event The raw event to be wrapped in this DAO
|
||||
*
|
||||
* @param {Object=} clearEvent For encrypted events, the plaintext payload for
|
||||
* the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*
|
||||
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
|
||||
* this property</b> directly unless you absolutely have to. Prefer the getter
|
||||
* methods defined on this class. Using the getter methods shields your app
|
||||
* from changes to event JSON between Matrix versions.
|
||||
*
|
||||
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
|
||||
* this is a presence event.
|
||||
* @prop {RoomMember} target The room member who is the target of this event, e.g.
|
||||
* the invitee, the person being banned, etc.
|
||||
* @prop {EventStatus} status The sending status of the event.
|
||||
* @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
|
||||
* that getDirectionalContent() will return event.content and not event.prev_content.
|
||||
* Default: true. <strong>This property is experimental and may change.</strong>
|
||||
*/
|
||||
module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
|
||||
this.event = event || {};
|
||||
this.sender = null;
|
||||
this.target = null;
|
||||
this.status = null;
|
||||
this.forwardLooking = true;
|
||||
|
||||
this._clearEvent = clearEvent || {};
|
||||
};
|
||||
|
||||
module.exports.MatrixEvent.prototype = {
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||
* </code>
|
||||
*/
|
||||
getId: function() {
|
||||
return this.event.event_id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the user_id for this event.
|
||||
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
|
||||
*/
|
||||
getSender: function() {
|
||||
return this.event.sender || this.event.user_id; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) type of event.
|
||||
*
|
||||
* @return {string} The event type, e.g. <code>m.room.message</code>
|
||||
*/
|
||||
getType: function() {
|
||||
return this._clearEvent.type || this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (possibly encrypted) type of the event that will be sent to the
|
||||
* homeserver.
|
||||
*
|
||||
* @return {string} The event type.
|
||||
*/
|
||||
getWireType: function() {
|
||||
return this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the room_id for this event. This will return <code>undefined</code>
|
||||
* for <code>m.presence</code> events.
|
||||
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* </code>
|
||||
*/
|
||||
getRoomId: function() {
|
||||
return this.event.room_id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the timestamp of this event.
|
||||
* @return {Number} The event timestamp, e.g. <code>1433502692297</code>
|
||||
*/
|
||||
getTs: function() {
|
||||
return this.event.origin_server_ts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) event content JSON.
|
||||
*
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getContent: function() {
|
||||
return this._clearEvent.content || this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (possibly encrypted) event content JSON that will be sent to the
|
||||
* homeserver.
|
||||
*
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getWireContent: function() {
|
||||
return this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the previous event content JSON. This will only return something for
|
||||
* state events which exist in the timeline.
|
||||
* @return {Object} The previous event content JSON, or an empty object.
|
||||
*/
|
||||
getPrevContent: function() {
|
||||
// v2 then v1 then default
|
||||
return this.getUnsigned().prev_content || this.event.prev_content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get either 'content' or 'prev_content' depending on if this event is
|
||||
* 'forward-looking' or not. This can be modified via event.forwardLooking.
|
||||
* In practice, this means we get the chronologically earlier content value
|
||||
* for this event (this method should surely be called getEarlierContent)
|
||||
* <strong>This method is experimental and may change.</strong>
|
||||
* @return {Object} event.content if this event is forward-looking, else
|
||||
* event.prev_content.
|
||||
*/
|
||||
getDirectionalContent: function() {
|
||||
return this.forwardLooking ? this.getContent() : this.getPrevContent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the age of this event. This represents the age of the event when the
|
||||
* event arrived at the device, and not the age of the event when this
|
||||
* function was called.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
*/
|
||||
getAge: function() {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event state_key if it has one. This will return <code>undefined
|
||||
* </code> for message events.
|
||||
* @return {string} The event's <code>state_key</code>.
|
||||
*/
|
||||
getStateKey: function() {
|
||||
return this.event.state_key;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if this event is a state event.
|
||||
* @return {boolean} True if this is a state event.
|
||||
*/
|
||||
isState: function() {
|
||||
return this.event.state_key !== undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace the content of this event with encrypted versions.
|
||||
* (This is used when sending an event; it should not be used by applications).
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param {string} crypto_type type of the encrypted event - typically
|
||||
* <tt>"m.room.encrypted"</tt>
|
||||
*
|
||||
* @param {object} crypto_content raw 'content' for the encrypted event.
|
||||
*/
|
||||
makeEncrypted: function(crypto_type, crypto_content) {
|
||||
// keep the plain-text data for 'view source'
|
||||
this._clearEvent = {
|
||||
type: this.event.type,
|
||||
content: this.event.content,
|
||||
};
|
||||
this.event.type = crypto_type;
|
||||
this.event.content = crypto_content;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the event is encrypted.
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
isEncrypted: function() {
|
||||
return Boolean(this._clearEvent.type);
|
||||
},
|
||||
|
||||
getUnsigned: function() {
|
||||
return this.event.unsigned || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the content of an event in the same way it would be by the server
|
||||
* if it were redacted before it was sent to us
|
||||
*
|
||||
* @param {Object} the raw event causing the redaction
|
||||
*/
|
||||
makeRedacted: function(redaction_event) {
|
||||
if (!this.event.unsigned) {
|
||||
this.event.unsigned = {};
|
||||
}
|
||||
this.event.unsigned.redacted_because = redaction_event;
|
||||
|
||||
var key;
|
||||
for (key in this.event) {
|
||||
if (!this.event.hasOwnProperty(key)) { continue; }
|
||||
if (!_REDACT_KEEP_KEY_MAP[key]) {
|
||||
delete this.event[key];
|
||||
}
|
||||
}
|
||||
|
||||
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
|
||||
var content = this.getContent();
|
||||
for (key in content) {
|
||||
if (!content.hasOwnProperty(key)) { continue; }
|
||||
if (!keeps[key]) {
|
||||
delete content[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if this event has been redacted
|
||||
*
|
||||
* @return {boolean} True if this event has been redacted
|
||||
*/
|
||||
isRedacted: function() {
|
||||
return Boolean(this.getUnsigned().redacted_because);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
|
||||
*
|
||||
* the server should strip off any keys not in the following list:
|
||||
* event_id
|
||||
* type
|
||||
* room_id
|
||||
* user_id
|
||||
* state_key
|
||||
* prev_state
|
||||
* content
|
||||
* [we keep 'unsigned' as well, since that is created by the local server]
|
||||
*
|
||||
* The content object should also be stripped of all keys, unless it is one of
|
||||
* one of the following event types:
|
||||
* m.room.member allows key membership
|
||||
* m.room.create allows key creator
|
||||
* m.room.join_rules allows key join_rule
|
||||
* m.room.power_levels allows keys ban, events, events_default, kick,
|
||||
* redact, state_default, users, users_default.
|
||||
* m.room.aliases allows key aliases
|
||||
*/
|
||||
// a map giving the keys we keep when an event is redacted
|
||||
var _REDACT_KEEP_KEY_MAP = [
|
||||
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
|
||||
'content', 'unsigned',
|
||||
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});
|
||||
|
||||
// a map from event type to the .content keys we keep when an event is redacted
|
||||
var _REDACT_KEEP_CONTENT_MAP = {
|
||||
'm.room.member': {'membership': 1},
|
||||
'm.room.create': {'creator': 1},
|
||||
'm.room.join_rules': {'join_rule': 1},
|
||||
'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
|
||||
'kick': 1, 'redact': 1, 'state_default': 1,
|
||||
'users': 1, 'users_default': 1,
|
||||
},
|
||||
'm.room.aliases': {'aliases': 1},
|
||||
};
|
||||
@@ -1,431 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
/**
|
||||
* @module models/room-state
|
||||
*/
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var utils = require("../utils");
|
||||
var RoomMember = require("./room-member");
|
||||
|
||||
/**
|
||||
* Construct room state.
|
||||
* @constructor
|
||||
* @param {string} roomId Required. The ID of the room which has this state.
|
||||
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
|
||||
* on the user's ID.
|
||||
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
|
||||
* events dictionary, keyed on the event type and then the state_key value.
|
||||
* @prop {string} paginationToken The pagination token for this state.
|
||||
*/
|
||||
function RoomState(roomId) {
|
||||
this.roomId = roomId;
|
||||
this.members = {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this.events = {
|
||||
// eventType: { stateKey: MatrixEvent }
|
||||
};
|
||||
this.paginationToken = null;
|
||||
|
||||
this._sentinels = {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this._updateModifiedTime();
|
||||
this._displayNameToUserIds = {};
|
||||
this._userIdsToDisplayNames = {};
|
||||
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
|
||||
}
|
||||
utils.inherits(RoomState, EventEmitter);
|
||||
|
||||
/**
|
||||
* Get all RoomMembers in this room.
|
||||
* @return {Array<RoomMember>} A list of RoomMembers.
|
||||
*/
|
||||
RoomState.prototype.getMembers = function() {
|
||||
return utils.values(this.members);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a room member by their user ID.
|
||||
* @param {string} userId The room member's user ID.
|
||||
* @return {RoomMember} The member or null if they do not exist.
|
||||
*/
|
||||
RoomState.prototype.getMember = function(userId) {
|
||||
return this.members[userId] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a room member whose properties will not change with this room state. You
|
||||
* typically want this if you want to attach a RoomMember to a MatrixEvent which
|
||||
* may no longer be represented correctly by Room.currentState or Room.oldState.
|
||||
* The term 'sentinel' refers to the fact that this RoomMember is an unchanging
|
||||
* guardian for state at this particular point in time.
|
||||
* @param {string} userId The room member's user ID.
|
||||
* @return {RoomMember} The member or null if they do not exist.
|
||||
*/
|
||||
RoomState.prototype.getSentinelMember = function(userId) {
|
||||
return this._sentinels[userId] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get state events from the state of the room.
|
||||
* @param {string} eventType The event type of the state event.
|
||||
* @param {string} stateKey Optional. The state_key of the state event. If
|
||||
* this is <code>undefined</code> then all matching state events will be
|
||||
* returned.
|
||||
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
|
||||
* <code>undefined</code>, else a single event (or null if no match found).
|
||||
*/
|
||||
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
|
||||
if (!this.events[eventType]) {
|
||||
// no match
|
||||
return stateKey === undefined ? [] : null;
|
||||
}
|
||||
if (stateKey === undefined) { // return all values
|
||||
return utils.values(this.events[eventType]);
|
||||
}
|
||||
var event = this.events[eventType][stateKey];
|
||||
return event ? event : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an array of one or more state MatrixEvents, overwriting
|
||||
* any existing state with the same {type, stateKey} tuple. Will fire
|
||||
* "RoomState.events" for every event added. May fire "RoomState.members"
|
||||
* if there are <code>m.room.member</code> events.
|
||||
* @param {MatrixEvent[]} stateEvents a list of state events for this room.
|
||||
* @fires module:client~MatrixClient#event:"RoomState.members"
|
||||
* @fires module:client~MatrixClient#event:"RoomState.newMember"
|
||||
* @fires module:client~MatrixClient#event:"RoomState.events"
|
||||
*/
|
||||
RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
var self = this;
|
||||
this._updateModifiedTime();
|
||||
|
||||
// update the core event dict
|
||||
utils.forEach(stateEvents, function(event) {
|
||||
if (event.getRoomId() !== self.roomId) { return; }
|
||||
if (!event.isState()) { return; }
|
||||
|
||||
if (self.events[event.getType()] === undefined) {
|
||||
self.events[event.getType()] = {};
|
||||
}
|
||||
self.events[event.getType()][event.getStateKey()] = event;
|
||||
if (event.getType() === "m.room.member") {
|
||||
_updateDisplayNameCache(
|
||||
self, event.getStateKey(), event.getContent().displayname
|
||||
);
|
||||
_updateThirdPartyTokenCache(self, event);
|
||||
}
|
||||
self.emit("RoomState.events", event, self);
|
||||
});
|
||||
|
||||
// update higher level data structures. This needs to be done AFTER the
|
||||
// core event dict as these structures may depend on other state events in
|
||||
// the given array (e.g. disambiguating display names in one go to do both
|
||||
// clashing names rather than progressively which only catches 1 of them).
|
||||
utils.forEach(stateEvents, function(event) {
|
||||
if (event.getRoomId() !== self.roomId) { return; }
|
||||
if (!event.isState()) { return; }
|
||||
|
||||
if (event.getType() === "m.room.member") {
|
||||
var userId = event.getStateKey();
|
||||
|
||||
// leave events apparently elide the displayname or avatar_url,
|
||||
// so let's fake one up so that we don't leak user ids
|
||||
// into the timeline
|
||||
if (event.getContent().membership === "leave" ||
|
||||
event.getContent().membership === "ban")
|
||||
{
|
||||
event.getContent().avatar_url =
|
||||
event.getContent().avatar_url ||
|
||||
event.getPrevContent().avatar_url;
|
||||
event.getContent().displayname =
|
||||
event.getContent().displayname ||
|
||||
event.getPrevContent().displayname;
|
||||
}
|
||||
|
||||
var member = self.members[userId];
|
||||
if (!member) {
|
||||
member = new RoomMember(event.getRoomId(), userId);
|
||||
self.emit("RoomState.newMember", event, self, member);
|
||||
}
|
||||
// Add a new sentinel for this change. We apply the same
|
||||
// operations to both sentinel and member rather than deep copying
|
||||
// so we don't make assumptions about the properties of RoomMember
|
||||
// (e.g. and manage to break it because deep copying doesn't do
|
||||
// everything).
|
||||
var sentinel = new RoomMember(event.getRoomId(), userId);
|
||||
utils.forEach([member, sentinel], function(roomMember) {
|
||||
roomMember.setMembershipEvent(event, self);
|
||||
// this member may have a power level already, so set it.
|
||||
var pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
|
||||
if (pwrLvlEvent) {
|
||||
roomMember.setPowerLevelEvent(pwrLvlEvent);
|
||||
}
|
||||
});
|
||||
|
||||
self._sentinels[userId] = sentinel;
|
||||
self.members[userId] = member;
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
}
|
||||
else if (event.getType() === "m.room.power_levels") {
|
||||
var members = utils.values(self.members);
|
||||
utils.forEach(members, function(member) {
|
||||
member.setPowerLevelEvent(event);
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current typing event for this room.
|
||||
* @param {MatrixEvent} event The typing event
|
||||
*/
|
||||
RoomState.prototype.setTypingEvent = function(event) {
|
||||
utils.forEach(utils.values(this.members), function(member) {
|
||||
member.setTypingEvent(event);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the m.room.member event which has the given third party invite token.
|
||||
*
|
||||
* @param {string} token The token
|
||||
* @return {?MatrixEvent} The m.room.member event or null
|
||||
*/
|
||||
RoomState.prototype.getInviteForThreePidToken = function(token) {
|
||||
return this._tokenToInvite[token] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the last modified time to the current time.
|
||||
*/
|
||||
RoomState.prototype._updateModifiedTime = function() {
|
||||
this._modified = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the timestamp when this room state was last updated. This timestamp is
|
||||
* updated when this object has received new state events.
|
||||
* @return {number} The timestamp
|
||||
*/
|
||||
RoomState.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user IDs with the specified display name.
|
||||
* @param {string} displayName The display name to get user IDs from.
|
||||
* @return {string[]} An array of user IDs or an empty array.
|
||||
*/
|
||||
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
|
||||
return this._displayNameToUserIds[displayName] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Short-form for maySendEvent('m.room.message', userId)
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* message events into the given room.
|
||||
*/
|
||||
RoomState.prototype.maySendMessage = function(userId) {
|
||||
return this._maySendEventOfType('m.room.message', userId, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given user ID has permission to send a normal
|
||||
* event of type `eventType` into this room.
|
||||
* @param {string} type The type of event to test
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* the given type of event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.maySendEvent = function(eventType, userId) {
|
||||
return this._maySendEventOfType(eventType, userId, false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} type The type of state events to test
|
||||
* @param {MatrixClient} The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
|
||||
if (cli.isGuest()) {
|
||||
return false;
|
||||
}
|
||||
return this.maySendStateEvent(stateEventType, cli.credentials.userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given user ID has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} type The type of state events to test
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
|
||||
return this._maySendEventOfType(stateEventType, userId, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given user ID has permission to send a normal or state
|
||||
* event of type `eventType` into this room.
|
||||
* @param {string} type The type of event to test
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @param {boolean} state If true, tests if the user may send a state
|
||||
event of this type. Otherwise tests whether
|
||||
they may send a regular event.
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* the given type of event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
|
||||
var member = this.getMember(userId);
|
||||
if (!member || member.membership == 'leave') { return false; }
|
||||
|
||||
var power_levels_event = this.getStateEvents('m.room.power_levels', '');
|
||||
|
||||
var power_levels;
|
||||
var events_levels = {};
|
||||
|
||||
var default_user_level = 0;
|
||||
var user_levels = [];
|
||||
|
||||
var state_default = 0;
|
||||
var events_default = 0;
|
||||
if (power_levels_event) {
|
||||
power_levels = power_levels_event.getContent();
|
||||
events_levels = power_levels.events || {};
|
||||
|
||||
default_user_level = parseInt(power_levels.users_default || 0);
|
||||
user_levels = power_levels.users || {};
|
||||
|
||||
if (power_levels.state_default !== undefined) {
|
||||
state_default = power_levels.state_default;
|
||||
} else {
|
||||
state_default = 50;
|
||||
}
|
||||
if (power_levels.events_default !== undefined) {
|
||||
events_default = power_levels.events_default;
|
||||
}
|
||||
}
|
||||
|
||||
var required_level = state ? state_default : events_default;
|
||||
if (events_levels[eventType] !== undefined) {
|
||||
required_level = events_levels[eventType];
|
||||
}
|
||||
return member.powerLevel >= required_level;
|
||||
};
|
||||
|
||||
/**
|
||||
* The RoomState class.
|
||||
*/
|
||||
module.exports = RoomState;
|
||||
|
||||
|
||||
function _updateThirdPartyTokenCache(roomState, memberEvent) {
|
||||
if (!memberEvent.getContent().third_party_invite) {
|
||||
return;
|
||||
}
|
||||
var token = (memberEvent.getContent().third_party_invite.signed || {}).token;
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
var threePidInvite = roomState.getStateEvents(
|
||||
"m.room.third_party_invite", token
|
||||
);
|
||||
if (!threePidInvite) {
|
||||
return;
|
||||
}
|
||||
roomState._tokenToInvite[token] = memberEvent;
|
||||
}
|
||||
|
||||
function _updateDisplayNameCache(roomState, userId, displayName) {
|
||||
var oldName = roomState._userIdsToDisplayNames[userId];
|
||||
delete roomState._userIdsToDisplayNames[userId];
|
||||
if (oldName) {
|
||||
// Remove the old name from the cache.
|
||||
// We clobber the user_id > name lookup but the name -> [user_id] lookup
|
||||
// means we need to remove that user ID from that array rather than nuking
|
||||
// the lot.
|
||||
var existingUserIds = roomState._displayNameToUserIds[oldName] || [];
|
||||
for (var i = 0; i < existingUserIds.length; i++) {
|
||||
if (existingUserIds[i] === userId) {
|
||||
// remove this user ID from this array
|
||||
existingUserIds.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
roomState._displayNameToUserIds[oldName] = existingUserIds;
|
||||
}
|
||||
|
||||
roomState._userIdsToDisplayNames[userId] = displayName;
|
||||
if (!roomState._displayNameToUserIds[displayName]) {
|
||||
roomState._displayNameToUserIds[displayName] = [];
|
||||
}
|
||||
roomState._displayNameToUserIds[displayName].push(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires whenever the event dictionary in room state is updated.
|
||||
* @event module:client~MatrixClient#"RoomState.events"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.events dictionary
|
||||
* was updated.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.events", function(event, state){
|
||||
* var newStateEvent = event;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever a member in the members dictionary is updated in any way.
|
||||
* @event module:client~MatrixClient#"RoomState.members"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.members dictionary
|
||||
* was updated.
|
||||
* @param {RoomMember} member The room member that was updated.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.members", function(event, state, member){
|
||||
* var newMembershipState = member.membership;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever a member is added to the members dictionary. The RoomMember
|
||||
* will not be fully populated yet (e.g. no membership state).
|
||||
* @event module:client~MatrixClient#"RoomState.newMember"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.members dictionary
|
||||
* was updated with a new entry.
|
||||
* @param {RoomMember} member The room member that was added.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.newMember", function(event, state, member){
|
||||
* // add event listeners on 'member'
|
||||
* });
|
||||
*/
|
||||
-1611
File diff suppressed because it is too large
Load Diff
@@ -1,300 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* @module pushprocessor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Construct a Push Processor.
|
||||
* @constructor
|
||||
* @param {Object} client The Matrix client object to use
|
||||
*/
|
||||
function PushProcessor(client) {
|
||||
var escapeRegExp = function(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
var matchingRuleFromKindSet = function(ev, kindset, device) {
|
||||
var rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
|
||||
for (var ruleKindIndex = 0;
|
||||
ruleKindIndex < rulekinds_in_order.length;
|
||||
++ruleKindIndex) {
|
||||
var kind = rulekinds_in_order[ruleKindIndex];
|
||||
var ruleset = kindset[kind];
|
||||
|
||||
for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
|
||||
var rule = ruleset[ruleIndex];
|
||||
if (!rule.enabled) { continue; }
|
||||
|
||||
var rawrule = templateRuleToRaw(kind, rule, device);
|
||||
if (!rawrule) { continue; }
|
||||
|
||||
if (ruleMatchesEvent(rawrule, ev)) {
|
||||
rule.kind = kind;
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
var templateRuleToRaw = function(kind, tprule, device) {
|
||||
var rawrule = {
|
||||
'rule_id': tprule.rule_id,
|
||||
'actions': tprule.actions,
|
||||
'conditions': []
|
||||
};
|
||||
switch (kind) {
|
||||
case 'underride':
|
||||
case 'override':
|
||||
rawrule.conditions = tprule.conditions;
|
||||
break;
|
||||
case 'room':
|
||||
if (!tprule.rule_id) { return null; }
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'key': 'room_id',
|
||||
'pattern': tprule.rule_id
|
||||
});
|
||||
break;
|
||||
case 'sender':
|
||||
if (!tprule.rule_id) { return null; }
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'key': 'user_id',
|
||||
'pattern': tprule.rule_id
|
||||
});
|
||||
break;
|
||||
case 'content':
|
||||
if (!tprule.pattern) { return null; }
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'key': 'content.body',
|
||||
'pattern': tprule.pattern
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (device) {
|
||||
rawrule.conditions.push({
|
||||
'kind': 'device',
|
||||
'profile_tag': device
|
||||
});
|
||||
}
|
||||
return rawrule;
|
||||
};
|
||||
|
||||
var ruleMatchesEvent = function(rule, ev) {
|
||||
var ret = true;
|
||||
for (var i = 0; i < rule.conditions.length; ++i) {
|
||||
var cond = rule.conditions[i];
|
||||
ret &= eventFulfillsCondition(cond, ev);
|
||||
}
|
||||
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
|
||||
return ret;
|
||||
};
|
||||
|
||||
var eventFulfillsCondition = function(cond, ev) {
|
||||
var condition_functions = {
|
||||
"event_match": eventFulfillsEventMatchCondition,
|
||||
"device": eventFulfillsDeviceCondition,
|
||||
"contains_display_name": eventFulfillsDisplayNameCondition,
|
||||
"room_member_count": eventFulfillsRoomMemberCountCondition
|
||||
};
|
||||
if (condition_functions[cond.kind]) {
|
||||
return condition_functions[cond.kind](cond, ev);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
|
||||
if (!cond.is) { return false; }
|
||||
|
||||
var room = client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState || !room.currentState.members) { return false; }
|
||||
|
||||
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
|
||||
return room.currentState.members[m].membership == 'join';
|
||||
}).length;
|
||||
|
||||
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
|
||||
if (!m) { return false; }
|
||||
var ineq = m[1];
|
||||
var rhs = parseInt(m[2]);
|
||||
if (isNaN(rhs)) { return false; }
|
||||
switch (ineq) {
|
||||
case '':
|
||||
case '==':
|
||||
return memberCount == rhs;
|
||||
case '<':
|
||||
return memberCount < rhs;
|
||||
case '>':
|
||||
return memberCount > rhs;
|
||||
case '<=':
|
||||
return memberCount <= rhs;
|
||||
case '>=':
|
||||
return memberCount >= rhs;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
var eventFulfillsDisplayNameCondition = function(cond, ev) {
|
||||
var content = ev.getContent();
|
||||
if (!content || !content.body || typeof content.body != 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var room = client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState || !room.currentState.members ||
|
||||
!room.currentState.getMember(client.credentials.userId)) { return false; }
|
||||
|
||||
var displayName = room.currentState.getMember(client.credentials.userId).name;
|
||||
|
||||
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
|
||||
// as shorthand for [^0-9A-Za-z_].
|
||||
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
|
||||
return content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
var eventFulfillsDeviceCondition = function(cond, ev) {
|
||||
return false; // XXX: Allow a profile tag to be set for the web client instance
|
||||
};
|
||||
|
||||
var eventFulfillsEventMatchCondition = function(cond, ev) {
|
||||
var val = valueForDottedKey(cond.key, ev);
|
||||
if (!val || typeof val != 'string') { return false; }
|
||||
|
||||
var pat;
|
||||
if (cond.key == 'content.body') {
|
||||
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
|
||||
} else {
|
||||
pat = '^' + globToRegexp(cond.pattern) + '$';
|
||||
}
|
||||
var regex = new RegExp(pat, 'i');
|
||||
return !!val.match(regex);
|
||||
};
|
||||
|
||||
var globToRegexp = function(glob) {
|
||||
// From
|
||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||
// Because micromatch is about 130KB with dependencies,
|
||||
// and minimatch is not much better.
|
||||
var pat = escapeRegExp(glob);
|
||||
pat = pat.replace(/\\\*/, '.*');
|
||||
pat = pat.replace(/\?/, '.');
|
||||
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
|
||||
var first = p1 && '^' || '';
|
||||
var second = p2.replace(/\\\-/, '-');
|
||||
return '[' + first + second + ']';
|
||||
});
|
||||
return pat;
|
||||
};
|
||||
|
||||
var valueForDottedKey = function(key, ev) {
|
||||
var parts = key.split('.');
|
||||
var val;
|
||||
|
||||
// special-case the first component to deal with encrypted messages
|
||||
var firstPart = parts[0];
|
||||
if (firstPart == 'content') {
|
||||
val = ev.getContent();
|
||||
parts.shift();
|
||||
} else if (firstPart == 'type') {
|
||||
val = ev.getType();
|
||||
parts.shift();
|
||||
} else {
|
||||
// use the raw event for any other fields
|
||||
val = ev.event;
|
||||
}
|
||||
|
||||
while (parts.length > 0) {
|
||||
var thispart = parts.shift();
|
||||
if (!val[thispart]) { return null; }
|
||||
val = val[thispart];
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets || !rulesets.device) { return null; }
|
||||
if (ev.getSender() == client.credentials.userId) { return null; }
|
||||
|
||||
var allDevNames = Object.keys(rulesets.device);
|
||||
for (var i = 0; i < allDevNames.length; ++i) {
|
||||
var devname = allDevNames[i];
|
||||
var devrules = rulesets.device[devname];
|
||||
|
||||
var matchingRule = matchingRuleFromKindSet(devrules, devname);
|
||||
if (matchingRule) { return matchingRule; }
|
||||
}
|
||||
return matchingRuleFromKindSet(ev, rulesets.global);
|
||||
};
|
||||
|
||||
var actionListToActionsObject = function(actionlist) {
|
||||
var actionobj = { 'notify': false, 'tweaks': {} };
|
||||
for (var i = 0; i < actionlist.length; ++i) {
|
||||
var action = actionlist[i];
|
||||
if (action === 'notify') {
|
||||
actionobj.notify = true;
|
||||
} else if (typeof action === 'object') {
|
||||
if (action.value === undefined) { action.value = true; }
|
||||
actionobj.tweaks[action.set_tweak] = action.value;
|
||||
}
|
||||
}
|
||||
return actionobj;
|
||||
};
|
||||
|
||||
var pushActionsForEventAndRulesets = function(ev, rulesets) {
|
||||
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
|
||||
if (!rule) { return {}; }
|
||||
|
||||
var actionObj = actionListToActionsObject(rule.actions);
|
||||
|
||||
// Some actions are implicit in some situations: we add those here
|
||||
if (actionObj.tweaks.highlight === undefined) {
|
||||
// if it isn't specified, highlight if it's a content
|
||||
// rule but otherwise not
|
||||
actionObj.tweaks.highlight = (rule.kind == 'content');
|
||||
}
|
||||
|
||||
return actionObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user's push actions for the given event
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} ev
|
||||
*
|
||||
* @return {PushAction}
|
||||
*/
|
||||
this.actionsForEvent = function(ev) {
|
||||
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PushAction
|
||||
* @type {Object}
|
||||
* @property {boolean} notify Whether this event should notify the user or not.
|
||||
* @property {Object} tweaks How this event should be notified.
|
||||
* @property {boolean} tweaks.highlight Whether this event should be highlighted
|
||||
* on the UI.
|
||||
* @property {boolean} tweaks.sound Whether this notification should produce a
|
||||
* noise.
|
||||
*/
|
||||
|
||||
/** The PushProcessor class. */
|
||||
module.exports = PushProcessor;
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
/**
|
||||
* @module store/session/webstorage
|
||||
*/
|
||||
|
||||
var utils = require("../../utils");
|
||||
|
||||
var DEBUG = false; // set true to enable console logging.
|
||||
var E2E_PREFIX = "session.e2e.";
|
||||
|
||||
/**
|
||||
* Construct a web storage session store, capable of storing account keys,
|
||||
* session keys and access tokens.
|
||||
* @constructor
|
||||
* @param {WebStorage} webStore A web storage implementation, e.g.
|
||||
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
|
||||
* @throws if the supplied 'store' does not meet the Storage interface of the
|
||||
* WebStorage API.
|
||||
*/
|
||||
function WebStorageSessionStore(webStore) {
|
||||
this.store = webStore;
|
||||
if (!utils.isFunction(webStore.getItem) ||
|
||||
!utils.isFunction(webStore.setItem) ||
|
||||
!utils.isFunction(webStore.removeItem)) {
|
||||
throw new Error(
|
||||
"Supplied webStore does not meet the WebStorage API interface"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WebStorageSessionStore.prototype = {
|
||||
|
||||
/**
|
||||
* Store the end to end account for the logged-in user.
|
||||
* @param {string} account Base64 encoded account.
|
||||
*/
|
||||
storeEndToEndAccount: function(account) {
|
||||
this.store.setItem(KEY_END_TO_END_ACCOUNT, account);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the end to end account for the logged-in user.
|
||||
* @return {?string} Base64 encoded account.
|
||||
*/
|
||||
getEndToEndAccount: function() {
|
||||
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the known devices for a user.
|
||||
* @param {string} userId The user's ID.
|
||||
* @param {object} devices A map from device ID to keys for the device.
|
||||
*/
|
||||
storeEndToEndDevicesForUser: function(userId, devices) {
|
||||
setJsonItem(this.store, keyEndToEndDevicesForUser(userId), devices);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the known devices for a user.
|
||||
* @param {string} userId The user's ID.
|
||||
* @return {object} A map from device ID to keys for the device.
|
||||
*/
|
||||
getEndToEndDevicesForUser: function(userId) {
|
||||
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a session between the logged-in user and another device
|
||||
* @param {string} deviceKey The public key of the other device.
|
||||
* @param {string} sessionId The ID for this end-to-end session.
|
||||
* @param {string} session Base64 encoded end-to-end session.
|
||||
*/
|
||||
storeEndToEndSession: function(deviceKey, sessionId, session) {
|
||||
var sessions = this.getEndToEndSessions(deviceKey) || {};
|
||||
sessions[sessionId] = session;
|
||||
setJsonItem(
|
||||
this.store, keyEndToEndSessions(deviceKey), sessions
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the end-to-end sessions between the logged-in user and another
|
||||
* device.
|
||||
* @param {string} deviceKey The public key of the other device.
|
||||
* @return {object} A map from sessionId to Base64 end-to-end session.
|
||||
*/
|
||||
getEndToEndSessions: function(deviceKey) {
|
||||
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the end-to-end state for a room.
|
||||
* @param {string} roomId The room's ID.
|
||||
* @param {object} roomInfo The end-to-end info for the room.
|
||||
*/
|
||||
storeEndToEndRoom: function(roomId, roomInfo) {
|
||||
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the end-to-end state for a room
|
||||
* @param {string} roomId The room's ID.
|
||||
* @return {object} The end-to-end info for the room.
|
||||
*/
|
||||
getEndToEndRoom: function(roomId) {
|
||||
return getJsonItem(this.store, keyEndToEndRoom(roomId));
|
||||
}
|
||||
};
|
||||
|
||||
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
}
|
||||
|
||||
function keyEndToEndSessions(deviceKey) {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndRoom(roomId) {
|
||||
return E2E_PREFIX + "rooms/" + roomId;
|
||||
}
|
||||
|
||||
function getJsonItem(store, key) {
|
||||
try {
|
||||
return JSON.parse(store.getItem(key));
|
||||
}
|
||||
catch (e) {
|
||||
debuglog("Failed to get key %s: %s", key, e);
|
||||
debuglog(e.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setJsonItem(store, key, val) {
|
||||
store.setItem(key, JSON.stringify(val));
|
||||
}
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/** */
|
||||
module.exports = WebStorageSessionStore;
|
||||
@@ -1,686 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
/**
|
||||
* This is an internal module. Implementation details:
|
||||
* <pre>
|
||||
* Room data is stored as follows:
|
||||
* room_$ROOMID_timeline_$INDEX : [ Event, Event, Event ]
|
||||
* room_$ROOMID_state : {
|
||||
* pagination_token: <oldState.paginationToken>,
|
||||
* events: {
|
||||
* <event_type>: { <state_key> : {JSON} }
|
||||
* }
|
||||
* }
|
||||
* User data is stored as follows:
|
||||
* user_$USERID : User
|
||||
* Sync token:
|
||||
* sync_token : $TOKEN
|
||||
*
|
||||
* Room Retrieval
|
||||
* --------------
|
||||
* Retrieving a room requires the $ROOMID which then pulls out the current state
|
||||
* from room_$ROOMID_state. A defined starting batch of timeline events are then
|
||||
* extracted from the highest numbered $INDEX for room_$ROOMID_timeline_$INDEX
|
||||
* (more indices as required). The $INDEX may be negative. These are
|
||||
* added to the timeline in the same way as /initialSync (old state will diverge).
|
||||
* If there exists a room_$ROOMID_timeline_live key, then a timeline sync should
|
||||
* be performed before retrieving.
|
||||
*
|
||||
* Retrieval of earlier messages
|
||||
* -----------------------------
|
||||
* The earliest event the Room instance knows about is E. Retrieving earlier
|
||||
* messages requires a Room which has a storageToken defined.
|
||||
* This token maps to the index I where the Room is at. Events are then retrieved from
|
||||
* room_$ROOMID_timeline_{I} and elements before E are extracted. If the limit
|
||||
* demands more events, I-1 is retrieved, up until I=min $INDEX where it gives
|
||||
* less than the limit. Index may go negative if you have paginated in the past.
|
||||
*
|
||||
* Full Insertion
|
||||
* --------------
|
||||
* Storing a room requires the timeline and state keys for $ROOMID to
|
||||
* be blown away and completely replaced, which is computationally expensive.
|
||||
* Room.timeline is batched according to the given batch size B. These batches
|
||||
* are then inserted into storage as room_$ROOMID_timeline_$INDEX. Finally,
|
||||
* the current room state is persisted to room_$ROOMID_state.
|
||||
*
|
||||
* Incremental Insertion
|
||||
* ---------------------
|
||||
* As events arrive, the store can quickly persist these new events. This
|
||||
* involves pushing the events to room_$ROOMID_timeline_live. If the
|
||||
* current room state has been modified by the new event, then
|
||||
* room_$ROOMID_state should be updated in addition to the timeline.
|
||||
*
|
||||
* Timeline sync
|
||||
* -------------
|
||||
* Retrieval of events from the timeline depends on the proper batching of
|
||||
* events. This is computationally expensive to perform on every new event, so
|
||||
* is deferred by inserting live events to room_$ROOMID_timeline_live. A
|
||||
* timeline sync reconciles timeline_live and timeline_$INDEX. This involves
|
||||
* retrieving _live and the highest numbered $INDEX batch. If the batch is < B,
|
||||
* the earliest entries from _live are inserted into the $INDEX until the
|
||||
* batch == B. Then, the remaining entries in _live are batched to $INDEX+1,
|
||||
* $INDEX+2, and so on. The easiest way to visualise this is that the timeline
|
||||
* goes from old to new, left to right:
|
||||
* -2 -1 0 1
|
||||
* <--OLD---------------------------------------NEW-->
|
||||
* [a,b,c] [d,e,f] [g,h,i] [j,k,l]
|
||||
*
|
||||
* Purging
|
||||
* -------
|
||||
* Events from the timeline can be purged by removing the lowest
|
||||
* timeline_$INDEX in the store.
|
||||
*
|
||||
* Example
|
||||
* -------
|
||||
* A room with room_id !foo:bar has 9 messages (M1->9 where 9=newest) with a
|
||||
* batch size of 4. The very first time, there is no entry for !foo:bar until
|
||||
* storeRoom() is called, which results in the keys: [Full Insert]
|
||||
* room_!foo:bar_timeline_0 : [M1, M2, M3, M4]
|
||||
* room_!foo:bar_timeline_1 : [M5, M6, M7, M8]
|
||||
* room_!foo:bar_timeline_2 : [M9]
|
||||
* room_!foo:bar_state: { ... }
|
||||
*
|
||||
* 5 new messages (N1-5, 5=newest) arrive and are then added: [Incremental Insert]
|
||||
* room_!foo:bar_timeline_live: [N1]
|
||||
* room_!foo:bar_timeline_live: [N1, N2]
|
||||
* room_!foo:bar_timeline_live: [N1, N2, N3]
|
||||
* room_!foo:bar_timeline_live: [N1, N2, N3, N4]
|
||||
* room_!foo:bar_timeline_live: [N1, N2, N3, N4, N5]
|
||||
*
|
||||
* App is shutdown. Restarts. The timeline is synced [Timeline Sync]
|
||||
* room_!foo:bar_timeline_2 : [M9, N1, N2, N3]
|
||||
* room_!foo:bar_timeline_3 : [N4, N5]
|
||||
* room_!foo:bar_timeline_live: []
|
||||
*
|
||||
* And the room is retrieved with 8 messages: [Room Retrieval]
|
||||
* Room.timeline: [M7, M8, M9, N1, N2, N3, N4, N5]
|
||||
* Room.storageToken: => early_index = 1 because that's where M7 is.
|
||||
*
|
||||
* 3 earlier messages are requested: [Earlier retrieval]
|
||||
* Use storageToken to find batch index 1. Scan batch for earliest event ID.
|
||||
* earliest event = M7
|
||||
* events = room_!foo:bar_timeline_1 where event < M7 = [M5, M6]
|
||||
* Too few events, use next index (0) and get 1 more:
|
||||
* events = room_!foo:bar_timeline_0 = [M1, M2, M3, M4] => [M4]
|
||||
* Return concatentation:
|
||||
* [M4, M5, M6]
|
||||
*
|
||||
* Purge oldest events: [Purge]
|
||||
* del room_!foo:bar_timeline_0
|
||||
* </pre>
|
||||
* @module store/webstorage
|
||||
*/
|
||||
var DEBUG = false; // set true to enable console logging.
|
||||
var utils = require("../utils");
|
||||
var Room = require("../models/room");
|
||||
var User = require("../models/user");
|
||||
var MatrixEvent = require("../models/event").MatrixEvent;
|
||||
|
||||
/**
|
||||
* Construct a web storage store, capable of storing rooms and users.
|
||||
* @constructor
|
||||
* @param {WebStorage} webStore A web storage implementation, e.g.
|
||||
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
|
||||
* @param {integer} batchSize The number of events to store per key/value (room
|
||||
* scoped). Use -1 to store all events for a room under one key/value.
|
||||
* @throws if the supplied 'store' does not meet the Storage interface of the
|
||||
* WebStorage API.
|
||||
*/
|
||||
function WebStorageStore(webStore, batchSize) {
|
||||
this.store = webStore;
|
||||
this.batchSize = batchSize;
|
||||
if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) ||
|
||||
!utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key)) {
|
||||
throw new Error(
|
||||
"Supplied webStore does not meet the WebStorage API interface"
|
||||
);
|
||||
}
|
||||
if (!parseInt(webStore.length) && webStore.length !== 0) {
|
||||
throw new Error(
|
||||
"Supplied webStore does not meet the WebStorage API interface (length)"
|
||||
);
|
||||
}
|
||||
// cached list of room_ids this is storing.
|
||||
this._roomIds = [];
|
||||
this._syncedWithStore = false;
|
||||
// tokens used to remember which index the room instance is at.
|
||||
this._tokens = [
|
||||
// { earliestIndex: -4 }
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the token to stream from.
|
||||
* @return {string} The token or null.
|
||||
*/
|
||||
WebStorageStore.prototype.getSyncToken = function() {
|
||||
return this.store.getItem("sync_token");
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the token to stream from.
|
||||
* @param {string} token The token to stream from.
|
||||
*/
|
||||
WebStorageStore.prototype.setSyncToken = function(token) {
|
||||
this.store.setItem("sync_token", token);
|
||||
};
|
||||
|
||||
/**
|
||||
* Store a room in web storage.
|
||||
* @param {Room} room
|
||||
*/
|
||||
WebStorageStore.prototype.storeRoom = function(room) {
|
||||
var serRoom = SerialisedRoom.fromRoom(room, this.batchSize);
|
||||
persist(this.store, serRoom);
|
||||
if (this._roomIds.indexOf(room.roomId) === -1) {
|
||||
this._roomIds.push(room.roomId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a room from web storage.
|
||||
* @param {string} roomId
|
||||
* @return {?Room}
|
||||
*/
|
||||
WebStorageStore.prototype.getRoom = function(roomId) {
|
||||
// probe if room exists; break early if not. Every room should have state.
|
||||
if (!getItem(this.store, keyName(roomId, "state"))) {
|
||||
debuglog("getRoom: No room with id %s found.", roomId);
|
||||
return null;
|
||||
}
|
||||
var timelineKeys = getTimelineIndices(this.store, roomId);
|
||||
if (timelineKeys.indexOf("live") !== -1) {
|
||||
debuglog("getRoom: Live events found. Syncing timeline for %s", roomId);
|
||||
this._syncTimeline(roomId, timelineKeys);
|
||||
}
|
||||
return loadRoom(this.store, roomId, this.batchSize, this._tokens);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of all rooms from web storage.
|
||||
* @return {Array} An empty array.
|
||||
*/
|
||||
WebStorageStore.prototype.getRooms = function() {
|
||||
var rooms = [];
|
||||
var i;
|
||||
if (!this._syncedWithStore) {
|
||||
// sync with the store to set this._roomIds correctly. We know there is
|
||||
// exactly one 'state' key for each room, so we grab them.
|
||||
this._roomIds = [];
|
||||
for (i = 0; i < this.store.length; i++) {
|
||||
if (this.store.key(i).indexOf("room_") === 0 &&
|
||||
this.store.key(i).indexOf("_state") !== -1) {
|
||||
// grab the middle bit which is the room ID
|
||||
var k = this.store.key(i);
|
||||
this._roomIds.push(
|
||||
k.substring("room_".length, k.length - "_state".length)
|
||||
);
|
||||
}
|
||||
}
|
||||
this._syncedWithStore = true;
|
||||
}
|
||||
// call getRoom on each room_id
|
||||
for (i = 0; i < this._roomIds.length; i++) {
|
||||
var rm = this.getRoom(this._roomIds[i]);
|
||||
if (rm) {
|
||||
rooms.push(rm);
|
||||
}
|
||||
}
|
||||
return rooms;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of summaries from web storage.
|
||||
* @return {Array} An empty array.
|
||||
*/
|
||||
WebStorageStore.prototype.getRoomSummaries = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Store a user in web storage.
|
||||
* @param {User} user
|
||||
*/
|
||||
WebStorageStore.prototype.storeUser = function(user) {
|
||||
// persist the events used to make the user, we can reconstruct on demand.
|
||||
setItem(this.store, "user_" + user.userId, {
|
||||
presence: user.events.presence ? user.events.presence.event : null
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a user from web storage.
|
||||
* @param {string} userId
|
||||
* @return {User}
|
||||
*/
|
||||
WebStorageStore.prototype.getUser = function(userId) {
|
||||
var userData = getItem(this.store, "user_" + userId);
|
||||
if (!userData) {
|
||||
return null;
|
||||
}
|
||||
var user = new User(userId);
|
||||
if (userData.presence) {
|
||||
user.setPresenceEvent(new MatrixEvent(userData.presence));
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve scrollback for this room. Automatically adds events to the timeline.
|
||||
* @param {Room} room The matrix room to add the events to the start of the timeline.
|
||||
* @param {integer} limit The max number of old events to retrieve.
|
||||
* @return {Array<Object>} An array of objects which will be at most 'limit'
|
||||
* length and at least 0. The objects are the raw event JSON. The last element
|
||||
* is the 'oldest' (for parity with homeserver scrollback APIs).
|
||||
*/
|
||||
WebStorageStore.prototype.scrollback = function(room, limit) {
|
||||
if (room.storageToken === undefined || room.storageToken >= this._tokens.length) {
|
||||
return [];
|
||||
}
|
||||
// find the index of the earliest event in this room's timeline
|
||||
var storeData = this._tokens[room.storageToken] || {};
|
||||
var i;
|
||||
var earliestIndex = storeData.earliestIndex;
|
||||
var earliestEventId = room.timeline[0] ? room.timeline[0].getId() : null;
|
||||
debuglog(
|
||||
"scrollback in %s (timeline=%s msgs) i=%s, timeline[0].id=%s - req %s events",
|
||||
room.roomId, room.timeline.length, earliestIndex, earliestEventId, limit
|
||||
);
|
||||
var batch = getItem(
|
||||
this.store, keyName(room.roomId, "timeline", earliestIndex)
|
||||
);
|
||||
if (!batch) {
|
||||
// bad room or already at start, either way we have nothing to give.
|
||||
debuglog("No batch with index %s found.", earliestIndex);
|
||||
return [];
|
||||
}
|
||||
// populate from this batch first
|
||||
var scrollback = [];
|
||||
var foundEventId = false;
|
||||
for (i = batch.length - 1; i >= 0; i--) {
|
||||
// go back and find the earliest event ID, THEN start adding entries.
|
||||
// Make a MatrixEvent so we don't assume .event_id exists
|
||||
// (e.g v2/v3 JSON may be different)
|
||||
var matrixEvent = new MatrixEvent(batch[i]);
|
||||
if (matrixEvent.getId() === earliestEventId) {
|
||||
foundEventId = true;
|
||||
debuglog(
|
||||
"Found timeline[0] event at position %s in batch %s",
|
||||
i, earliestIndex
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!foundEventId) {
|
||||
continue;
|
||||
}
|
||||
// add entry
|
||||
debuglog("Add event at position %s in batch %s", i, earliestIndex);
|
||||
scrollback.push(batch[i]);
|
||||
if (scrollback.length === limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scrollback.length === limit) {
|
||||
debuglog("Batch has enough events to satisfy request.");
|
||||
return scrollback;
|
||||
}
|
||||
if (!foundEventId) {
|
||||
// the earliest index batch didn't contain the event. In other words,
|
||||
// this timeline is at a state we don't know, so bail.
|
||||
debuglog(
|
||||
"Failed to find event ID %s in batch %s", earliestEventId, earliestIndex
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// get the requested earlier events from earlier batches
|
||||
while (scrollback.length < limit) {
|
||||
earliestIndex--;
|
||||
batch = getItem(
|
||||
this.store, keyName(room.roomId, "timeline", earliestIndex)
|
||||
);
|
||||
if (!batch) {
|
||||
// no more events
|
||||
debuglog("No batch found at index %s", earliestIndex);
|
||||
break;
|
||||
}
|
||||
for (i = batch.length - 1; i >= 0; i--) {
|
||||
debuglog("Add event at position %s in batch %s", i, earliestIndex);
|
||||
scrollback.push(batch[i]);
|
||||
if (scrollback.length === limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
debuglog(
|
||||
"Out of %s requested events, returning %s. New index=%s",
|
||||
limit, scrollback.length, earliestIndex
|
||||
);
|
||||
room.addEventsToTimeline(utils.map(scrollback, function(e) {
|
||||
return new MatrixEvent(e);
|
||||
}), true, room.getLiveTimeline());
|
||||
|
||||
this._tokens[room.storageToken] = {
|
||||
earliestIndex: earliestIndex
|
||||
};
|
||||
return scrollback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Store events for a room. The events have already been added to the timeline.
|
||||
* @param {Room} room The room to store events for.
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
* @param {string} token The token associated with these events.
|
||||
* @param {boolean} toStart True if these are paginated results. The last element
|
||||
* is the 'oldest' (for parity with homeserver scrollback APIs).
|
||||
*/
|
||||
WebStorageStore.prototype.storeEvents = function(room, events, token, toStart) {
|
||||
if (toStart) {
|
||||
// add paginated events to lowest batch indexes (can go -ve)
|
||||
var lowIndex = getIndexExtremity(
|
||||
getTimelineIndices(this.store, room.roomId), true
|
||||
);
|
||||
var i, key, batch;
|
||||
for (i = 0; i < events.length; i++) { // loop events to be stored
|
||||
key = keyName(room.roomId, "timeline", lowIndex);
|
||||
batch = getItem(this.store, key) || [];
|
||||
while (batch.length < this.batchSize && i < events.length) {
|
||||
batch.unshift(events[i].event);
|
||||
i++; // increment to insert next event into this batch
|
||||
}
|
||||
i--; // decrement to avoid skipping one (for loop ++s)
|
||||
setItem(this.store, key, batch);
|
||||
lowIndex--; // decrement index to get a new batch.
|
||||
}
|
||||
}
|
||||
else {
|
||||
// dump as live events
|
||||
var liveEvents = getItem(
|
||||
this.store, keyName(room.roomId, "timeline", "live")
|
||||
) || [];
|
||||
debuglog(
|
||||
"Adding %s events to %s live list (which has %s already)",
|
||||
events.length, room.roomId, liveEvents.length
|
||||
);
|
||||
var updateState = false;
|
||||
liveEvents = liveEvents.concat(utils.map(events, function(me) {
|
||||
// cheeky check to avoid looping twice
|
||||
if (me.isState()) {
|
||||
updateState = true;
|
||||
}
|
||||
return me.event;
|
||||
}));
|
||||
setItem(
|
||||
this.store, keyName(room.roomId, "timeline", "live"), liveEvents
|
||||
);
|
||||
if (updateState) {
|
||||
debuglog("Storing state for %s as new events updated state", room.roomId);
|
||||
// use 0 batch size; we don't care about batching right now.
|
||||
var serRoom = SerialisedRoom.fromRoom(room, 0);
|
||||
setItem(this.store, keyName(serRoom.roomId, "state"), serRoom.state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync the 'live' timeline, batching live events according to 'batchSize'.
|
||||
* @param {string} roomId The room to sync the timeline.
|
||||
* @param {Array<String>} timelineIndices Optional. The indices in the timeline
|
||||
* if known already.
|
||||
*/
|
||||
WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
|
||||
timelineIndices = timelineIndices || getTimelineIndices(this.store, roomId);
|
||||
var liveEvents = getItem(this.store, keyName(roomId, "timeline", "live")) || [];
|
||||
|
||||
// get the highest numbered $INDEX batch
|
||||
var highestIndex = getIndexExtremity(timelineIndices);
|
||||
var hiKey = keyName(roomId, "timeline", highestIndex);
|
||||
var hiBatch = getItem(this.store, hiKey) || [];
|
||||
// fill up the existing batch first.
|
||||
while (hiBatch.length < this.batchSize && liveEvents.length > 0) {
|
||||
hiBatch.push(liveEvents.shift());
|
||||
}
|
||||
setItem(this.store, hiKey, hiBatch);
|
||||
|
||||
// start adding new batches as required
|
||||
var batch = [];
|
||||
while (liveEvents.length > 0) {
|
||||
batch.push(liveEvents.shift());
|
||||
if (batch.length === this.batchSize || liveEvents.length === 0) {
|
||||
// persist the full batch and make another
|
||||
highestIndex++;
|
||||
hiKey = keyName(roomId, "timeline", highestIndex);
|
||||
setItem(this.store, hiKey, batch);
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
// reset live array
|
||||
setItem(this.store, keyName(roomId, "timeline", "live"), []);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Store a filter.
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
WebStorageStore.prototype.storeFilter = function(filter) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a filter.
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @return {?Filter} A filter or null.
|
||||
*/
|
||||
WebStorageStore.prototype.getFilter = function(userId, filterId) {
|
||||
return null;
|
||||
};
|
||||
|
||||
function SerialisedRoom(roomId) {
|
||||
this.state = {
|
||||
events: {}
|
||||
};
|
||||
this.timeline = {
|
||||
// $INDEX: []
|
||||
};
|
||||
this.roomId = roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Room instance into a SerialisedRoom instance which can be stored
|
||||
* in the key value store.
|
||||
* @param {Room} room The matrix room to convert
|
||||
* @param {integer} batchSize The number of events per timeline batch
|
||||
* @return {SerialisedRoom} A serialised room representation of 'room'.
|
||||
*/
|
||||
SerialisedRoom.fromRoom = function(room, batchSize) {
|
||||
var self = new SerialisedRoom(room.roomId);
|
||||
var index;
|
||||
self.state.pagination_token = room.oldState.paginationToken;
|
||||
// [room_$ROOMID_state] downcast to POJO from MatrixEvent
|
||||
utils.forEach(utils.keys(room.currentState.events), function(eventType) {
|
||||
utils.forEach(utils.keys(room.currentState.events[eventType]), function(skey) {
|
||||
if (!self.state.events[eventType]) {
|
||||
self.state.events[eventType] = {};
|
||||
}
|
||||
self.state.events[eventType][skey] = (
|
||||
room.currentState.events[eventType][skey].event
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// [room_$ROOMID_timeline_$INDEX]
|
||||
if (batchSize > 0) {
|
||||
index = 0;
|
||||
while (index * batchSize < room.timeline.length) {
|
||||
self.timeline[index] = room.timeline.slice(
|
||||
index * batchSize, (index + 1) * batchSize
|
||||
);
|
||||
self.timeline[index] = utils.map(self.timeline[index], function(me) {
|
||||
// use POJO not MatrixEvent
|
||||
return me.event;
|
||||
});
|
||||
index++;
|
||||
}
|
||||
}
|
||||
else { // don't batch
|
||||
self.timeline[0] = utils.map(room.timeline, function(matrixEvent) {
|
||||
return matrixEvent.event;
|
||||
});
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
function loadRoom(store, roomId, numEvents, tokenArray) {
|
||||
var room = new Room(roomId, {
|
||||
storageToken: tokenArray.length
|
||||
});
|
||||
|
||||
// populate state (flatten nested struct to event array)
|
||||
var currentStateMap = getItem(store, keyName(roomId, "state"));
|
||||
var stateEvents = [];
|
||||
utils.forEach(utils.keys(currentStateMap.events), function(eventType) {
|
||||
utils.forEach(utils.keys(currentStateMap.events[eventType]), function(skey) {
|
||||
stateEvents.push(currentStateMap.events[eventType][skey]);
|
||||
});
|
||||
});
|
||||
// TODO: Fix logic dupe with MatrixClient._processRoomEvents
|
||||
var oldStateEvents = utils.map(
|
||||
utils.deepCopy(stateEvents), function(e) {
|
||||
return new MatrixEvent(e);
|
||||
}
|
||||
);
|
||||
var currentStateEvents = utils.map(stateEvents, function(e) {
|
||||
return new MatrixEvent(e);
|
||||
}
|
||||
);
|
||||
room.oldState.setStateEvents(oldStateEvents);
|
||||
room.currentState.setStateEvents(currentStateEvents);
|
||||
|
||||
// add most recent numEvents
|
||||
var recentEvents = [];
|
||||
var index = getIndexExtremity(getTimelineIndices(store, roomId));
|
||||
var eventIndex = index;
|
||||
var i, key, batch;
|
||||
while (recentEvents.length < numEvents) {
|
||||
key = keyName(roomId, "timeline", index);
|
||||
batch = getItem(store, key) || [];
|
||||
if (batch.length === 0) {
|
||||
// nothing left in the store.
|
||||
break;
|
||||
}
|
||||
for (i = batch.length - 1; i >= 0; i--) {
|
||||
recentEvents.unshift(new MatrixEvent(batch[i]));
|
||||
if (recentEvents.length === numEvents) {
|
||||
eventIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
index--;
|
||||
}
|
||||
// add events backwards to diverge old state correctly.
|
||||
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
|
||||
room.oldState.paginationToken = currentStateMap.pagination_token;
|
||||
// set the token data to let us know which index this room instance is at
|
||||
// for scrollback.
|
||||
tokenArray.push({
|
||||
earliestIndex: eventIndex
|
||||
});
|
||||
return room;
|
||||
}
|
||||
|
||||
function persist(store, serRoom) {
|
||||
setItem(store, keyName(serRoom.roomId, "state"), serRoom.state);
|
||||
utils.forEach(utils.keys(serRoom.timeline), function(index) {
|
||||
setItem(store,
|
||||
keyName(serRoom.roomId, "timeline", index),
|
||||
serRoom.timeline[index]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getTimelineIndices(store, roomId) {
|
||||
var keys = [];
|
||||
for (var i = 0; i < store.length; i++) {
|
||||
if (store.key(i).indexOf(keyName(roomId, "timeline_")) !== -1) {
|
||||
// e.g. room_$ROOMID_timeline_0 => 0
|
||||
keys.push(
|
||||
store.key(i).replace(keyName(roomId, "timeline_"), "")
|
||||
);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getIndexExtremity(timelineIndices, getLowest) {
|
||||
var extremity, index;
|
||||
for (var i = 0; i < timelineIndices.length; i++) {
|
||||
index = parseInt(timelineIndices[i]);
|
||||
if (!isNaN(index) && (
|
||||
extremity === undefined ||
|
||||
!getLowest && index > extremity ||
|
||||
getLowest && index < extremity)) {
|
||||
extremity = index;
|
||||
}
|
||||
}
|
||||
return extremity;
|
||||
}
|
||||
|
||||
function keyName(roomId, key, index) {
|
||||
return "room_" + roomId + "_" + key + (
|
||||
index === undefined ? "" : ("_" + index)
|
||||
);
|
||||
}
|
||||
|
||||
function getItem(store, key) {
|
||||
try {
|
||||
return JSON.parse(store.getItem(key));
|
||||
}
|
||||
catch (e) {
|
||||
debuglog("Failed to get key %s: %s", key, e);
|
||||
debuglog(e.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setItem(store, key, val) {
|
||||
store.setItem(key, JSON.stringify(val));
|
||||
}
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
function delRoomStruct(store, roomId) {
|
||||
var prefix = "room_" + roomId;
|
||||
var keysToRemove = [];
|
||||
for (var i = 0; i < store.length; i++) {
|
||||
if (store.key(i).indexOf(prefix) !== -1) {
|
||||
keysToRemove.push(store.key(i));
|
||||
}
|
||||
}
|
||||
utils.forEach(keysToRemove, function(key) {
|
||||
store.removeItem(key);
|
||||
});
|
||||
} */
|
||||
|
||||
/** Web Storage Store class. */
|
||||
module.exports = WebStorageStore;
|
||||
-1083
File diff suppressed because it is too large
Load Diff
+75
-21
@@ -1,20 +1,27 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.5.5",
|
||||
"version": "2.4.2-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
|
||||
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
|
||||
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
|
||||
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
|
||||
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
|
||||
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
|
||||
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt",
|
||||
"version": "npm run release && git add dist/$npm_package_version"
|
||||
"test:build": "babel -s -d specbuild spec",
|
||||
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
|
||||
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
|
||||
"test": "yarn test:build && yarn test:run",
|
||||
"check": "yarn test:build && _mocha --recursive specbuild --colors",
|
||||
"gendoc": "babel --no-babelrc --plugins transform-class-properties -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
|
||||
"start": "yarn start:init && yarn start:watch",
|
||||
"start:watch": "babel -s -w --skip-initial-build -d lib src",
|
||||
"start:init": "babel -s -d lib src",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
|
||||
"dist": "yarn build",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 93 src spec",
|
||||
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -23,22 +30,69 @@
|
||||
"browser": "browser-index.js",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
".babelrc",
|
||||
".eslintrc.js",
|
||||
"spec/.eslintrc.js",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"RELEASING.md",
|
||||
"examples",
|
||||
"git-hooks",
|
||||
"git-revision.txt",
|
||||
"index.js",
|
||||
"browser-index.js",
|
||||
"jenkins.sh",
|
||||
"lib",
|
||||
"package.json",
|
||||
"release.sh",
|
||||
"spec",
|
||||
"src"
|
||||
],
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bluebird": "^3.5.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"browserify": "^10.2.3",
|
||||
"q": "^1.4.1",
|
||||
"request": "^2.53.0"
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.2",
|
||||
"loglevel": "1.6.1",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"watchify": "^3.2.1",
|
||||
"istanbul": "^0.3.13",
|
||||
"jasmine-node": "^1.14.5",
|
||||
"jshint": "^2.8.0",
|
||||
"jsdoc": "^3.4.0",
|
||||
"uglifyjs": "^2.4.10"
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"exorcist": "^0.4.0",
|
||||
"expect": "^1.20.2",
|
||||
"istanbul": "^0.4.5",
|
||||
"jsdoc": "^3.5.5",
|
||||
"lolex": "^1.5.2",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-jenkins-reporter": "^0.4.0",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
||||
"rimraf": "^2.5.4",
|
||||
"source-map-support": "^0.4.11",
|
||||
"sourceify": "^0.1.0",
|
||||
"terser": "^4.0.0",
|
||||
"watchify": "^3.11.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-1.0.0.tgz"
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"sourceify"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+233
-17
@@ -1,13 +1,32 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk. Performs the steps documented
|
||||
# in RELEASING.md
|
||||
# Script to perform a release of matrix-js-sdk.
|
||||
#
|
||||
# Requires github-changelog-generator; to install, do
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# Requires:
|
||||
# github-changelog-generator; install via:
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# npm; typically installed by Node.js
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
|
||||
exit
|
||||
fi
|
||||
else
|
||||
echo "hub is required: please install it"
|
||||
exit
|
||||
fi
|
||||
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
|
||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
@@ -20,10 +39,28 @@ $USAGE
|
||||
EOF
|
||||
}
|
||||
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached HEAD; then
|
||||
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet; then
|
||||
echo "this git checkout has uncommitted changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
changelog_file="CHANGELOG.md"
|
||||
while getopts hc:xz f; do
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xz f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
@@ -38,6 +75,9 @@ while getopts hc:xz f; do
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
@@ -47,11 +87,35 @@ if [ $# -ne 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
# see if the version has a hyphen in it. Crude,
|
||||
# but semver doesn't support postreleases so anything
|
||||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
if ! command -v update_changelog >/dev/null 2>&1; then
|
||||
echo "release.sh requires github-changelog-generator. Try:" >&2
|
||||
@@ -61,9 +125,16 @@ if [ -z "$skip_changelog" ]; then
|
||||
fi
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
|
||||
# If we're on any branch starting with 'release', we don't create
|
||||
# a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
echo "Using current branch ($curbranch) for release"
|
||||
rel_branch=$curbranch
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
@@ -76,16 +147,158 @@ if [ -z "$skip_changelog" ]; then
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
echo "npm version"
|
||||
npm version "$release"
|
||||
# Bump package.json and build the dist
|
||||
echo "yarn version"
|
||||
# yarn version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because it can only sign with the default key, but we can
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
then
|
||||
pkglock='yarn.lock'
|
||||
else
|
||||
pkglock=''
|
||||
fi
|
||||
git commit package.json $pkglock -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
|
||||
fi
|
||||
|
||||
|
||||
# If there is a 'dist' script in the package.json,
|
||||
# run it in a separate checkout of the project, then
|
||||
# upload any files in the 'dist' directory as release
|
||||
# assets.
|
||||
# We make a completely separate checkout to be sure
|
||||
# we're using released versions of the dependencies
|
||||
# (rather than whatever we're pulling in from yarn link)
|
||||
assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
yarn install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
||||
popd
|
||||
|
||||
for i in "$builddir"/dist/*; do
|
||||
assets="$assets -a $i"
|
||||
if [ -n "$signing_id" ]
|
||||
then
|
||||
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
|
||||
assets="$assets -a $i.asc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag and the release branch
|
||||
git push origin "$rel_branch" "$tag"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signature for the source tarball.
|
||||
#
|
||||
# github will make us a tarball from the tag - we want to create a
|
||||
# signature for it, which means that first of all we need to check that
|
||||
# it's correct.
|
||||
#
|
||||
# we can't deterministically build exactly the same tarball, due to
|
||||
# differences in gzip implementation - but we *can* build the same tar - so
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
tarfile="$tag.tar.gz"
|
||||
gh_project_url=$(git remote get-url origin |
|
||||
sed -e 's#^git@github\.com:#https://github.com/#' \
|
||||
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
|
||||
-e 's/\.git$//')
|
||||
project_name="${gh_project_url##*/}"
|
||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||
|
||||
# unzip it and compare it with the tar we would generate
|
||||
if ! cmp --silent <(gunzip -c $tarfile) \
|
||||
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
||||
|
||||
# we don't bail out here, because really it's more likely that our comparison
|
||||
# screwed up and it's super annoying to abort the script at this point.
|
||||
cat >&2 <<EOF
|
||||
!!!!!!!!!!!!!!!!!
|
||||
!!!! WARNING !!!!
|
||||
|
||||
Mismatch between our own tarfile and that generated by github: not signing
|
||||
source tarball.
|
||||
|
||||
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
|
||||
attach it to the release as $source_sigfile.
|
||||
|
||||
!!!!!!!!!!!!!!!!!
|
||||
EOF
|
||||
else
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
|
||||
assets="$assets -a $source_sigfile"
|
||||
fi
|
||||
fi
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
|
||||
release_text=`mktemp`
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
hub release create $hubflags $assets -F "${release_text}" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
npm publish
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
npm run gendoc
|
||||
yarn gendoc
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
@@ -98,21 +311,24 @@ if [ -z "$skip_jsdoc" ]; then
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# merge release branch to master
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
git merge "$rel_branch"
|
||||
|
||||
# push everything to github
|
||||
git push origin master "$rel_branch" "$tag"
|
||||
# push master and docs (if generated) to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# publish to npmjs
|
||||
npm publish
|
||||
|
||||
# finally, merge master back onto develop
|
||||
git checkout develop
|
||||
git pull
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Outputs the body of the first entry of changelog file on stdin
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
found_first_header = False
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if re.match(r"^Changes in \[.*\]", line):
|
||||
if found_first_header:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print line
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
@@ -40,14 +40,16 @@ MockStorageApi.prototype = {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
var keys = [];
|
||||
for (var k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) { continue; }
|
||||
const keys = [];
|
||||
for (const k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) {
|
||||
continue;
|
||||
}
|
||||
keys.push(k);
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/** */
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import sdk from '..';
|
||||
import testUtils from './test-utils';
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
|
||||
import logger from '../src/logger';
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} userId
|
||||
* @param {string} deviceId
|
||||
* @param {string} accessToken
|
||||
*
|
||||
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
|
||||
* session store. If undefined, we will create a MockStorageApi.
|
||||
* @param {object} options additional options to pass to the client
|
||||
*/
|
||||
export default function TestClient(
|
||||
userId, deviceId, accessToken, sessionStoreBackend, options,
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
}
|
||||
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
options = Object.assign({
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: sessionStore,
|
||||
request: this.httpBackend.requestFn,
|
||||
}, options);
|
||||
if (!options.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
options.cryptoStore = this.cryptoStore;
|
||||
}
|
||||
this.client = sdk.createClient(options);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
return 'TestClient[' + this.userId + ']';
|
||||
};
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
|
||||
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: 'detached',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
this.httpBackend.flushAllExpected(),
|
||||
testUtils.syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(this + ': started');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
TestClient.prototype.stop = function() {
|
||||
this.client.stopClient();
|
||||
return this.httpBackend.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
*/
|
||||
TestClient.prototype.expectDeviceKeyUpload = function() {
|
||||
const self = this;
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(self + ': received device keys');
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
|
||||
|
||||
self.deviceKeys = content.device_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns {Promise} for the one-time keys
|
||||
*/
|
||||
TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return {one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).toNotEqual({});
|
||||
logger.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return {one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query device keys.
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} response response to the query.
|
||||
*/
|
||||
TestClient.prototype.expectKeyQuery = function(response) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, (path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual(
|
||||
{},
|
||||
"Expected key query for " + userId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getDeviceKey = function() {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getSigningKey = function() {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @returns {Promise} promise which completes once the sync has been flushed
|
||||
*/
|
||||
TestClient.prototype.flushSync = function() {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
testUtils.syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import TestClient from '../TestClient';
|
||||
import testUtils from '../test-utils';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single e2e room (ROOM_ID), with the
|
||||
* members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} sync response
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
const stateEvents = [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
Array.prototype.push.apply(
|
||||
stateEvents,
|
||||
roomMembers.map(
|
||||
(m) => testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
state: {
|
||||
events: stateEvents,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionStoreBackend;
|
||||
let aliceTestClient;
|
||||
|
||||
async function createTestClient() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
|
||||
);
|
||||
await testClient.client.initCrypto();
|
||||
return testClient;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
|
||||
aliceTestClient = await createTestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, {event_id: '$event1'});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client._crypto._deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: {'@chris:abc': {}},
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: {'@bob:xyz': {}},
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
}).timeout(3000);
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz']));
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
0, "Alice should be tracking bob's device list",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Alice leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves whilst Alice is offline", async function() {
|
||||
aliceTestClient.stop();
|
||||
|
||||
const anotherTestClient = await createTestClient();
|
||||
|
||||
try {
|
||||
await anotherTestClient.start();
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
|
||||
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
anotherTestClient.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,27 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend;
|
||||
var selfUserId = "@alice:localhost";
|
||||
var selfAccessToken = "aseukfgwef";
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client;
|
||||
let httpBackend;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: selfUserId,
|
||||
accessToken: selfAccessToken
|
||||
accessToken: selfAccessToken,
|
||||
});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
@@ -25,17 +30,18 @@ describe("MatrixClient events", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
var SYNC_DATA = {
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online"
|
||||
})
|
||||
]
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online",
|
||||
}),
|
||||
],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
@@ -43,30 +49,30 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
|
||||
})
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
|
||||
}),
|
||||
],
|
||||
prev_batch: "s"
|
||||
prev_batch: "s",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar"
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar"
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
creator: "@foo:bar",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
var NEXT_SYNC_DATA = {
|
||||
const NEXT_SYNC_DATA = {
|
||||
next_batch: "e_6_7",
|
||||
rooms: {
|
||||
join: {
|
||||
@@ -74,44 +80,45 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
|
||||
room: "!erufh:bar", user: "@foo:bar",
|
||||
msg: "ello ello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
|
||||
}),
|
||||
]
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: "!erufh:bar", content: {
|
||||
user_ids: ["@foo:bar"]
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
user_ids: ["@foo:bar"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function(done) {
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var expectedEvents = [];
|
||||
let expectedEvents = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client.on("event", function(event) {
|
||||
var found = false;
|
||||
for (var i = 0; i < expectedEvents.length; i++) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
@@ -119,49 +126,56 @@ describe("MatrixClient events", function() {
|
||||
}
|
||||
}
|
||||
expect(found).toBe(
|
||||
true, "Unexpected 'event' emitted: " + event.getType()
|
||||
true, "Unexpected 'event' emitted: " + event.getType(),
|
||||
);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client).then(() => {
|
||||
return utils.syncPromise(client);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(
|
||||
0, "Failed to see all events from /sync calls"
|
||||
0, "Failed to see all events from /sync calls",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
var fired = false;
|
||||
let fired = false;
|
||||
client.on("User.presence", function(event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeDefined();
|
||||
expect(event).toBeDefined();
|
||||
if (!user || !event) { return; }
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
if (!user || !event) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0].content.presence
|
||||
SYNC_DATA.presence.events[0].content.presence,
|
||||
);
|
||||
});
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
httpBackend.flushAllExpected().done(function() {
|
||||
expect(fired).toBe(true, "User.presence didn't fire.");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room events", function(done) {
|
||||
it("should emit Room events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
var roomInvokeCount = 0;
|
||||
var roomNameInvokeCount = 0;
|
||||
var timelineFireCount = 0;
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client.on("Room", function(room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
@@ -176,35 +190,37 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(roomInvokeCount).toEqual(
|
||||
1, "Room fired wrong number of times."
|
||||
1, "Room fired wrong number of times.",
|
||||
);
|
||||
expect(roomNameInvokeCount).toEqual(
|
||||
1, "Room.name fired wrong number of times."
|
||||
1, "Room.name fired wrong number of times.",
|
||||
);
|
||||
expect(timelineFireCount).toEqual(
|
||||
3, "Room.timeline fired the wrong number of times"
|
||||
3, "Room.timeline fired the wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function(done) {
|
||||
it("should emit RoomState events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create"
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
];
|
||||
var eventsInvokeCount = 0;
|
||||
var membersInvokeCount = 0;
|
||||
var newMemberInvokeCount = 0;
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
var index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType()
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).toNotEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
if (index >= 0) {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
@@ -225,28 +241,30 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(membersInvokeCount).toEqual(
|
||||
1, "RoomState.members fired wrong number of times"
|
||||
1, "RoomState.members fired wrong number of times",
|
||||
);
|
||||
expect(newMemberInvokeCount).toEqual(
|
||||
1, "RoomState.newMember fired wrong number of times"
|
||||
1, "RoomState.newMember fired wrong number of times",
|
||||
);
|
||||
expect(eventsInvokeCount).toEqual(
|
||||
2, "RoomState.events fired wrong number of times"
|
||||
2, "RoomState.events fired wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function(done) {
|
||||
it("should emit RoomMember events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var typingInvokeCount = 0;
|
||||
var powerLevelInvokeCount = 0;
|
||||
var nameInvokeCount = 0;
|
||||
var membershipInvokeCount = 0;
|
||||
let typingInvokeCount = 0;
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client.on("RoomMember.name", function(event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
@@ -264,40 +282,61 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(typingInvokeCount).toEqual(
|
||||
1, "RoomMember.typing fired wrong number of times"
|
||||
1, "RoomMember.typing fired wrong number of times",
|
||||
);
|
||||
expect(powerLevelInvokeCount).toEqual(
|
||||
0, "RoomMember.powerLevel fired wrong number of times"
|
||||
0, "RoomMember.powerLevel fired wrong number of times",
|
||||
);
|
||||
expect(nameInvokeCount).toEqual(
|
||||
0, "RoomMember.name fired wrong number of times"
|
||||
0, "RoomMember.name fired wrong number of times",
|
||||
);
|
||||
expect(membershipInvokeCount).toEqual(
|
||||
1, "RoomMember.membership fired wrong number of times"
|
||||
1, "RoomMember.membership fired wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
|
||||
var sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(event, member) {
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times"
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
import logger from '../../src/logger';
|
||||
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var userId = "@alice:localhost";
|
||||
var userName = "Alice";
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!foo:bar";
|
||||
var otherUserId = "@bob:localhost";
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
|
||||
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
|
||||
var ROOM_NAME_EVENT = utils.mkEvent({
|
||||
const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
name: "Old room name",
|
||||
},
|
||||
});
|
||||
|
||||
var INITIAL_SYNC_DATA = {
|
||||
const INITIAL_SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
@@ -31,33 +33,33 @@ var INITIAL_SYNC_DATA = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello"
|
||||
})
|
||||
room: roomId, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1"
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob"
|
||||
user: otherUserId, name: "Bob",
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
creator: userId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var EVENTS = [
|
||||
const EVENTS = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "we",
|
||||
}),
|
||||
@@ -81,26 +83,27 @@ function startClient(httpBackend, client) {
|
||||
client.startClient();
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
var deferred = q.defer();
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function(state) {
|
||||
console.log("sync", state);
|
||||
logger.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return deferred.promise;
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
describe("getEventTimeline support", function() {
|
||||
var httpBackend;
|
||||
var client;
|
||||
let httpBackend;
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
@@ -109,6 +112,7 @@ describe("getEventTimeline support", function() {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function(done) {
|
||||
@@ -118,15 +122,17 @@ describe("getEventTimeline support", function() {
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toThrow();
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function(done) {
|
||||
it("timeline support works when enabled", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@@ -134,14 +140,13 @@ describe("getEventTimeline support", function() {
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
.not.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
return startClient(httpBackend, client).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toNotThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -153,9 +158,9 @@ describe("getEventTimeline support", function() {
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
var room;
|
||||
let room;
|
||||
|
||||
startClient(httpBackend, client
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
|
||||
@@ -192,18 +197,19 @@ describe("getEventTimeline support", function() {
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[1]);
|
||||
|
||||
httpBackend.when("GET", "/messages").respond(200, {
|
||||
chunk: [EVENTS[0]],
|
||||
start: "pagin_start",
|
||||
end: "pagin_end",
|
||||
});
|
||||
|
||||
|
||||
return httpBackend.flush("/sync", 2);
|
||||
}).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[1]);
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
return client.scrollback(room);
|
||||
}).then(function() {
|
||||
@@ -211,15 +217,18 @@ describe("getEventTimeline support", function() {
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[0]);
|
||||
expect(room.timeline[1].event).toEqual(EVENTS[1]);
|
||||
expect(room.oldState.paginationToken).toEqual("pagin_end");
|
||||
}).catch(utils.failTest).done(done);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
var client, httpBackend;
|
||||
import expect from 'expect';
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this);
|
||||
describe("MatrixClient event timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
@@ -230,8 +239,7 @@ describe("MatrixClient event timelines", function() {
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client)
|
||||
.catch(utils.failTest).done(done);
|
||||
return startClient(httpBackend, client);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -240,8 +248,9 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should create a new timeline for new events", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
@@ -257,23 +266,25 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(room, "event1:bar").then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
for (var i = 0; i < 4; i++) {
|
||||
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
|
||||
expect(tl.getEvents()[i].sender.name).toEqual(userName);
|
||||
}
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
|
||||
expect(tl.getEvents()[i].sender.name).toEqual(userName);
|
||||
}
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return existing timeline for known events", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should return existing timeline for known events", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
@@ -290,21 +301,25 @@ describe("MatrixClient event timelines", function() {
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.flush("/sync").then(function() {
|
||||
return client.getEventTimeline(room, EVENTS[0].event_id);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync"),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].sender.name).toEqual(userName);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("f_1_1");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("s_5_4");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("f_1_1");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
});
|
||||
});
|
||||
|
||||
it("should update timelines where they overlap a previous /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should update timelines where they overlap a previous /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
@@ -334,8 +349,9 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(room, EVENTS[2].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
@@ -345,15 +361,20 @@ describe("MatrixClient event timelines", function() {
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).catch(utils.failTest).done(done);
|
||||
}).done(() => deferred.resolve(),
|
||||
(e) => deferred.reject(e));
|
||||
});
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should join timelines where they overlap a previous /context",
|
||||
function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
@@ -409,44 +430,46 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
var tl0, tl2, tl3;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl0 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl2 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[3].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl3 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[1].event_id);
|
||||
}).then(function(tl) {
|
||||
// we expect it to get merged in with event 2
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(tl0);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(tl3);
|
||||
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token3");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
let tl0;
|
||||
let tl3;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl0 = tl;
|
||||
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl3 = tl;
|
||||
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
|
||||
}).then(function(tl) {
|
||||
// we expect it to get merged in with event 2
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(tl0);
|
||||
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(tl3);
|
||||
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toBe(null);
|
||||
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token3");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should fail gracefully if there is no event field", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should fail gracefully if there is no event field", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
|
||||
@@ -460,21 +483,23 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(room, "event1"
|
||||
).then(function(tl) {
|
||||
// could do with a fail()
|
||||
expect(true).toBeFalsy();
|
||||
}).catch(function(e) {
|
||||
expect(String(e)).toMatch(/'event'/);
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, "event1",
|
||||
).then(function(tl) {
|
||||
// could do with a fail()
|
||||
expect(true).toBeFalsy();
|
||||
}, function(e) {
|
||||
expect(String(e)).toMatch(/'event'/);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline", function() {
|
||||
it("should allow you to paginate backwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should allow you to paginate backwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
@@ -491,7 +516,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
var params = req.queryParams;
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual(30);
|
||||
@@ -502,29 +527,31 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token1");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token0");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
let tl;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token1");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token0");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it("should allow you to paginate forwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should allow you to paginate forwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
@@ -541,7 +568,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
var params = req.queryParams;
|
||||
const params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual(20);
|
||||
@@ -552,31 +579,32 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
tl, {backwards: false, limit: 20});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token1");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
let tl;
|
||||
return Promise.all([
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
tl, {backwards: false, limit: 20});
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token0");
|
||||
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
.toEqual("end_token1");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event timeline for sent events", function() {
|
||||
var TXN_ID = "txn1";
|
||||
var event = utils.mkMessage({
|
||||
const TXN_ID = "txn1";
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body",
|
||||
});
|
||||
event.unsigned = {transaction_id: TXN_ID};
|
||||
@@ -595,7 +623,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
event
|
||||
event,
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
@@ -605,54 +633,138 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should work when /send returns before /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should work when /send returns before /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
}).then(function(tl) {
|
||||
// 2 because the initial sync contained an event
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
return Promise.all([
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
// 2 because the initial sync contained an event
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
|
||||
// now let the sync complete, and check it again
|
||||
return httpBackend.flush("/sync", 1);
|
||||
}).then(function() {
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
}).catch(utils.failTest).done(done);
|
||||
// now let the sync complete, and check it again
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
}),
|
||||
|
||||
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest);
|
||||
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should work when /send returns after /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
it("should work when /send returns after /sync", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
// initiate the send, and set up checks to be done when it completes
|
||||
// - but note that it won't complete until after the /sync does, below.
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
console.log("sendTextMessage completed");
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (2)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
}).catch(utils.failTest).done(done);
|
||||
return Promise.all([
|
||||
// initiate the send, and set up checks to be done when it completes
|
||||
// - but note that it won't complete until after the /sync does, below.
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
logger.log("sendTextMessage completed");
|
||||
expect(res.event_id).toEqual(event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
logger.log("getEventTimeline completed (2)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
|
||||
}),
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (1)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
logger.log("getEventTimeline completed (1)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
|
||||
// now let the send complete.
|
||||
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
|
||||
}).catch(utils.failTest);
|
||||
// now let the send complete.
|
||||
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function(done) {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
const event = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: otherUserId,
|
||||
});
|
||||
const redaction = utils.mkEvent({
|
||||
type: "m.room.redaction",
|
||||
room_id: roomId,
|
||||
sender: otherUserId,
|
||||
content: {},
|
||||
});
|
||||
redaction.redacts = event.event_id;
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
redaction,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[1].isRedacted()).toBe(true);
|
||||
|
||||
const sync2 = {
|
||||
next_batch: "batch2",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
sync2.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, sync2);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var publicGlobals = require("../../lib/matrix");
|
||||
var Room = publicGlobals.Room;
|
||||
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
|
||||
var Filter = publicGlobals.Filter;
|
||||
var utils = require("../test-utils");
|
||||
var MockStorageApi = require("../MockStorageApi");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const publicGlobals = require("../../lib/matrix");
|
||||
const Room = publicGlobals.Room;
|
||||
const MemoryStore = publicGlobals.MemoryStore;
|
||||
const Filter = publicGlobals.Filter;
|
||||
const utils = require("../test-utils");
|
||||
const MockStorageApi = require("../MockStorageApi");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend, store, sessionStore;
|
||||
var userId = "@alice:localhost";
|
||||
var accessToken = "aseukfgwef";
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let store = null;
|
||||
let sessionStore = null;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MatrixInMemoryStore();
|
||||
store = new MemoryStore();
|
||||
|
||||
var mockStorage = new MockStorageApi();
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
|
||||
|
||||
sdk.request(httpBackend.requestFn);
|
||||
@@ -35,16 +41,130 @@ describe("MatrixClient", function() {
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("uploadContent", function() {
|
||||
const buf = new Buffer('hello world');
|
||||
it("should upload the file", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.queryParams.filename).toEqual("hi.txt");
|
||||
if (!(req.queryParams.access_token == accessToken ||
|
||||
req.headers["Authorization"] == "Bearer " + accessToken)) {
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
expect(req.headers["Content-Type"]).toEqual("text/plain");
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
expect(req.opts.timeout).toBe(undefined);
|
||||
}).respond(200, "content", true);
|
||||
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
expect(prom).toBeTruthy();
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
// for backwards compatibility, we return the raw JSON
|
||||
expect(response).toEqual("content");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse the response if rawResponse=false", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(200, { "content_uri": "uri" });
|
||||
|
||||
client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}, {
|
||||
rawResponse: false,
|
||||
}).then(function(response) {
|
||||
expect(response.content_uri).toEqual("uri");
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse errors into a MatrixError", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(400, {
|
||||
"errcode": "M_SNAFU",
|
||||
"error": "broken",
|
||||
});
|
||||
|
||||
client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}).then(function(response) {
|
||||
throw Error("request not failed");
|
||||
}, function(error) {
|
||||
expect(error.httpStatus).toEqual(400);
|
||||
expect(error.errcode).toEqual("M_SNAFU");
|
||||
expect(error.message).toEqual("broken");
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should return a promise which can be cancelled", function(done) {
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
throw Error("request not aborted");
|
||||
}, function(error) {
|
||||
expect(error).toEqual("aborted");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
|
||||
const r = client.cancelUpload(prom);
|
||||
expect(r).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("joinRoom", function() {
|
||||
it("should no-op if you've already joined a room", function() {
|
||||
var roomId = "!foo:bar";
|
||||
var room = new Room(roomId);
|
||||
const roomId = "!foo:bar";
|
||||
const room = new Room(roomId, userId);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", event: true
|
||||
})
|
||||
user: userId, room: roomId, mship: "join", event: true,
|
||||
}),
|
||||
]);
|
||||
store.storeRoom(room);
|
||||
client.joinRoom(roomId);
|
||||
@@ -53,11 +173,11 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("getFilter", function() {
|
||||
var filterId = "f1lt3r1d";
|
||||
const filterId = "f1lt3r1d";
|
||||
|
||||
it("should return a filter from the store if allowCached", function(done) {
|
||||
var filter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client"
|
||||
const filter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store.storeFilter(filter);
|
||||
client.getFilter(userId, filterId, true).done(function(gotFilter) {
|
||||
@@ -69,16 +189,16 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("should do an HTTP request if !allowCached even if one exists",
|
||||
function(done) {
|
||||
var httpFilterDefinition = {
|
||||
event_format: "federation"
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
|
||||
var storeFilter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client"
|
||||
const storeFilter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store.storeFilter(storeFilter);
|
||||
client.getFilter(userId, filterId, false).done(function(gotFilter) {
|
||||
@@ -91,17 +211,17 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("should do an HTTP request if nothing is in the cache and then store it",
|
||||
function(done) {
|
||||
var httpFilterDefinition = {
|
||||
event_format: "federation"
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
expect(store.getFilter(userId, filterId)).toBeNull();
|
||||
expect(store.getFilter(userId, filterId)).toBe(null);
|
||||
|
||||
httpBackend.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
client.getFilter(userId, filterId, true).done(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store.getFilter(userId, filterId)).toBeDefined();
|
||||
expect(store.getFilter(userId, filterId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -110,21 +230,21 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("createFilter", function() {
|
||||
var filterId = "f1llllllerid";
|
||||
const filterId = "f1llllllerid";
|
||||
|
||||
it("should do an HTTP request and then store the filter", function(done) {
|
||||
expect(store.getFilter(userId, filterId)).toBeNull();
|
||||
expect(store.getFilter(userId, filterId)).toBe(null);
|
||||
|
||||
var filterDefinition = {
|
||||
event_format: "client"
|
||||
const filterDefinition = {
|
||||
event_format: "client",
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
"POST", "/user/" + encodeURIComponent(userId) + "/filter"
|
||||
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(filterDefinition);
|
||||
}).respond(200, {
|
||||
filter_id: filterId
|
||||
filter_id: filterId,
|
||||
});
|
||||
|
||||
client.createFilter(filterDefinition).done(function(gotFilter) {
|
||||
@@ -138,8 +258,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("searching", function() {
|
||||
|
||||
var response = {
|
||||
const response = {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
count: 24,
|
||||
@@ -152,26 +271,26 @@ describe("MatrixClient", function() {
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
body: "a result",
|
||||
msgtype: "m.text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("searchMessageText should perform a /search for room_events", function(done) {
|
||||
client.searchMessageText({
|
||||
query: "monkeys"
|
||||
query: "monkeys",
|
||||
});
|
||||
httpBackend.when("POST", "/search").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
search_categories: {
|
||||
room_events: {
|
||||
search_term: "monkeys"
|
||||
}
|
||||
}
|
||||
search_term: "monkeys",
|
||||
},
|
||||
},
|
||||
});
|
||||
}).respond(200, response);
|
||||
|
||||
@@ -183,98 +302,118 @@ describe("MatrixClient", function() {
|
||||
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
it("should do an HTTP request and then store the keys", function(done) {
|
||||
var ed25519key = "wV5E3EUSHpHuoZLljNzojlabjGdXT3Mz7rugG9zgbkI";
|
||||
var borisKeys = {
|
||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
const borisKeys = {
|
||||
dev1: {
|
||||
algorithms: ["1"], keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
device_id: "dev1",
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
signatures: {
|
||||
boris: {
|
||||
"ed25519:dev1":
|
||||
"u99n8WZ61G//K6eVgYc+RDLVapmjttxqhjNucIFGEIJ" +
|
||||
"oA4TUY8FmiGv3zl0EA71zrvPDfnFL5XLNsdc55NGbDg"
|
||||
}
|
||||
"ed25519:dev1":
|
||||
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
|
||||
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
|
||||
},
|
||||
},
|
||||
unsigned: { "abc": "def" },
|
||||
}
|
||||
user_id: "boris",
|
||||
},
|
||||
};
|
||||
var chazKeys = {
|
||||
const chazKeys = {
|
||||
dev2: {
|
||||
algorithms: ["2"], keys: { "ed25519:dev2": ed25519key },
|
||||
algorithms: ["2"],
|
||||
device_id: "dev2",
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
chaz: {
|
||||
"ed25519:dev2":
|
||||
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
|
||||
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
|
||||
}
|
||||
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
|
||||
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
|
||||
},
|
||||
},
|
||||
unsigned: { "ghi": "def" },
|
||||
}
|
||||
};
|
||||
var daveKeys = {
|
||||
dev3: {
|
||||
algorithms: ["3"], keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
dave: {
|
||||
"ed25519:dev2":
|
||||
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
|
||||
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
|
||||
}
|
||||
},
|
||||
unsigned: { "ghi": "def" },
|
||||
}
|
||||
user_id: "chaz",
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
function sign(o) {
|
||||
var anotherjson = require('another-json');
|
||||
var b = JSON.parse(JSON.stringify(o));
|
||||
delete(b.signatures);
|
||||
delete(b.unsigned);
|
||||
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
logger.log("Ed25519: " + ed25519key);
|
||||
logger.log("boris:", sign(borisKeys.dev1));
|
||||
logger.log("chaz:", sign(chazKeys.dev2));
|
||||
*/
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}, dave: {}}});
|
||||
expect(req.data).toEqual({device_keys: {
|
||||
'boris': {},
|
||||
'chaz': {},
|
||||
}});
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
chaz: chazKeys,
|
||||
dave: daveKeys,
|
||||
},
|
||||
});
|
||||
|
||||
client.downloadKeys(["boris", "chaz", "dave"]).then(function(res) {
|
||||
expect(res).toEqual({
|
||||
boris: {
|
||||
dev1: {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { "abc": "def" },
|
||||
},
|
||||
},
|
||||
chaz: {
|
||||
dev2: {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2" : ed25519key },
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
},
|
||||
},
|
||||
dave: {
|
||||
// dave's key fails validation.
|
||||
},
|
||||
client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { "abc": "def" },
|
||||
});
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
assertObjectContains(res.chaz.dev2, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
});
|
||||
}).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return a rejected promise if the request fails", function(done) {
|
||||
httpBackend.when("POST", "/keys/query").respond(400);
|
||||
describe("deleteDevice", function() {
|
||||
const auth = {a: 1};
|
||||
it("should pass through an auth dict", function(done) {
|
||||
httpBackend.when(
|
||||
"DELETE", "/_matrix/client/r0/devices/my_device",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({auth: auth});
|
||||
}).respond(200);
|
||||
|
||||
var exceptionThrown;
|
||||
client.downloadKeys(["bottom"]).then(function() {
|
||||
fail("download didn't fail");
|
||||
}, function(err) {
|
||||
exceptionThrown = err;
|
||||
}).then(function() {
|
||||
expect(exceptionThrown).toBeTruthy();
|
||||
}).catch(utils.failTest).done(done);
|
||||
client.deleteDevice(
|
||||
"my_device", auth,
|
||||
).nodeify(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
for (const k in expected) {
|
||||
if (expected.hasOwnProperty(k)) {
|
||||
expect(obj[k]).toEqual(expected[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var MatrixClient = sdk.MatrixClient;
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend;
|
||||
var userId = "@alice:localhost";
|
||||
var userB = "@bob:localhost";
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!foo:bar";
|
||||
var syncData = {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const syncData = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {},
|
||||
rooms: {
|
||||
@@ -20,45 +25,46 @@ describe("MatrixClient opts", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "hello"
|
||||
})
|
||||
room: roomId, user: userB, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1"
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
name: "Old room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userB, name: "Bob"
|
||||
room: roomId, mship: "join", user: userB, name: "Bob",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: "Alice"
|
||||
room: roomId, mship: "join", user: userId, name: "Alice",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
creator: userId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("without opts.store", function() {
|
||||
@@ -69,7 +75,7 @@ describe("MatrixClient opts", function() {
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
scheduler: new sdk.MatrixScheduler()
|
||||
scheduler: new sdk.MatrixScheduler(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,9 +84,9 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
it("should be able to send messages", function(done) {
|
||||
var eventId = "$flibble:wibble";
|
||||
const eventId = "$flibble:wibble";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId
|
||||
event_id: eventId,
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
@@ -89,33 +95,32 @@ describe("MatrixClient opts", function() {
|
||||
httpBackend.flush("/txn1", 1);
|
||||
});
|
||||
|
||||
it("should be able to sync / get new events", function(done) {
|
||||
var expectedEventTypes = [ // from /initialSync
|
||||
it("should be able to sync / get new events", async function() {
|
||||
const expectedEventTypes = [ // from /initialSync
|
||||
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
|
||||
"m.room.create"
|
||||
"m.room.create",
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1, "Recv unexpected event type: " + event.getType()
|
||||
expect(expectedEventTypes.indexOf(event.getType())).toNotEqual(
|
||||
-1, "Recv unexpected event type: " + event.getType(),
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
expectedEventTypes.indexOf(event.getType()), 1
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
);
|
||||
});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
client.startClient();
|
||||
httpBackend.flush("/pushrules", 1).then(function() {
|
||||
return httpBackend.flush("/filter", 1);
|
||||
}).then(function() {
|
||||
return httpBackend.flush("/sync", 1);
|
||||
}).done(function() {
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes
|
||||
);
|
||||
done();
|
||||
});
|
||||
await client.startClient();
|
||||
await httpBackend.flush("/pushrules", 1);
|
||||
await httpBackend.flush("/filter", 1);
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,18 +128,18 @@ describe("MatrixClient opts", function() {
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
store: new sdk.MatrixInMemoryStore(),
|
||||
store: new sdk.MemoryStore(),
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
scheduler: undefined
|
||||
scheduler: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").fail(500, {
|
||||
errcode: "M_SOMETHING",
|
||||
error: "Ruh roh"
|
||||
error: "Ruh roh",
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
|
||||
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
|
||||
@@ -147,13 +152,13 @@ describe("MatrixClient opts", function() {
|
||||
|
||||
it("shouldn't queue events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "AAA"
|
||||
event_id: "AAA",
|
||||
});
|
||||
httpBackend.when("PUT", "/txn2").respond(200, {
|
||||
event_id: "BBB"
|
||||
event_id: "BBB",
|
||||
});
|
||||
var sentA = false;
|
||||
var sentB = false;
|
||||
let sentA = false;
|
||||
let sentB = false;
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
|
||||
sentA = true;
|
||||
expect(sentB).toBe(true);
|
||||
@@ -171,7 +176,7 @@ describe("MatrixClient opts", function() {
|
||||
|
||||
it("should be able to send messages", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "foo"
|
||||
event_id: "foo",
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
|
||||
expect(res.event_id).toEqual("foo");
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var EventStatus = sdk.EventStatus;
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const EventStatus = sdk.EventStatus;
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend;
|
||||
var scheduler;
|
||||
var userId = "@alice:localhost";
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!room:here";
|
||||
var room;
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let scheduler;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
scheduler = new sdk.MatrixScheduler();
|
||||
@@ -31,6 +36,7 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function() {
|
||||
@@ -49,28 +55,31 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
||||
// send a couple of events; the second will be queued
|
||||
var ev1, ev2;
|
||||
client.sendMessage(roomId, "m1").then(function(ev) {
|
||||
expect(ev).toEqual(ev1);
|
||||
});
|
||||
client.sendMessage(roomId, "m2").then(function(ev) {
|
||||
expect(ev).toEqual(ev2);
|
||||
const p1 = client.sendMessage(roomId, "m1").then(function(ev) {
|
||||
// we expect the first message to fail
|
||||
throw new Error('Message 1 unexpectedly sent successfully');
|
||||
}, (e) => {
|
||||
// this is expected
|
||||
});
|
||||
|
||||
// XXX: it turns out that the promise returned by this message
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client.sendMessage(roomId, "m2");
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
var tl = room.getLiveTimeline().getEvents();
|
||||
const tl = room.getLiveTimeline().getEvents();
|
||||
expect(tl.length).toEqual(2);
|
||||
ev1 = tl[0];
|
||||
ev2 = tl[1];
|
||||
const ev1 = tl[0];
|
||||
const ev2 = tl[1];
|
||||
|
||||
expect(ev1.status).toEqual(EventStatus.SENDING);
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// give the reactor a chance to run, so that ev2 gets queued
|
||||
q().then(function() {
|
||||
// the first message should get sent, and the second should get queued
|
||||
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
|
||||
// ev2 should now have been queued
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
@@ -80,13 +89,18 @@ describe("MatrixClient retrying", function() {
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() { client.cancelPendingEvent(ev1); })
|
||||
.toThrow();
|
||||
expect(function() {
|
||||
client.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
// fail the first send
|
||||
httpBackend.when("PUT", "/send/m.room.message/")
|
||||
.respond(400);
|
||||
return httpBackend.flush();
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
if(ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}).then(function() {
|
||||
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
|
||||
expect(tl.length).toEqual(1);
|
||||
@@ -95,7 +109,13 @@ describe("MatrixClient retrying", function() {
|
||||
client.cancelPendingEvent(ev1);
|
||||
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(0);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
p1,
|
||||
p3,
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("resending", function() {
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var EventStatus = sdk.EventStatus;
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const EventStatus = sdk.EventStatus;
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend;
|
||||
var userId = "@alice:localhost";
|
||||
var userName = "Alice";
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!foo:bar";
|
||||
var otherUserId = "@bob:localhost";
|
||||
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
var ROOM_NAME_EVENT = utils.mkEvent({
|
||||
const ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
name: "Old room name",
|
||||
},
|
||||
});
|
||||
var NEXT_SYNC_DATA;
|
||||
var SYNC_DATA = {
|
||||
let NEXT_SYNC_DATA;
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
@@ -30,30 +35,30 @@ describe("MatrixClient room timelines", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello"
|
||||
})
|
||||
room: roomId, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "f_1_1"
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob"
|
||||
user: otherUserId, name: "Bob",
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
creator: userId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function setNextSyncData(events) {
|
||||
@@ -67,11 +72,11 @@ describe("MatrixClient room timelines", function() {
|
||||
"!foo:bar": {
|
||||
timeline: { events: [] },
|
||||
state: { events: [] },
|
||||
ephemeral: { events: [] }
|
||||
}
|
||||
ephemeral: { events: [] },
|
||||
},
|
||||
},
|
||||
leave: {}
|
||||
}
|
||||
leave: {},
|
||||
},
|
||||
};
|
||||
events.forEach(function(e) {
|
||||
if (e.room_id !== roomId) {
|
||||
@@ -81,7 +86,7 @@ describe("MatrixClient room timelines", function() {
|
||||
if (e.__prev_event === undefined) {
|
||||
throw new Error(
|
||||
"setNextSyncData needs the prev state set to '__prev_event' " +
|
||||
"for " + e.type
|
||||
"for " + e.type,
|
||||
);
|
||||
}
|
||||
if (e.__prev_event !== null) {
|
||||
@@ -90,18 +95,16 @@ describe("MatrixClient room timelines", function() {
|
||||
}
|
||||
// push the current
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
}
|
||||
else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
|
||||
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -121,21 +124,23 @@ describe("MatrixClient room timelines", function() {
|
||||
client.startClient();
|
||||
httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/filter");
|
||||
}).done(done);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("local echo events", function() {
|
||||
|
||||
it("should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
@@ -144,7 +149,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
var member = room.timeline[1].sender;
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member.userId).toEqual(userId);
|
||||
expect(member.name).toEqual(userName);
|
||||
|
||||
@@ -157,21 +162,23 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream", function(done) {
|
||||
var eventId = "$foo:bar";
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
var ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
@@ -187,22 +194,24 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"AFTER the event comes down the event stream", function(done) {
|
||||
var eventId = "$foo:bar";
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
var ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId
|
||||
const ev = utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
@@ -212,15 +221,14 @@ describe("MatrixClient room timelines", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginated events", function() {
|
||||
var sbEvents;
|
||||
var sbEndTok = "pagin_end";
|
||||
let sbEvents;
|
||||
const sbEndTok = "pagin_end";
|
||||
|
||||
beforeEach(function() {
|
||||
sbEvents = [];
|
||||
@@ -228,7 +236,7 @@ describe("MatrixClient room timelines", function() {
|
||||
return {
|
||||
chunk: sbEvents,
|
||||
start: "pagin_start",
|
||||
end: sbEndTok
|
||||
end: sbEndTok,
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -236,18 +244,23 @@ describe("MatrixClient room timelines", function() {
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).done(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBeNull();
|
||||
done();
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
@@ -263,55 +276,60 @@ describe("MatrixClient room timelines", function() {
|
||||
// <Bob> hello
|
||||
|
||||
// make an m.room.member event for alice's join
|
||||
var joinMshipEvent = utils.mkMembership({
|
||||
const joinMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
||||
url: null
|
||||
url: null,
|
||||
});
|
||||
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
// change
|
||||
var oldMshipEvent = utils.mkMembership({
|
||||
const oldMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: userName,
|
||||
url: "mxc://some/url"
|
||||
url: "mxc://some/url",
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: null,
|
||||
membership: "join"
|
||||
membership: "join",
|
||||
};
|
||||
|
||||
// set the list of events to return on scrollback (/messages)
|
||||
// N.B. synapse returns /messages in reverse chronological order
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm alice"
|
||||
user: userId, room: roomId, msg: "I'm alice",
|
||||
}),
|
||||
oldMshipEvent,
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm old alice"
|
||||
user: userId, room: roomId, msg: "I'm old alice",
|
||||
}),
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).done(function() {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
var joinMsg = room.timeline[0];
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||
var oldMsg = room.timeline[1];
|
||||
const oldMsg = room.timeline[1];
|
||||
expect(oldMsg.sender.name).toEqual("Old Alice");
|
||||
var newMsg = room.timeline[3];
|
||||
const newMsg = room.timeline[3];
|
||||
expect(newMsg.sender.name).toEqual(userName);
|
||||
done();
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
@@ -320,27 +338,32 @@ describe("MatrixClient room timelines", function() {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am new"
|
||||
user: userId, room: roomId, msg: "I am new",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am old"
|
||||
})
|
||||
user: userId, room: roomId, msg: "I am old",
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).done(function() {
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
done();
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
@@ -349,22 +372,26 @@ describe("MatrixClient room timelines", function() {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I am new"
|
||||
})
|
||||
user: userId, room: roomId, msg: "I am new",
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.oldState.paginationToken).toBeDefined();
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client.scrollback(room, 1).done(function() {
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend.flush("/messages", 1).done(function() {
|
||||
done();
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
@@ -372,18 +399,20 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
|
||||
describe("new events", function() {
|
||||
it("should be added to the right place in the timeline", function(done) {
|
||||
var eventData = [
|
||||
it("should be added to the right place in the timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({user: userId, room: roomId})
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
var index = 0;
|
||||
let index = 0;
|
||||
client.on("Room.timeline", function(event, rm, toStart) {
|
||||
expect(toStart).toBe(false);
|
||||
expect(rm).toEqual(room);
|
||||
@@ -392,172 +421,194 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(index).toEqual(2);
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[2].event).toEqual(
|
||||
eventData[1]
|
||||
eventData[1],
|
||||
);
|
||||
expect(room.timeline[1].event).toEqual(
|
||||
eventData[0]
|
||||
eventData[0],
|
||||
);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
var eventData = [
|
||||
it("should set the right event.sender values", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", name: "New Name"
|
||||
user: userId, room: roomId, mship: "join", name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({user: userId, room: roomId})
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
];
|
||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
var preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
var postNameEvent = room.timeline[room.timeline.length - 1];
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
const preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
const postNameEvent = room.timeline[room.timeline.length - 1];
|
||||
expect(preNameEvent.sender.name).toEqual(userName);
|
||||
expect(postNameEvent.sender.name).toEqual("New Name");
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right room.name", function(done) {
|
||||
var secondRoomNameEvent = utils.mkEvent({
|
||||
it("should set the right room.name", function() {
|
||||
const secondRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 2"
|
||||
}
|
||||
name: "Room 2",
|
||||
},
|
||||
});
|
||||
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
|
||||
setNextSyncData([secondRoomNameEvent]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
var nameEmitCount = 0;
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
let nameEmitCount = 0;
|
||||
client.on("Room.name", function(rm) {
|
||||
nameEmitCount += 1;
|
||||
});
|
||||
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(nameEmitCount).toEqual(1);
|
||||
expect(room.name).toEqual("Room 2");
|
||||
// do another round
|
||||
var thirdRoomNameEvent = utils.mkEvent({
|
||||
const thirdRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 3"
|
||||
}
|
||||
name: "Room 3",
|
||||
},
|
||||
});
|
||||
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
|
||||
setNextSyncData([thirdRoomNameEvent]);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
expect(nameEmitCount).toEqual(2);
|
||||
expect(room.name).toEqual("Room 3");
|
||||
done();
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(nameEmitCount).toEqual(2);
|
||||
expect(room.name).toEqual("Room 3");
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right room members", function(done) {
|
||||
var userC = "@cee:bar";
|
||||
var userD = "@dee:bar";
|
||||
var eventData = [
|
||||
it("should set the right room members", function() {
|
||||
const userC = "@cee:bar";
|
||||
const userD = "@dee:bar";
|
||||
const eventData = [
|
||||
utils.mkMembership({
|
||||
user: userC, room: roomId, mship: "join", name: "C"
|
||||
user: userC, room: roomId, mship: "join", name: "C",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, room: roomId, mship: "invite", skey: userD
|
||||
})
|
||||
user: userC, room: roomId, mship: "invite", skey: userD,
|
||||
}),
|
||||
];
|
||||
eventData[0].__prev_event = null;
|
||||
eventData[1].__prev_event = null;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC).name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC).membership).toEqual(
|
||||
"join"
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(userD).name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD).membership).toEqual(
|
||||
"invite"
|
||||
"invite",
|
||||
);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gappy sync", function() {
|
||||
it("should copy the last known state to the new timeline", function(done) {
|
||||
var eventData = [
|
||||
it("should copy the last known state to the new timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||
expect(room.currentState.getMembers().length).toEqual(2);
|
||||
expect(room.currentState.getMember(userId).name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId).membership).toEqual(
|
||||
"join"
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId).membership).toEqual(
|
||||
"join"
|
||||
"join",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should emit a 'Room.timelineReset' event", function(done) {
|
||||
var eventData = [
|
||||
it("should emit a 'Room.timelineReset' event", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
client.on("Room.timelineReset", function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var MatrixEvent = sdk.MatrixEvent;
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend;
|
||||
var selfUserId = "@alice:localhost";
|
||||
var selfAccessToken = "aseukfgwef";
|
||||
var otherUserId = "@bob:localhost";
|
||||
var userA = "@alice:bar";
|
||||
var userB = "@bob:bar";
|
||||
var userC = "@claire:bar";
|
||||
var roomOne = "!foo:localhost";
|
||||
var roomTwo = "!bar:localhost";
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const otherUserId = "@bob:localhost";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bob:bar";
|
||||
const userC = "@claire:bar";
|
||||
const roomOne = "!foo:localhost";
|
||||
const roomTwo = "!bar:localhost";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: selfUserId,
|
||||
accessToken: selfAccessToken
|
||||
accessToken: selfAccessToken,
|
||||
});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
@@ -33,13 +38,14 @@ describe("MatrixClient syncing", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("startClient", function() {
|
||||
var syncData = {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {},
|
||||
presence: {}
|
||||
presence: {},
|
||||
};
|
||||
|
||||
it("should /sync after /pushrules and /filter.", function(done) {
|
||||
@@ -47,7 +53,7 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
httpBackend.flushAllExpected().done(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -61,24 +67,23 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
httpBackend.flushAllExpected().done(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolving invites to profile info", function() {
|
||||
|
||||
var syncData = {
|
||||
const syncData = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: []
|
||||
events: [],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -87,98 +92,103 @@ describe("MatrixClient syncing", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
})
|
||||
]
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId
|
||||
room: roomOne, mship: "join", user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId
|
||||
room: roomOne, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("should resolve incoming invites from /sync", function(done) {
|
||||
it("should resolve incoming invites from /sync", function() {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC
|
||||
})
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
|
||||
200, {
|
||||
avatar_url: "mxc://flibble/wibble",
|
||||
displayname: "The Boss"
|
||||
}
|
||||
displayname: "The Boss",
|
||||
},
|
||||
);
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var member = client.getRoom(roomOne).getMember(userC);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const member = client.getRoom(roomOne).getMember(userC);
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false)
|
||||
).toBeDefined();
|
||||
done();
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use cached values from m.presence wherever possible", function(done) {
|
||||
it("should use cached values from m.presence wherever possible", function() {
|
||||
syncData.presence.events = [
|
||||
utils.mkPresence({
|
||||
user: userC, presence: "online", name: "The Ghost"
|
||||
user: userC, presence: "online", name: "The Ghost",
|
||||
}),
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC
|
||||
})
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var member = client.getRoom(roomOne).getMember(userC);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const member = client.getRoom(roomOne).getMember(userC);
|
||||
expect(member.name).toEqual("The Ghost");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should result in events on the room member firing", function(done) {
|
||||
it("should result in events on the room member firing", function() {
|
||||
syncData.presence.events = [
|
||||
utils.mkPresence({
|
||||
user: userC, presence: "online", name: "The Ghost"
|
||||
})
|
||||
user: userC, presence: "online", name: "The Ghost",
|
||||
}),
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC
|
||||
})
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
var latestFiredName = null;
|
||||
let latestFiredName = null;
|
||||
client.on("RoomMember.name", function(event, m) {
|
||||
if (m.userId === userC && m.roomId === roomOne) {
|
||||
latestFiredName = m.name;
|
||||
@@ -186,141 +196,147 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
expect(latestFiredName).toEqual("The Ghost");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
|
||||
it("should no-op if resolveInvitesToProfiles is not set", function() {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC
|
||||
})
|
||||
room: roomOne, mship: "invite", user: userC,
|
||||
}),
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var member = client.getRoom(roomOne).getMember(userC);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const member = client.getRoom(roomOne).getMember(userC);
|
||||
expect(member.name).toEqual(userC);
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false)
|
||||
).toBeNull();
|
||||
done();
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false),
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("users", function() {
|
||||
var syncData = {
|
||||
const syncData = {
|
||||
next_batch: "nb",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: userA, presence: "online"
|
||||
user: userA, presence: "online",
|
||||
}),
|
||||
utils.mkPresence({
|
||||
user: userB, presence: "unavailable"
|
||||
})
|
||||
]
|
||||
}
|
||||
user: userB, presence: "unavailable",
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should create users for presence events from /sync",
|
||||
function(done) {
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
expect(client.getUser(userA).presence).toEqual("online");
|
||||
expect(client.getUser(userB).presence).toEqual("unavailable");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("room state", function() {
|
||||
var msgText = "some text here";
|
||||
var otherDisplayName = "Bob Smith";
|
||||
const msgText = "some text here";
|
||||
const otherDisplayName = "Bob Smith";
|
||||
|
||||
var syncData = {
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
})
|
||||
]
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
name: "Old room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId
|
||||
room: roomOne, mship: "join", user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId
|
||||
room: roomOne, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hiii"
|
||||
})
|
||||
]
|
||||
room: roomTwo, user: otherUserId, msg: "hiii",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: otherUserId,
|
||||
name: otherDisplayName
|
||||
name: otherDisplayName,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: selfUserId
|
||||
room: roomTwo, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomTwo, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
var nextSyncData = {
|
||||
const nextSyncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
nextSyncData.rooms.join[roomOne] = {
|
||||
@@ -328,89 +344,125 @@ describe("MatrixClient syncing", function() {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: selfUserId,
|
||||
content: { name: "A new room name" }
|
||||
})
|
||||
]
|
||||
}
|
||||
content: { name: "A new room name" },
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
nextSyncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: msgText
|
||||
})
|
||||
]
|
||||
room: roomTwo, user: otherUserId, msg: msgText,
|
||||
}),
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: roomTwo,
|
||||
content: { user_ids: [otherUserId] }
|
||||
})
|
||||
]
|
||||
}
|
||||
content: { user_ids: [otherUserId] },
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should continually recalculate the right room name.", function(done) {
|
||||
it("should continually recalculate the right room name.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
// should have clobbered the name to the one from /events
|
||||
expect(room.name).toEqual(
|
||||
nextSyncData.rooms.join[roomOne].state.events[0].content.name
|
||||
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should store the right events in the timeline.", function(done) {
|
||||
it("should store the right events in the timeline.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
// should have added the message from /events
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getContent().body).toEqual(msgText);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right room name.", function(done) {
|
||||
it("should set the right room name.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
// should use the display name of the other person.
|
||||
expect(room.name).toEqual(otherDisplayName);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the right user's typing flag.", function(done) {
|
||||
it("should set the right user's typing flag.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
var member = room.getMember(otherUserId);
|
||||
expect(member).toBeDefined();
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
let member = room.getMember(otherUserId);
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(true);
|
||||
member = room.getMember(selfUserId);
|
||||
expect(member).toBeDefined();
|
||||
expect(member).toBeTruthy();
|
||||
expect(member.typing).toEqual(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// XXX: This test asserts that the js-sdk obeys the spec and treats state
|
||||
// events that arrive in the incremental sync as if they preceeded the
|
||||
// timeline events, however this breaks peeking, so it's disabled
|
||||
// (see sync.js)
|
||||
xit("should correctly interpret state in incremental sync.", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(2),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
const stateAtStart = room.getLiveTimeline().getState(
|
||||
EventTimeline.BACKWARDS,
|
||||
);
|
||||
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
|
||||
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
|
||||
|
||||
const stateAtEnd = room.getLiveTimeline().getState(
|
||||
EventTimeline.FORWARDS,
|
||||
);
|
||||
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
|
||||
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -425,7 +477,7 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
describe("timeline", function() {
|
||||
beforeEach(function() {
|
||||
var syncData = {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
@@ -435,7 +487,7 @@ describe("MatrixClient syncing", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
@@ -445,11 +497,14 @@ describe("MatrixClient syncing", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
httpBackend.flush();
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on new rooms", function(done) {
|
||||
var syncData = {
|
||||
it("should set the back-pagination token on new rooms", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
@@ -459,7 +514,7 @@ describe("MatrixClient syncing", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "roomtwo"
|
||||
room: roomTwo, user: otherUserId, msg: "roomtwo",
|
||||
}),
|
||||
],
|
||||
prev_batch: "roomtwotok",
|
||||
@@ -468,17 +523,20 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
var tok = room.getLiveTimeline()
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
expect(room).toExist();
|
||||
const tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("roomtwotok");
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on gappy syncs", function(done) {
|
||||
var syncData = {
|
||||
it("should set the back-pagination token on gappy syncs", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
@@ -488,7 +546,7 @@ describe("MatrixClient syncing", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world"
|
||||
room: roomOne, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
@@ -497,104 +555,108 @@ describe("MatrixClient syncing", function() {
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
var resetCallCount = 0;
|
||||
let resetCallCount = 0;
|
||||
// the token should be set *before* timelineReset is emitted
|
||||
client.on("Room.timelineReset", function(room) {
|
||||
resetCallCount++;
|
||||
|
||||
var tl = room.getLiveTimeline();
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(0);
|
||||
var tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("newerTok");
|
||||
});
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
var tl = room.getLiveTimeline();
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(resetCallCount).toEqual(1);
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("receipts", function() {
|
||||
var syncData = {
|
||||
const syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
room: roomOne, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world"
|
||||
})
|
||||
]
|
||||
room: roomOne, user: otherUserId, msg: "world",
|
||||
}),
|
||||
],
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
name: "Old room name",
|
||||
},
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId
|
||||
room: roomOne, mship: "join", user: otherUserId,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId
|
||||
room: roomOne, mship: "join", user: selfUserId,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
creator: selfUserId,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
syncData.rooms.join[roomOne].ephemeral = {
|
||||
events: []
|
||||
events: [],
|
||||
};
|
||||
});
|
||||
|
||||
it("should sync receipts from /sync.", function(done) {
|
||||
var ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
|
||||
var receipt = {};
|
||||
it("should sync receipts from /sync.", function() {
|
||||
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
|
||||
const receipt = {};
|
||||
receipt[ackEvent.event_id] = {
|
||||
"m.read": {}
|
||||
"m.read": {},
|
||||
};
|
||||
receipt[ackEvent.event_id]["m.read"][userC] = {
|
||||
ts: 176592842636
|
||||
ts: 176592842636,
|
||||
};
|
||||
syncData.rooms.join[roomOne].ephemeral.events = [{
|
||||
content: receipt,
|
||||
room_id: roomOne,
|
||||
type: "m.receipt"
|
||||
type: "m.receipt",
|
||||
}];
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomOne);
|
||||
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
|
||||
type: "m.read",
|
||||
userId: userC,
|
||||
data: {
|
||||
ts: 176592842636
|
||||
}
|
||||
ts: 176592842636,
|
||||
},
|
||||
}]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -614,7 +676,7 @@ describe("MatrixClient syncing", function() {
|
||||
beforeEach(function(done) {
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
httpBackend.flushAllExpected().then(function() {
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
@@ -624,33 +686,38 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and use an appropriate filter", function(done) {
|
||||
it("should create and use an appropriate filter", function() {
|
||||
httpBackend.when("POST", "/filter").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
room: { timeline: {limit: 1},
|
||||
include_leave: true }});
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
const defer = Promise.defer();
|
||||
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
done();
|
||||
defer.resolve();
|
||||
}).respond(200, {});
|
||||
|
||||
client.syncLeftRooms();
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
// flush the syncs
|
||||
return httpBackend.flush();
|
||||
}).catch(utils.failTest);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
// flush the syncs
|
||||
return httpBackend.flushAllExpected();
|
||||
}),
|
||||
defer.promise,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on left rooms", function(done) {
|
||||
var syncData = {
|
||||
it("should set the back-pagination token on left rooms", function() {
|
||||
const syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
leave: {}
|
||||
leave: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -658,7 +725,7 @@ describe("MatrixClient syncing", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hello"
|
||||
room: roomTwo, user: otherUserId, msg: "hello",
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
@@ -666,25 +733,36 @@ describe("MatrixClient syncing", function() {
|
||||
};
|
||||
|
||||
httpBackend.when("POST", "/filter").respond(200, {
|
||||
filter_id: "another_id"
|
||||
filter_id: "another_id",
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.syncLeftRooms().then(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
var tok = room.getLiveTimeline().getPaginationToken(
|
||||
EventTimeline.BACKWARDS);
|
||||
return Promise.all([
|
||||
client.syncLeftRooms().then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
const tok = room.getLiveTimeline().getPaginationToken(
|
||||
EventTimeline.BACKWARDS);
|
||||
|
||||
expect(tok).toEqual("pagTok");
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
expect(tok).toEqual("pagTok");
|
||||
}),
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
return httpBackend.flush();
|
||||
}).catch(utils.failTest);
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
return httpBackend.flushAllExpected();
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* waits for the MatrixClient to emit one or more 'sync' events.
|
||||
*
|
||||
* @param {Number?} numSyncs number of syncs to wait for
|
||||
* @returns {Promise} promise which resolves after the sync events have happened
|
||||
*/
|
||||
function awaitSyncEvent(numSyncs) {
|
||||
return utils.syncPromise(client, numSyncs);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,974 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
const anotherjson = require('another-json');
|
||||
import Promise from 'bluebird';
|
||||
import expect from 'expect';
|
||||
|
||||
const utils = require('../../lib/utils');
|
||||
const testUtils = require('../test-utils');
|
||||
const TestClient = require('../TestClient').default;
|
||||
import logger from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* start an Olm session with a given recipient
|
||||
*
|
||||
* @param {Olm.Account} olmAccount
|
||||
* @param {TestClient} recipientTestClient
|
||||
* @return {Promise} promise for Olm.Session
|
||||
*/
|
||||
function createOlmSession(olmAccount, recipientTestClient) {
|
||||
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
const otkId = utils.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
session.create_outbound(
|
||||
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
|
||||
);
|
||||
return session;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt an event with olm
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string=} opts.sender
|
||||
* @param {string} opts.senderKey
|
||||
* @param {Olm.Session} opts.p2pSession
|
||||
* @param {TestClient} opts.recipient
|
||||
* @param {object=} opts.plaincontent
|
||||
* @param {string=} opts.plaintype
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptOlmEvent(opts) {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.p2pSession).toBeTruthy();
|
||||
expect(opts.recipient).toBeTruthy();
|
||||
|
||||
const plaintext = {
|
||||
content: opts.plaincontent || {},
|
||||
recipient: opts.recipient.userId,
|
||||
recipient_keys: {
|
||||
ed25519: opts.recipient.getSigningKey(),
|
||||
},
|
||||
sender: opts.sender || '@bob:xyz',
|
||||
type: opts.plaintype || 'm.test',
|
||||
};
|
||||
|
||||
const event = {
|
||||
content: {
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
ciphertext: {},
|
||||
sender_key: opts.senderKey,
|
||||
},
|
||||
sender: opts.sender || '@bob:xyz',
|
||||
type: 'm.room.encrypted',
|
||||
};
|
||||
event.content.ciphertext[opts.recipient.getDeviceKey()] =
|
||||
opts.p2pSession.encrypt(JSON.stringify(plaintext));
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt an event with megolm
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.senderKey
|
||||
* @param {Olm.OutboundGroupSession} opts.groupSession
|
||||
* @param {object=} opts.plaintext
|
||||
* @param {string=} opts.room_id
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptMegolmEvent(opts) {
|
||||
expect(opts.senderKey).toBeTruthy();
|
||||
expect(opts.groupSession).toBeTruthy();
|
||||
|
||||
const plaintext = opts.plaintext || {};
|
||||
if (!plaintext.content) {
|
||||
plaintext.content = {
|
||||
body: '42',
|
||||
msgtype: "m.text",
|
||||
};
|
||||
}
|
||||
if (!plaintext.type) {
|
||||
plaintext.type = "m.room.message";
|
||||
}
|
||||
if (!plaintext.room_id) {
|
||||
expect(opts.room_id).toBeTruthy();
|
||||
plaintext.room_id = opts.room_id;
|
||||
}
|
||||
|
||||
return {
|
||||
event_id: 'test_megolm_event',
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
|
||||
device_id: "testDevice",
|
||||
sender_key: opts.senderKey,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* build an encrypted room_key event to share a group session
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.senderKey
|
||||
* @param {TestClient} opts.recipient
|
||||
* @param {Olm.Session} opts.p2pSession
|
||||
* @param {Olm.OutboundGroupSession} opts.groupSession
|
||||
* @param {string=} opts.room_id
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptGroupSessionKey(opts) {
|
||||
return encryptOlmEvent({
|
||||
senderKey: opts.senderKey,
|
||||
recipient: opts.recipient,
|
||||
p2pSession: opts.p2pSession,
|
||||
plaincontent: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: opts.room_id,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
session_key: opts.groupSession.session_key(),
|
||||
},
|
||||
plaintype: 'm.room_key',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single room (ROOM_ID),
|
||||
* with the members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
const roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: roomMembers[i],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = roomResponse;
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running megolm tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount;
|
||||
let testSenderKey;
|
||||
let aliceTestClient;
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
* response to /keys/query
|
||||
*
|
||||
* @param {string} userId The user ID to query for
|
||||
* @returns {Object} The fake query response
|
||||
*/
|
||||
function getTestKeysQueryResponse(userId) {
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
const testDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'DEVICE_ID',
|
||||
keys: {
|
||||
'curve25519:DEVICE_ID': testE2eKeys.curve25519,
|
||||
'ed25519:DEVICE_ID': testE2eKeys.ed25519,
|
||||
},
|
||||
user_id: userId,
|
||||
};
|
||||
const j = anotherjson.stringify(testDeviceKeys);
|
||||
const sig = testOlmAccount.sign(j);
|
||||
testDeviceKeys.signatures = {};
|
||||
testDeviceKeys.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
const queryResponse = {
|
||||
device_keys: {},
|
||||
};
|
||||
|
||||
queryResponse.device_keys[userId] = {
|
||||
'DEVICE_ID': testDeviceKeys,
|
||||
};
|
||||
|
||||
return queryResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a one-time key for testOlmAccount in a format suitable for a
|
||||
* response to /keys/claim
|
||||
|
||||
* @param {string} userId The user ID to query for
|
||||
* @returns {Object} The fake key claim response
|
||||
*/
|
||||
function getTestKeysClaimResponse(userId) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
const keyId = utils.keys(testOneTimeKeys.curve25519)[0];
|
||||
const oneTimeKey = testOneTimeKeys.curve25519[keyId];
|
||||
const keyResult = {
|
||||
'key': oneTimeKey,
|
||||
};
|
||||
const j = anotherjson.stringify(keyResult);
|
||||
const sig = testOlmAccount.sign(j);
|
||||
keyResult.signatures = {};
|
||||
keyResult.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
const claimResponse = {one_time_keys: {}};
|
||||
claimResponse.one_time_keys[userId] = {
|
||||
'DEVICE_ID': {},
|
||||
};
|
||||
claimResponse.one_time_keys[userId].DEVICE_ID['signed_curve25519:' + keyId] =
|
||||
keyResult;
|
||||
return claimResponse;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
await aliceTestClient.client.initCrypto();
|
||||
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", function() {
|
||||
return aliceTestClient.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
const messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message before the session keys", function() {
|
||||
// https://github.com/vector-im/riot-web/issues/2273
|
||||
let roomKeyEncrypted;
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
const messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice just gets the message event to start with
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().msgtype).toEqual('m.bad.encrypted');
|
||||
|
||||
// now she gets the room_key event
|
||||
const syncResponse = {
|
||||
next_batch: 2,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent().msgtype != 'm.bad.encrypted') {
|
||||
return event;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}).then((event) => {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice gets a second room_key message", function() {
|
||||
return aliceTestClient.start().then(() => {
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
const messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// make a second room_key event now that we have advanced the group
|
||||
// session.
|
||||
const roomKeyEncrypted2 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// on the first sync, send the best room key
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted1],
|
||||
},
|
||||
});
|
||||
|
||||
// on the second sync, send the advanced room key, along with the
|
||||
// message. This simulates the situation where Alice has been sent a
|
||||
// later copy of the room key and is reloading the client.
|
||||
const syncResponse2 = {
|
||||
next_batch: 2,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted2],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse2.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
|
||||
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it('Alice sends a megolm message', function() {
|
||||
let p2pSession;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((_p2pSession) => {
|
||||
p2pSession = _p2pSession;
|
||||
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
// start out with the device unknown - the send should be rejected.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||
throw new Error("sendTextMessage failed on an unknown device");
|
||||
}, (e) => {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
}),
|
||||
aliceTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
}).then(function() {
|
||||
// mark the device as known, and resend.
|
||||
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
let inboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
const ct = content.ciphertext;
|
||||
const r = inboundGroupSession.decrypt(ct);
|
||||
logger.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
const decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const pendingMsg = room.getPendingEvents()[0];
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.resendEvent(pendingMsg, room),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
logger.log('Telling alice to send a megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("We should start a new megolm session when a device is blocked", function() {
|
||||
let p2pSession;
|
||||
let megolmSessionId;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((_p2pSession) => {
|
||||
p2pSession = _p2pSession;
|
||||
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log("Fetching bob's devices and marking known");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flushAllExpected(),
|
||||
]).then((keys) => {
|
||||
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
|
||||
});
|
||||
}).then(function() {
|
||||
logger.log('Telling alice to send a megolm message');
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log('sendToDevice: ', content);
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(1); // normal message
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
logger.log('decrypted sendToDevice:', decrypted);
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
megolmSessionId = decrypted.content.session_id;
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log('/send:', content);
|
||||
expect(content.session_id).toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
logger.log('Telling alice to send another megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log('/send:', content);
|
||||
expect(content.session_id).toNotEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
|
||||
aliceTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/2676
|
||||
it("Alice should send to her other devices", function() {
|
||||
// for this test, we make the testOlmAccount be another of Alice's devices.
|
||||
// it ought to get included in messages Alice sends.
|
||||
|
||||
let p2pSession;
|
||||
let inboundGroupSession;
|
||||
let decrypted;
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
// an encrypted room with just alice
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: aliceTestClient.userId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
// the completion of the first initialsync hould make Alice
|
||||
// invalidate the device cache for all members in e2e rooms (ie,
|
||||
// herself), and do a key query.
|
||||
aliceTestClient.expectKeyQuery(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId),
|
||||
);
|
||||
|
||||
return aliceTestClient.httpBackend.flushAllExpected();
|
||||
}).then(function() {
|
||||
// start out with the device unknown - the send should be rejected.
|
||||
return aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||
throw new Error("sendTextMessage failed on an unknown device");
|
||||
}, (e) => {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
|
||||
expect(Object.keys(e.devices[aliceTestClient.userId])).
|
||||
toEqual(['DEVICE_ID']);
|
||||
});
|
||||
}).then(function() {
|
||||
// mark the device as known, and resend.
|
||||
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
||||
200, function(path, content) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
||||
.toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log("sendToDevice: ", content);
|
||||
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
p2pSession = new Olm.Session();
|
||||
p2pSession.create_inbound(testOlmAccount, ct.body);
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
const ct = content.ciphertext;
|
||||
const r = inboundGroupSession.decrypt(ct);
|
||||
logger.log('Decrypted received megolm message', r);
|
||||
decrypted = JSON.parse(r.plaintext);
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
// Grab the event that we'll need to resend
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const pendingEvents = room.getPendingEvents();
|
||||
expect(pendingEvents.length).toEqual(1);
|
||||
const unsentEvent = pendingEvents[0];
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.resendEvent(unsentEvent, room),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message',
|
||||
function() {
|
||||
let downloadPromise;
|
||||
let sendPromise;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
const olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
// this will block
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
|
||||
// so will this.
|
||||
sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
||||
.then(() => {
|
||||
throw new Error("sendTextMessage failed on an unknown device");
|
||||
}, (e) => {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
return aliceTestClient.httpBackend.flushAllExpected();
|
||||
}).then(function() {
|
||||
return Promise.all([downloadPromise, sendPromise]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Alice exports megolm keys and imports them to a new device", function() {
|
||||
let messageEncrypted;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
}).then((p2pSession) => {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
|
||||
return aliceTestClient.client.exportRoomKeys();
|
||||
}).then(function(exported) {
|
||||
// start a new client
|
||||
aliceTestClient.stop();
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
return aliceTestClient.client.initCrypto().then(() => {
|
||||
aliceTestClient.client.importRoomKeys(exported);
|
||||
return aliceTestClient.start();
|
||||
});
|
||||
}).then(function() {
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,227 +0,0 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
|
||||
/**
|
||||
* Construct a mock HTTP backend, heavily inspired by Angular.js.
|
||||
* @constructor
|
||||
*/
|
||||
function HttpBackend() {
|
||||
this.requests = [];
|
||||
this.expectedRequests = [];
|
||||
var self = this;
|
||||
// the request function dependency that the SDK needs.
|
||||
this.requestFn = function(opts, callback) {
|
||||
var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
|
||||
realReq.callback = callback;
|
||||
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
|
||||
self.requests.push(realReq);
|
||||
|
||||
var abort = function() {
|
||||
var idx = self.requests.indexOf(realReq);
|
||||
if (idx >= 0) {
|
||||
console.log("Aborting HTTP request: %s %s", opts.method,
|
||||
opts.uri);
|
||||
self.requests.splice(idx, 1);
|
||||
realReq.callback("aborted");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
abort: abort
|
||||
};
|
||||
};
|
||||
}
|
||||
HttpBackend.prototype = {
|
||||
/**
|
||||
* Respond to all of the requests (flush the queue).
|
||||
* @param {string} path The path to flush (optional) default: all.
|
||||
* @param {integer} numToFlush The number of things to flush (optional), default: all.
|
||||
* @return {Promise} resolved when there is nothing left to flush.
|
||||
*/
|
||||
flush: function(path, numToFlush) {
|
||||
var defer = q.defer();
|
||||
var self = this;
|
||||
var flushed = 0;
|
||||
var triedWaiting = false;
|
||||
console.log(
|
||||
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
|
||||
);
|
||||
var tryFlush = function() {
|
||||
// if there's more real requests and more expected requests, flush 'em.
|
||||
console.log(
|
||||
" trying to flush queue => reqs=%s expected=%s [%s]",
|
||||
self.requests.length, self.expectedRequests.length, path
|
||||
);
|
||||
if (self._takeFromQueue(path)) {
|
||||
// try again on the next tick.
|
||||
console.log(" flushed. Trying for more. [%s]", path);
|
||||
flushed += 1;
|
||||
if (numToFlush && flushed === numToFlush) {
|
||||
console.log(" [%s] Flushed assigned amount: %s", path, numToFlush);
|
||||
defer.resolve();
|
||||
}
|
||||
else {
|
||||
setTimeout(tryFlush, 0);
|
||||
}
|
||||
}
|
||||
else if (flushed === 0 && !triedWaiting) {
|
||||
// we may not have made the request yet, wait a generous amount of
|
||||
// time before giving up.
|
||||
setTimeout(tryFlush, 5);
|
||||
triedWaiting = true;
|
||||
}
|
||||
else {
|
||||
console.log(" no more flushes. [%s]", path);
|
||||
defer.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(tryFlush, 0);
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attempts to resolve requests/expected requests.
|
||||
* @param {string} path The path to flush (optional) default: all.
|
||||
* @return {boolean} true if something was resolved.
|
||||
*/
|
||||
_takeFromQueue: function(path) {
|
||||
var req = null;
|
||||
var i, j;
|
||||
var matchingReq, expectedReq, testResponse = null;
|
||||
for (i = 0; i < this.requests.length; i++) {
|
||||
req = this.requests[i];
|
||||
for (j = 0; j < this.expectedRequests.length; j++) {
|
||||
expectedReq = this.expectedRequests[j];
|
||||
if (path && path !== expectedReq.path) { continue; }
|
||||
if (expectedReq.method === req.method &&
|
||||
req.path.indexOf(expectedReq.path) !== -1) {
|
||||
if (!expectedReq.data || (JSON.stringify(expectedReq.data) ===
|
||||
JSON.stringify(req.data))) {
|
||||
matchingReq = expectedReq;
|
||||
this.expectedRequests.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingReq) {
|
||||
// remove from request queue
|
||||
this.requests.splice(i, 1);
|
||||
i--;
|
||||
|
||||
for (j = 0; j < matchingReq.checks.length; j++) {
|
||||
matchingReq.checks[j](req);
|
||||
}
|
||||
testResponse = matchingReq.response;
|
||||
console.log(" responding to %s", matchingReq.path);
|
||||
var body = testResponse.body;
|
||||
if (Object.prototype.toString.call(body) == "[object Function]") {
|
||||
body = body(req.path, req.data);
|
||||
}
|
||||
req.callback(
|
||||
testResponse.err, testResponse.response, body
|
||||
);
|
||||
matchingReq = null;
|
||||
}
|
||||
}
|
||||
if (testResponse) { // flushed something
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes sure that the SDK hasn't sent any more requests to the backend.
|
||||
*/
|
||||
verifyNoOutstandingRequests: function() {
|
||||
var firstOutstandingReq = this.requests[0] || {};
|
||||
expect(this.requests.length).toEqual(0,
|
||||
"Expected no more HTTP requests but received request to " +
|
||||
firstOutstandingReq.path
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes sure that the test doesn't have any unresolved requests.
|
||||
*/
|
||||
verifyNoOutstandingExpectation: function() {
|
||||
var firstOutstandingExpectation = this.expectedRequests[0] || {};
|
||||
expect(this.expectedRequests.length).toEqual(0,
|
||||
"Expected to see HTTP request for " + firstOutstandingExpectation.path
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create an expected request.
|
||||
* @param {string} method The HTTP method
|
||||
* @param {string} path The path (which can be partial)
|
||||
* @param {Object} data The expected data.
|
||||
* @return {Request} An expected request.
|
||||
*/
|
||||
when: function(method, path, data) {
|
||||
var pendingReq = new Request(method, path, data);
|
||||
this.expectedRequests.push(pendingReq);
|
||||
return pendingReq;
|
||||
}
|
||||
};
|
||||
|
||||
function Request(method, path, data, queryParams) {
|
||||
this.method = method;
|
||||
this.path = path;
|
||||
this.data = data;
|
||||
this.queryParams = queryParams;
|
||||
this.callback = null;
|
||||
this.response = null;
|
||||
this.checks = [];
|
||||
}
|
||||
Request.prototype = {
|
||||
/**
|
||||
* Execute a check when this request has been satisfied.
|
||||
* @param {Function} fn The function to execute.
|
||||
* @return {Request} for chaining calls.
|
||||
*/
|
||||
check: function(fn) {
|
||||
this.checks.push(fn);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Respond with the given data when this request is satisfied.
|
||||
* @param {Number} code The HTTP status code.
|
||||
* @param {Object|Function} data The HTTP JSON body. If this is a function,
|
||||
* it will be invoked when the JSON body is required (which should be returned).
|
||||
*/
|
||||
respond: function(code, data) {
|
||||
this.response = {
|
||||
response: {
|
||||
statusCode: code,
|
||||
headers: {}
|
||||
},
|
||||
body: data,
|
||||
err: null
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Fail with an Error when this request is satisfied.
|
||||
* @param {Number} code The HTTP status code.
|
||||
* @param {Error} err The error to throw (e.g. Network Error)
|
||||
*/
|
||||
fail: function(code, err) {
|
||||
this.response = {
|
||||
response: {
|
||||
statusCode: code,
|
||||
headers: {}
|
||||
},
|
||||
body: null,
|
||||
err: err
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The HttpBackend class.
|
||||
*/
|
||||
module.exports = HttpBackend;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2017 Vector creations Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import logger from '../src/logger';
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
}
|
||||
+118
-47
@@ -1,16 +1,56 @@
|
||||
"use strict";
|
||||
var sdk = require("..");
|
||||
var MatrixEvent = sdk.MatrixEvent;
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import logger from '../src/logger';
|
||||
import sdk from '..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
* SYNCING event.
|
||||
* @param {Object} client The client
|
||||
* @param {Number=} count Number of syncs to wait for (default 1)
|
||||
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
||||
*/
|
||||
module.exports.syncPromise = function(client, count) {
|
||||
if (count === undefined) {
|
||||
count = 1;
|
||||
}
|
||||
if (count <= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const cb = (state) => {
|
||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
if (state == 'SYNCING') {
|
||||
resolve();
|
||||
} else {
|
||||
client.once('sync', cb);
|
||||
}
|
||||
};
|
||||
client.once('sync', cb);
|
||||
});
|
||||
|
||||
return p.then(() => {
|
||||
return module.exports.syncPromise(client, count-1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform common actions before each test case, e.g. printing the test case
|
||||
* name to stdout.
|
||||
* @param {TestCase} testCase The test case that is about to be run.
|
||||
* @param {Mocha.Context} context The test context
|
||||
*/
|
||||
module.exports.beforeEach = function(testCase) {
|
||||
var desc = testCase.suite.description + " : " + testCase.description;
|
||||
console.log(desc);
|
||||
console.log(new Array(1 + desc.length).join("="));
|
||||
module.exports.beforeEach = function(context) {
|
||||
const desc = context.currentTest.fullTitle();
|
||||
|
||||
logger.log(desc);
|
||||
logger.log(new Array(1 + desc.length).join("="));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -20,21 +60,20 @@ module.exports.beforeEach = function(testCase) {
|
||||
* @return {Object} An instantiated object with spied methods/properties.
|
||||
*/
|
||||
module.exports.mock = function(constr, name) {
|
||||
// By Tim Buschtöns
|
||||
// Based on
|
||||
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
||||
var HelperConstr = new Function(); // jshint ignore:line
|
||||
const HelperConstr = new Function(); // jshint ignore:line
|
||||
HelperConstr.prototype = constr.prototype;
|
||||
var result = new HelperConstr();
|
||||
result.jasmineToString = function() {
|
||||
const result = new HelperConstr();
|
||||
result.toString = function() {
|
||||
return "mock" + (name ? " of " + name : "");
|
||||
};
|
||||
for (var key in constr.prototype) { // jshint ignore:line
|
||||
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jasmine.createSpy((name || "mock") + '.' + key);
|
||||
result[key] = expect.createSpy();
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
} catch (ex) {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
// cause exceptions.
|
||||
// Overwriting will not work either in that case.
|
||||
@@ -48,7 +87,7 @@ module.exports.mock = function(constr, name) {
|
||||
* @param {Object} opts Values for the event.
|
||||
* @param {string} opts.type The event.type
|
||||
* @param {string} opts.room The event.room_id
|
||||
* @param {string} opts.user The event.user_id
|
||||
* @param {string} opts.sender The event.sender
|
||||
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
|
||||
* @param {Object} opts.content The event.content
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
@@ -58,17 +97,16 @@ module.exports.mkEvent = function(opts) {
|
||||
if (!opts.type || !opts.content) {
|
||||
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
||||
}
|
||||
var event = {
|
||||
const event = {
|
||||
type: opts.type,
|
||||
room_id: opts.room,
|
||||
sender: opts.user,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
event_id: "$" + Math.random() + "-" + Math.random()
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
};
|
||||
if (opts.skey) {
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
}
|
||||
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||
"m.room.power_levels", "m.room.topic",
|
||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
||||
event.state_key = "";
|
||||
@@ -85,16 +123,16 @@ module.exports.mkPresence = function(opts) {
|
||||
if (!opts.user) {
|
||||
throw new Error("Missing user");
|
||||
}
|
||||
var event = {
|
||||
const event = {
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
type: "m.presence",
|
||||
sender: opts.user,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: {
|
||||
avatar_url: opts.url,
|
||||
displayname: opts.name,
|
||||
last_active_ago: opts.ago,
|
||||
presence: opts.presence || "offline"
|
||||
}
|
||||
presence: opts.presence || "offline",
|
||||
},
|
||||
};
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
};
|
||||
@@ -104,8 +142,8 @@ module.exports.mkPresence = function(opts) {
|
||||
* @param {Object} opts Values for the membership.
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.mship The content.membership for the event.
|
||||
* @param {string} opts.user The user ID for the event.
|
||||
* @param {string} opts.skey The other user ID for the event if applicable
|
||||
* @param {string} opts.sender The sender user ID for the event.
|
||||
* @param {string} opts.skey The target user ID for the event if applicable
|
||||
* e.g. for invites/bans.
|
||||
* @param {string} opts.name The content.displayname for the event.
|
||||
* @param {string} opts.url The content.avatar_url for the event.
|
||||
@@ -115,16 +153,20 @@ module.exports.mkPresence = function(opts) {
|
||||
module.exports.mkMembership = function(opts) {
|
||||
opts.type = "m.room.member";
|
||||
if (!opts.skey) {
|
||||
opts.skey = opts.user;
|
||||
opts.skey = opts.sender || opts.user;
|
||||
}
|
||||
if (!opts.mship) {
|
||||
throw new Error("Missing .mship => " + JSON.stringify(opts));
|
||||
}
|
||||
opts.content = {
|
||||
membership: opts.mship
|
||||
membership: opts.mship,
|
||||
};
|
||||
if (opts.name) { opts.content.displayname = opts.name; }
|
||||
if (opts.url) { opts.content.avatar_url = opts.url; }
|
||||
if (opts.name) {
|
||||
opts.content.displayname = opts.name;
|
||||
}
|
||||
if (opts.url) {
|
||||
opts.content.avatar_url = opts.url;
|
||||
}
|
||||
return module.exports.mkEvent(opts);
|
||||
};
|
||||
|
||||
@@ -147,27 +189,56 @@ module.exports.mkMessage = function(opts) {
|
||||
}
|
||||
opts.content = {
|
||||
msgtype: "m.text",
|
||||
body: opts.msg
|
||||
body: opts.msg,
|
||||
};
|
||||
return module.exports.mkEvent(opts);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* make the test fail, with the given exception
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
* <p>This is useful for use with integration tests which use asyncronous
|
||||
* methods: it can be added as a 'catch' handler in a promise chain.
|
||||
*
|
||||
* @param {Error} error exception to be reported
|
||||
*
|
||||
* @example
|
||||
* it("should not throw", function(done) {
|
||||
* asynchronousMethod().then(function() {
|
||||
* // some tests
|
||||
* }).catch(utils.failTest).done(done);
|
||||
* });
|
||||
* @constructor
|
||||
*/
|
||||
module.exports.failTest = function(error) {
|
||||
expect(error.stack).toBe(null);
|
||||
module.exports.MockStorageApi = function() {
|
||||
this.data = {};
|
||||
};
|
||||
module.exports.MockStorageApi.prototype = {
|
||||
get length() {
|
||||
return Object.keys(this.data).length;
|
||||
},
|
||||
key: function(i) {
|
||||
return Object.keys(this.data)[i];
|
||||
},
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
module.exports.awaitDecryption = function(event) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
const AutoDiscovery = sdk.AutoDiscovery;
|
||||
|
||||
import expect from 'expect';
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new MockHttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
|
||||
it("should throw an error when no domain is specified", function() {
|
||||
return Promise.all([
|
||||
AutoDiscovery.findClientConfig(/* no args */).then(() => {
|
||||
throw new Error("Expected a failure, not success with no args");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig("").then(() => {
|
||||
throw new Error("Expected a failure, not success with an empty string");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig(null).then(() => {
|
||||
throw new Error("Expected a failure, not success with null");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
|
||||
AutoDiscovery.findClientConfig(true).then(() => {
|
||||
throw new Error("Expected a failure, not success with a non-string");
|
||||
}, () => {
|
||||
return true;
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return PROMPT when .well-known 404s", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (empty string)", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
|
||||
"m.homeserver (no property)", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_PROMPT",
|
||||
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (disallowed scheme)", function() {
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "mxc://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 404)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 500)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
|
||||
"m.homeserver (verification failure: 200 but wrong content)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||
not_matrix_versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
|
||||
"m.homeserver", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS with the right homeserver URL", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(missing base_url)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
not_base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(empty base_url)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
|
||||
base_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(validation error: 404)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes.
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return FAIL_ERROR when the identity server configuration is wrong " +
|
||||
"(validation error: 500)", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IS,
|
||||
|
||||
// We still expect the base_url to be here for debugging purposes
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "FAIL_ERROR",
|
||||
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS when the identity server configuration is " +
|
||||
"verifiably accurate", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS and preserve non-standard keys from the " +
|
||||
".well-known response", function() {
|
||||
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
}).respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
|
||||
expect(req.opts.uri)
|
||||
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
}).respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,34 +1,37 @@
|
||||
"use strict";
|
||||
var ContentRepo = require("../../lib/content-repo");
|
||||
var testUtils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const ContentRepo = require("../../lib/content-repo");
|
||||
const testUtils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
var baseUrl = "https://my.home.server";
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this);
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
var httpUrl = "http://example.com/image.jpeg";
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(
|
||||
ContentRepo.getHttpUriForMxc(
|
||||
baseUrl, httpUrl, undefined, undefined, undefined, true
|
||||
)
|
||||
baseUrl, httpUrl, undefined, undefined, undefined, true,
|
||||
),
|
||||
).toEqual(httpUrl);
|
||||
});
|
||||
|
||||
it("should return the empty string HTTP URLs by default", function() {
|
||||
var httpUrl = "http://example.com/image.jpeg";
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a download URL if no width/height/resize are specified",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid";
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid"
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,27 +41,27 @@ describe("ContentRepo", function() {
|
||||
|
||||
it("should return a thumbnail URL if a width/height/resize is specified",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid";
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop"
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid#automade";
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade"
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade",
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid#automade";
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade"
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -70,22 +73,22 @@ describe("ContentRepo", function() {
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/identicon/foobar" +
|
||||
"?width=96&height=96"
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=96&height=96",
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to set custom w/h", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/identicon/foobar" +
|
||||
"?width=32&height=64"
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
|
||||
it("should URL encode the identicon string", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
|
||||
"?width=32&height=64"
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
import 'source-map-support/register';
|
||||
|
||||
import '../olm-loader';
|
||||
|
||||
import Crypto from '../../lib/crypto';
|
||||
import expect from 'expect';
|
||||
|
||||
import WebStorageSessionStore from '../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../MockStorageApi';
|
||||
import TestClient from '../TestClient';
|
||||
import {MatrixEvent} from '../../lib/models/event';
|
||||
import Room from '../../lib/models/room';
|
||||
import olmlib from '../../lib/crypto/olmlib';
|
||||
import lolex from 'lolex';
|
||||
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
const sdk = require("../..");
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
Olm.init().then(done);
|
||||
});
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||
});
|
||||
|
||||
|
||||
describe('Session management', function() {
|
||||
const otkResponse = {
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:FLIBBLE': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally a valid signature',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let crypto;
|
||||
let mockBaseApis;
|
||||
let mockRoomList;
|
||||
|
||||
let fakeEmitter;
|
||||
|
||||
beforeEach(async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
cryptoStore.storeEndToEndDeviceData({
|
||||
devices: {
|
||||
'@bob:home.server': {
|
||||
'BOBDEVICE': {
|
||||
keys: {
|
||||
'curve25519:BOBDEVICE': 'this is a key',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
trackingStatus: {},
|
||||
});
|
||||
|
||||
mockBaseApis = {
|
||||
sendToDevice: expect.createSpy(),
|
||||
getKeyBackupVersion: expect.createSpy(),
|
||||
isGuest: expect.createSpy(),
|
||||
};
|
||||
mockRoomList = {};
|
||||
|
||||
fakeEmitter = new EventEmitter();
|
||||
|
||||
crypto = new Crypto(
|
||||
mockBaseApis,
|
||||
sessionStore,
|
||||
"@alice:home.server",
|
||||
"FLIBBLE",
|
||||
sessionStore,
|
||||
cryptoStore,
|
||||
mockRoomList,
|
||||
);
|
||||
crypto.registerEventHandlers(fakeEmitter);
|
||||
await crypto.init();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
await crypto.stop();
|
||||
});
|
||||
|
||||
it("restarts wedged Olm sessions", async function() {
|
||||
const prom = new Promise((resolve) => {
|
||||
mockBaseApis.claimOneTimeKeys = function() {
|
||||
resolve();
|
||||
return otkResponse;
|
||||
};
|
||||
});
|
||||
|
||||
fakeEmitter.emit('toDeviceEvent', {
|
||||
getType: expect.createSpy().andReturn('m.room.message'),
|
||||
getContent: expect.createSpy().andReturn({
|
||||
msgtype: 'm.bad.encrypted',
|
||||
}),
|
||||
getWireContent: expect.createSpy().andReturn({
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
sender_key: 'this is a key',
|
||||
}),
|
||||
getSender: expect.createSpy().andReturn('@bob:home.server'),
|
||||
});
|
||||
|
||||
await prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Key requests', function() {
|
||||
let aliceClient;
|
||||
let bobClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await aliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
aliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it(
|
||||
"does not cancel keyshare requests if some messages are not decrypted",
|
||||
async function() {
|
||||
function awaitEvent(emitter, event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
emitter.once(event, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function keyshareEventForEvent(event, index) {
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await aliceClient._crypto._olmDevice
|
||||
.getInboundGroupSessionKey(
|
||||
roomId, eventContent.sender_key, eventContent.session_id,
|
||||
index,
|
||||
);
|
||||
const ksEvent = new MatrixEvent({
|
||||
type: "m.forwarded_room_key",
|
||||
sender: "@alice:example.com",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: eventContent.sender_key,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: eventContent.session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain:
|
||||
key.forwarding_curve_key_chain,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
ksEvent._senderCurve25519Key = "akey";
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$1",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "1",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$2",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "2",
|
||||
},
|
||||
}),
|
||||
];
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
event._clearEvent = {};
|
||||
event._senderCurve25519Key = null;
|
||||
event._claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient._crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
let eventPromise = Promise.all(events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}));
|
||||
|
||||
// keyshare the session key starting at the second message, so
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(events[1], 1);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient._cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
// the room key request should still be there, since we haven't
|
||||
// decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
|
||||
// keyshare the session key starting at the first message, so
|
||||
// that it can now be decrypted
|
||||
eventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||
ksEvent = await keyshareEventForEvent(events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
// the room key request should be gone since we've now decypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toNotExist();
|
||||
},
|
||||
);
|
||||
|
||||
it("creates a new keyshare request if we request a keyshare", async function() {
|
||||
// make sure that cancelAndResend... creates a new keyshare request
|
||||
// if there wasn't an already-existing one
|
||||
const event = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
const cryptoStore = aliceClient._cryptoStore;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: "!someroom",
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
};
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
});
|
||||
|
||||
it("uses a new txnid for re-requesting keys", async function() {
|
||||
const event = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
room_id: "!someroom",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
session_id: "sessionid",
|
||||
sender_key: "senderkey",
|
||||
},
|
||||
});
|
||||
/* return a promise and a function. When the function is called,
|
||||
* the promise will be resolved.
|
||||
*/
|
||||
function awaitFunctionCall() {
|
||||
let func;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
func = function(...args) {
|
||||
resolve(args);
|
||||
return new Promise((resolve, reject) => {
|
||||
// give us some time to process the result before
|
||||
// continuing
|
||||
global.setTimeout(resolve, 1);
|
||||
});
|
||||
};
|
||||
});
|
||||
return {func, promise};
|
||||
}
|
||||
|
||||
aliceClient.startClient();
|
||||
|
||||
const clock = lolex.install();
|
||||
|
||||
try {
|
||||
let promise;
|
||||
// make a room key request, and record the transaction ID for the
|
||||
// sendToDevice call
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
clock.runToLast();
|
||||
let args = await promise;
|
||||
const txnId = args[2];
|
||||
clock.runToLast();
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
// of the request
|
||||
await Promise.resolve();
|
||||
|
||||
// cancel and resend the room key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
clock.runToLast();
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
args = await promise;
|
||||
// the second call to sendToDevice will be the key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
clock.runToLast();
|
||||
args = await promise;
|
||||
clock.runToLast();
|
||||
expect(args[2]).toNotBe(txnId);
|
||||
} finally {
|
||||
clock.uninstall();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import DeviceList from '../../../lib/crypto/DeviceList';
|
||||
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import testUtils from '../../test-utils';
|
||||
import utils from '../../../lib/utils';
|
||||
import logger from '../../../src/logger';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const signedDeviceList = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test1:sw1v.org": {
|
||||
"HGKAWHRVJQ": {
|
||||
"signatures": {
|
||||
"@test1:sw1v.org": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
|
||||
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
|
||||
},
|
||||
},
|
||||
"user_id": "@test1:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:HGKAWHRVJQ":
|
||||
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
|
||||
"curve25519:HGKAWHRVJQ":
|
||||
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "HGKAWHRVJQ",
|
||||
"unsigned": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
let deviceLists = [];
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
deviceLists = [];
|
||||
|
||||
downloadSpy = expect.createSpy();
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
for (const dl of deviceLists) {
|
||||
dl.stop();
|
||||
}
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
};
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
|
||||
it("should successfully download and store device keys", function() {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have an outdated devicelist on an invalidation while an " +
|
||||
"update is in progress", function() {
|
||||
const dl = createTestDeviceList();
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
downloadSpy.reset();
|
||||
|
||||
// outdated notif arrives while the request is in flight.
|
||||
const queryDefer2 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer2.promise);
|
||||
|
||||
dl.invalidateUserDeviceList('@test1:sw1v.org');
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
dl.saveIfDirty().then(() => {
|
||||
// the first request completes
|
||||
queryDefer1.resolve({
|
||||
device_keys: {
|
||||
'@test1:sw1v.org': {},
|
||||
},
|
||||
});
|
||||
return prom1;
|
||||
}).then(() => {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.reset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
|
||||
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
// allow promise chain to complete
|
||||
return prom3;
|
||||
}).then(() => {
|
||||
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,350 @@
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import sdk from '../../../..';
|
||||
import algorithms from '../../../../lib/crypto/algorithms';
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import testUtils from '../../../test-utils';
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../../lib/crypto';
|
||||
import logger from '../../../../src/logger';
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("MegolmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let megolmDecryption;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockBaseApis;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await Olm.init();
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockBaseApis = {};
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
expect.createSpy().andReturn(Promise.resolve());
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
});
|
||||
|
||||
describe('receives some keys:', function() {
|
||||
let groupSession;
|
||||
beforeEach(async function() {
|
||||
groupSession = new global.Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
// 'crypto' implementation.
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: ROOM_ID,
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
},
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
|
||||
const mockCrypto = {
|
||||
decryptEvent: function() {
|
||||
return Promise.resolve(decryptedData);
|
||||
},
|
||||
};
|
||||
|
||||
await event.attemptDecryption(mockCrypto).then(() => {
|
||||
megolmDecryption.onRoomKeyEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
it('can decrypt an event', function() {
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: groupSession.session_id(),
|
||||
ciphertext: groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
|
||||
it('can respond to a key request event', function() {
|
||||
const keyRequest = {
|
||||
userId: '@alice:foo',
|
||||
deviceId: 'alidevice',
|
||||
requestBody: {
|
||||
room_id: ROOM_ID,
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: groupSession.session_id(),
|
||||
},
|
||||
};
|
||||
|
||||
return megolmDecryption.hasKeysForKeyRequest(
|
||||
keyRequest,
|
||||
).then((hasKeys) => {
|
||||
expect(hasKeys).toBe(true);
|
||||
|
||||
// set up some pre-conditions for the share call
|
||||
const deviceInfo = {};
|
||||
mockCrypto.getStoredDevice.andReturn(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.andReturn(
|
||||
Promise.resolve({'@alice:foo': {'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}}}),
|
||||
);
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.andCall(() => {
|
||||
res();
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
mockBaseApis.sendToDevice = expect.createSpy();
|
||||
|
||||
// do the share
|
||||
megolmDecryption.shareKeysWithDevice(keyRequest);
|
||||
|
||||
// it's asynchronous, so we have to wait a bit
|
||||
return awaitEncryptForDevice;
|
||||
}).then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice.calls.length)
|
||||
.toEqual(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.calls[0];
|
||||
const payload = call.arguments[6];
|
||||
|
||||
expect(payload.type).toEqual("m.forwarded_room_key");
|
||||
expect(payload.content).toInclude({
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
sender_claimed_ed25519_key: "SENDER_ED25519",
|
||||
session_id: groupSession.session_id(),
|
||||
chain_index: 0,
|
||||
forwarding_curve25519_key_chain: [],
|
||||
});
|
||||
expect(payload.content.session_key).toExist();
|
||||
});
|
||||
});
|
||||
|
||||
it("can detect replay attacks", function() {
|
||||
// trying to decrypt two different messages (marked by different
|
||||
// event IDs or timestamps) using the same (sender key, session id,
|
||||
// message index) triple should result in an exception being thrown
|
||||
// as it should be detected as a replay attack.
|
||||
const sessionId = groupSession.session_id();
|
||||
const cipherText = groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
}));
|
||||
const event1 = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const successHandler = expect.createSpy();
|
||||
const failureHandler = expect.createSpy()
|
||||
.andCall((err) => {
|
||||
expect(err.toString()).toMatch(
|
||||
/Duplicate message index, possible replay attack/,
|
||||
);
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event1).then((res) => {
|
||||
const event2 = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event2",
|
||||
origin_server_ts: 1507754149000,
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event2);
|
||||
}).then(
|
||||
successHandler,
|
||||
failureHandler,
|
||||
).then(() => {
|
||||
expect(successHandler).toNotHaveBeenCalled();
|
||||
expect(failureHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("allows re-decryption of the same event", function() {
|
||||
// in contrast with the previous test, if the event ID and
|
||||
// timestamp are the same, then it should not be considered a
|
||||
// replay attack
|
||||
const sessionId = groupSession.session_id();
|
||||
const cipherText = groupSession.encrypt(JSON.stringify({
|
||||
room_id: ROOM_ID,
|
||||
content: 'testytest',
|
||||
}));
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: ROOM_ID,
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
session_id: sessionId,
|
||||
ciphertext: cipherText,
|
||||
},
|
||||
event_id: "$event1",
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event).then((res) => {
|
||||
return megolmDecryption.decryptEvent(event);
|
||||
// test is successful if no exception is thrown
|
||||
});
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = expect.createSpy();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
'signed_curve25519:flooble': {
|
||||
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
|
||||
signatures: {
|
||||
'@alice:home.server': {
|
||||
'ed25519:aliceDevice': 'totally valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
|
||||
|
||||
mockCrypto.downloadKeys.andReturn(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: expect.createSpy().andReturn(false),
|
||||
isUnverified: expect.createSpy().andReturn(false),
|
||||
getIdentityKey: expect.createSpy().andReturn(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: expect.createSpy().andReturn(''),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
config: {
|
||||
rotation_period_ms: 9999999999999,
|
||||
},
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: expect.createSpy().andReturn(
|
||||
[{userId: "@alice:home.server"}],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
|
||||
};
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
});
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.reset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
Copyright 2018,2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import testUtils from '../../../test-utils';
|
||||
import logger from '../../../../src/logger';
|
||||
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
import olmlib from '../../../../lib/crypto/olmlib';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
function makeOlmDevice() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
return olmDevice;
|
||||
}
|
||||
|
||||
async function setupSession(initiator, opponent) {
|
||||
await opponent.generateOneTimeKeys(1);
|
||||
const keys = await opponent.getOneTimeKeys();
|
||||
const firstKey = Object.values(keys['curve25519'])[0];
|
||||
|
||||
const sid = await initiator.createOutboundSession(
|
||||
opponent.deviceCurve25519Key, firstKey,
|
||||
);
|
||||
return sid;
|
||||
}
|
||||
|
||||
describe("OlmDecryption", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let aliceOlmDevice;
|
||||
let bobOlmDevice;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await global.Olm.init();
|
||||
|
||||
aliceOlmDevice = makeOlmDevice();
|
||||
bobOlmDevice = makeOlmDevice();
|
||||
await aliceOlmDevice.init();
|
||||
await bobOlmDevice.init();
|
||||
});
|
||||
|
||||
describe('olm', function() {
|
||||
it("can decrypt messages", async function() {
|
||||
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sid,
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
);
|
||||
|
||||
const result = await bobOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
expect(result.payload).toEqual(
|
||||
"The olm or proteus is an aquatic salamander in the family Proteidae",
|
||||
);
|
||||
});
|
||||
|
||||
it("creates only one session at a time", async function() {
|
||||
// if we call ensureOlmSessionsForDevices multiple times, it should
|
||||
// only try to create one session at a time, even if the server is
|
||||
// slow
|
||||
let count = 0;
|
||||
const baseApis = {
|
||||
claimOneTimeKeys: () => {
|
||||
// simulate a very slow server (.5 seconds to respond)
|
||||
count++;
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
};
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
},
|
||||
}, "ABCDEFG"),
|
||||
],
|
||||
};
|
||||
function alwaysSucceed(promise) {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
// start two tasks that try to ensure that there's an olm session
|
||||
const promises = Promise.all([
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUser,
|
||||
)),
|
||||
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUser,
|
||||
)),
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 200);
|
||||
});
|
||||
|
||||
// after .2s, both tasks should have started, but one should be
|
||||
// waiting on the other before trying to create a session, so
|
||||
// claimOneTimeKeys should have only been called once
|
||||
expect(count).toBe(1);
|
||||
|
||||
await promises;
|
||||
|
||||
// after waiting for both tasks to complete, the first task should
|
||||
// have failed, so the second task should have tried to create a
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import sdk from '../../..';
|
||||
import algorithms from '../../../lib/crypto/algorithms';
|
||||
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../MockStorageApi';
|
||||
import testUtils from '../../test-utils';
|
||||
|
||||
import OlmDevice from '../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../lib/crypto';
|
||||
import logger from '../../../src/logger';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
|
||||
const ENCRYPTED_EVENT = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
room_id: '!ROOM:ID',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
sender_key: 'SENDER_CURVE25519',
|
||||
session_id: SESSION_ID,
|
||||
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
|
||||
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
|
||||
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
|
||||
},
|
||||
event_id: '$event1',
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const KEY_BACKUP_DATA = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
|
||||
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
|
||||
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
|
||||
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
|
||||
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
|
||||
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
|
||||
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
|
||||
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
|
||||
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
|
||||
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
|
||||
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
|
||||
mac: '5lxYBHQU80M',
|
||||
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
|
||||
},
|
||||
};
|
||||
|
||||
const BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
};
|
||||
|
||||
function makeTestClient(sessionStore, cryptoStore) {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
});
|
||||
}
|
||||
|
||||
describe("MegolmBackup", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockStorage;
|
||||
let sessionStore;
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
);
|
||||
mockCrypto.backupInfo = BACKUP_INFO;
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
expect.createSpy().andReturn(Promise.resolve());
|
||||
});
|
||||
|
||||
describe("backup", function() {
|
||||
let mockBaseApis;
|
||||
let realSetTimeout;
|
||||
|
||||
beforeEach(function() {
|
||||
mockBaseApis = {};
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
// clobber the setTimeout function to run 100x faster.
|
||||
// ideally we would use lolex, but we have no oportunity
|
||||
// to tick the clock between the first try and the retry.
|
||||
realSetTimeout = global.setTimeout;
|
||||
global.setTimeout = function(f, n) {
|
||||
return realSetTimeout(f, n/100);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.setTimeout = realSetTimeout;
|
||||
});
|
||||
|
||||
it('automatically calls the key back up', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
// 'crypto' implementation.
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: ROOM_ID,
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
},
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
|
||||
mockCrypto.decryptEvent = function() {
|
||||
return Promise.resolve(decryptedData);
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
|
||||
mockCrypto.backupGroupSession = expect.createSpy();
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(sessionStore, cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: "@alice:bar",
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
});
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "foobar",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
} else {
|
||||
return Promise.reject(
|
||||
new Error("this is an expected failure"),
|
||||
);
|
||||
}
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("restore", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = makeTestClient(sessionStore, cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('can restore from backup', function() {
|
||||
client._http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore backup by room', function() {
|
||||
client._http.authedRequest = function() {
|
||||
return Promise.resolve({
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[SESSION_ID]: KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
null, null, BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
import logger from '../../../../src/logger';
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("QR code verification", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
describe("showing", function() {
|
||||
it("should emit an event to show a QR code", async function() {
|
||||
const qrCode = new ShowQRCode({
|
||||
getUserId: () => "@alice:example.com",
|
||||
deviceId: "ABCDEFG",
|
||||
getDeviceEd25519Key: function() {
|
||||
return "device+ed25519+key";
|
||||
},
|
||||
});
|
||||
const spy = expect.createSpy().andCall((e) => {
|
||||
qrCode.done();
|
||||
});
|
||||
qrCode.on("show_qr_code", spy);
|
||||
await qrCode.verify();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanning", function() {
|
||||
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
|
||||
it("should verify when a QR code is sent", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client);
|
||||
qrCode.on("confirm_user_id", ({userId, confirm}) => {
|
||||
if (userId === "@alice:example.com") {
|
||||
confirm();
|
||||
} else {
|
||||
qrCode.cancel(new Error("Incorrect user"));
|
||||
}
|
||||
});
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
await qrCode.verify();
|
||||
expect(client.getStoredDevice)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
expect(client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
});
|
||||
|
||||
it("should error when the user ID doesn't match", async function() {
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy(),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toNotHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error if the key doesn't match", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
import logger from '../../../../src/logger';
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
describe("verification request", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.on("crypto.verification.request", (request) => {
|
||||
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||
bobVerifier.verify();
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
bobVerifier._endTimer();
|
||||
});
|
||||
const aliceVerifier = await alice.requestVerification("@bob:example.com");
|
||||
expect(aliceVerifier).toBeAn(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier._endTimer();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
import logger from '../../../../src/logger';
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import sdk from '../../../..';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
describe("SAS verification", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
|
||||
sas.handleEvent(new MatrixEvent({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
content: {},
|
||||
}));
|
||||
const spy = expect.createSpy();
|
||||
await sas.verify()
|
||||
.catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
// Cancel the SAS for cleanup (we started a verification, so abort)
|
||||
sas.cancel();
|
||||
});
|
||||
|
||||
describe("verification", function() {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
let bobSasEvent;
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
|
||||
beforeEach(async function() {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
alice.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Dynabook",
|
||||
);
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"ed25519:Osborne2": "alice+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
"Osborne2",
|
||||
);
|
||||
};
|
||||
bob.getDeviceEd25519Key = () => {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
bobSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(aliceSasEvent.sas);
|
||||
e.confirm();
|
||||
aliceSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
aliceSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!bobSasEvent) {
|
||||
aliceSasEvent = e;
|
||||
} else {
|
||||
try {
|
||||
expect(e.sas).toEqual(bobSasEvent.sas);
|
||||
e.confirm();
|
||||
bobSasEvent.confirm();
|
||||
} catch (error) {
|
||||
e.mismatch();
|
||||
bobSasEvent.mismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
let macMethod;
|
||||
const origSendToDevice = alice.sendToDevice;
|
||||
bob.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.getUserId()][alice.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
|
||||
it("should be able to verify using the old MAC", async function() {
|
||||
// pretend that Alice can only understand the old (incorrect) MAC,
|
||||
// and make sure that she can still verify with Bob
|
||||
let macMethod;
|
||||
const origSendToDevice = alice.sendToDevice;
|
||||
alice.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.start") {
|
||||
// Note: this modifies not only the message that Bob
|
||||
// receives, but also the copy of the message that Alice
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.getUserId()][bob.deviceId]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
bob.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.getUserId()][alice.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hmac-sha256");
|
||||
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
|
||||
const aliceSpy = expect.createSpy();
|
||||
const bobSpy = expect.createSpy();
|
||||
await Promise.all([
|
||||
aliceVerifier.verify().catch(aliceSpy),
|
||||
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
|
||||
]);
|
||||
expect(aliceSpy).toHaveBeenCalled();
|
||||
expect(bobSpy).toHaveBeenCalled();
|
||||
expect(alice.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
expect(bob.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import TestClient from '../../../TestClient';
|
||||
|
||||
import sdk from '../../../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
// console.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
setTimeout(
|
||||
() => clientMap[userId][deviceId]
|
||||
.emit("toDeviceEvent", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
const client = (new TestClient(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined,
|
||||
options,
|
||||
)).client;
|
||||
if (!(userInfo.userId in clientMap)) {
|
||||
clientMap[userInfo.userId] = {};
|
||||
}
|
||||
clientMap[userInfo.userId][userInfo.deviceId] = client;
|
||||
client.sendToDevice = sendToDevice;
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
await Promise.all(clients.map((client) => client.initCrypto()));
|
||||
|
||||
return clients;
|
||||
}
|
||||
@@ -1,27 +1,37 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
function mockRoomStates(timeline) {
|
||||
timeline._startState = utils.mock(sdk.RoomState, "startState");
|
||||
timeline._endState = utils.mock(sdk.RoomState, "endState");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
var roomId = "!foo:bar";
|
||||
var userA = "@alice:bar";
|
||||
var userB = "@bertha:bar";
|
||||
var timeline;
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bertha:bar";
|
||||
let timeline;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
timeline = new EventTimeline(roomId);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
const timelineSet = { room: { roomId: roomId }};
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
return timelineSet;
|
||||
};
|
||||
|
||||
timeline = new EventTimeline(timelineSet);
|
||||
});
|
||||
|
||||
describe("construction", function() {
|
||||
it("getRoomId should get room id", function() {
|
||||
var v = timeline.getRoomId();
|
||||
const v = timeline.getRoomId();
|
||||
expect(v).toEqual(roomId);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +42,7 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("should copy state events to start and end state", function() {
|
||||
var events = [
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA,
|
||||
event: true,
|
||||
@@ -41,34 +51,38 @@ describe("EventTimeline", function() {
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
event: true,
|
||||
content: { name: "New room" },
|
||||
})
|
||||
}),
|
||||
];
|
||||
timeline.initialiseState(events);
|
||||
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
|
||||
events
|
||||
events,
|
||||
);
|
||||
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
|
||||
events
|
||||
events,
|
||||
);
|
||||
});
|
||||
|
||||
it("should raise an exception if called after events are added", function() {
|
||||
var event =
|
||||
const event =
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "Adam stole the plushies",
|
||||
event: true,
|
||||
});
|
||||
|
||||
var state = [
|
||||
const state = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA,
|
||||
event: true,
|
||||
})
|
||||
}),
|
||||
];
|
||||
|
||||
expect(function() { timeline.initialiseState(state); }).not.toThrow();
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
}).toNotThrow();
|
||||
timeline.addEvent(event, false);
|
||||
expect(function() { timeline.initialiseState(state); }).toThrow();
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,8 +108,8 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should set neighbour", function() {
|
||||
var prev = {a: "a"};
|
||||
var next = {b: "b"};
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
|
||||
@@ -103,11 +117,11 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should throw if called twice", function() {
|
||||
var prev = {a: "a"};
|
||||
var next = {b: "b"};
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).not.toThrow();
|
||||
}).toNotThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(prev);
|
||||
expect(function() {
|
||||
@@ -116,7 +130,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
}).not.toThrow();
|
||||
}).toNotThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(next);
|
||||
expect(function() {
|
||||
@@ -130,7 +144,7 @@ describe("EventTimeline", function() {
|
||||
mockRoomStates(timeline);
|
||||
});
|
||||
|
||||
var events = [
|
||||
const events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
@@ -143,7 +157,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
it("should be able to add events to the end", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
var initialIndex = timeline.getBaseIndex();
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
@@ -153,7 +167,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
it("should be able to add events to the start", function() {
|
||||
timeline.addEvent(events[0], true);
|
||||
var initialIndex = timeline.getBaseIndex();
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], true);
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
@@ -162,38 +176,38 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("should set event.sender for new and old events", function() {
|
||||
var sentinel = {
|
||||
const sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
name: "Alice",
|
||||
};
|
||||
var oldSentinel = {
|
||||
const oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice"
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
.andCall(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
.andCall(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
var newEv = utils.mkEvent({
|
||||
const newEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "New Room Name" }
|
||||
content: { name: "New Room Name" },
|
||||
});
|
||||
var oldEv = utils.mkEvent({
|
||||
const oldEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "Old Room Name" }
|
||||
content: { name: "Old Room Name" },
|
||||
});
|
||||
|
||||
timeline.addEvent(newEv, false);
|
||||
@@ -204,36 +218,36 @@ describe("EventTimeline", function() {
|
||||
|
||||
it("should set event.target for new and old m.room.member events",
|
||||
function() {
|
||||
var sentinel = {
|
||||
const sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
name: "Alice",
|
||||
};
|
||||
var oldSentinel = {
|
||||
const oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice"
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
.andCall(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
.andCall(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
var newEv = utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
const newEv = utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
});
|
||||
var oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true
|
||||
const oldEv = utils.mkMembership({
|
||||
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
|
||||
});
|
||||
timeline.addEvent(newEv, false);
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
@@ -243,16 +257,16 @@ describe("EventTimeline", function() {
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for new events", function() {
|
||||
var events = [
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB, event: true,
|
||||
content: {
|
||||
name: "New room"
|
||||
}
|
||||
})
|
||||
name: "New room",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], false);
|
||||
@@ -267,22 +281,22 @@ describe("EventTimeline", function() {
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
var events = [
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB, event: true,
|
||||
content: {
|
||||
name: "New room"
|
||||
}
|
||||
})
|
||||
name: "New room",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], true);
|
||||
@@ -297,12 +311,12 @@ describe("EventTimeline", function() {
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeEvent", function() {
|
||||
var events = [
|
||||
const events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
@@ -322,7 +336,7 @@ describe("EventTimeline", function() {
|
||||
timeline.addEvent(events[1], false);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
|
||||
var ev = timeline.removeEvent(events[0].getId());
|
||||
let ev = timeline.removeEvent(events[0].getId());
|
||||
expect(ev).toBe(events[0]);
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
|
||||
@@ -354,7 +368,7 @@ describe("EventTimeline", function() {
|
||||
function() {
|
||||
timeline.addEvent(events[0], true);
|
||||
timeline.removeEvent(events[0].getId());
|
||||
var initialIndex = timeline.getBaseIndex();
|
||||
const initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[2], false);
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
@@ -362,4 +376,3 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import sdk from '../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import testUtils from '../test-utils';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
id: 'test_encrypted_event',
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', () => {
|
||||
let callCount = 0;
|
||||
|
||||
let prom2;
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: function() {
|
||||
++callCount;
|
||||
logger.log(`decrypt: ${callCount}`);
|
||||
if (callCount == 1) {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
prom2 = encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
expect(prom2.isFulfilled()).toBe(
|
||||
false, 'second attemptDecryption resolved too soon');
|
||||
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return encryptedEvent.attemptDecryption(crypto).then(() => {
|
||||
expect(callCount).toEqual(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
|
||||
// make sure the second attemptDecryption resolves
|
||||
return prom2;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
-15
@@ -1,24 +1,27 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var Filter = sdk.Filter;
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const Filter = sdk.Filter;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("Filter", function() {
|
||||
var filterId = "f1lt3ring15g00d4ursoul";
|
||||
var userId = "@sir_arthur_david:humming.tiger";
|
||||
var filter;
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
const userId = "@sir_arthur_david:humming.tiger";
|
||||
let filter;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
describe("fromJson", function() {
|
||||
it("create a new Filter from the provided values", function() {
|
||||
var definition = {
|
||||
event_fields: ["type", "content"]
|
||||
const definition = {
|
||||
event_fields: ["type", "content"],
|
||||
};
|
||||
var f = Filter.fromJson(userId, filterId, definition);
|
||||
const f = Filter.fromJson(userId, filterId, definition);
|
||||
expect(f.getDefinition()).toEqual(definition);
|
||||
expect(f.userId).toEqual(userId);
|
||||
expect(f.filterId).toEqual(filterId);
|
||||
@@ -31,17 +34,17 @@ describe("Filter", function() {
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
timeline: {
|
||||
limit: 10
|
||||
}
|
||||
}
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDefinition/getDefinition", function() {
|
||||
it("should set and get the filter body", function() {
|
||||
var definition = {
|
||||
event_format: "client"
|
||||
const definition = {
|
||||
event_format: "client",
|
||||
};
|
||||
filter.setDefinition(definition);
|
||||
expect(filter.getDefinition()).toEqual(definition);
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
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 strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
const InteractiveAuth = sdk.InteractiveAuth;
|
||||
const MatrixError = sdk.MatrixError;
|
||||
|
||||
import expect from 'expect';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
class FakeClient {
|
||||
generateClientSecret() {
|
||||
return "testcl1Ent5EcreT";
|
||||
}
|
||||
}
|
||||
|
||||
describe("InteractiveAuth", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
it("should start an auth stage and complete it", function(done) {
|
||||
const doRequest = expect.createSpy();
|
||||
const stateUpdated = expect.createSpy();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: ["logintype"] },
|
||||
],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.andCall(function(stage) {
|
||||
logger.log('aaaa');
|
||||
expect(stage).toEqual("logintype");
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = {"a": "b"};
|
||||
doRequest.andCall(function(authData) {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return Promise.resolve(requestRes);
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(1);
|
||||
expect(stateUpdated.calls.length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", function(done) {
|
||||
const doRequest = expect.createSpy();
|
||||
const stateUpdated = expect.createSpy();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
stateUpdated: stateUpdated,
|
||||
doRequest: doRequest,
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toBe(undefined);
|
||||
expect(ia.getStageParams("logintype")).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.andCall(function(authData) {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({});
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: ["logintype"] },
|
||||
],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = {"a": "b"};
|
||||
stateUpdated.andCall(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// submitAuthDict should trigger another call to doRequest
|
||||
doRequest.andCall(function(authData) {
|
||||
logger.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return Promise.resolve(requestRes);
|
||||
});
|
||||
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(2);
|
||||
expect(stateUpdated.calls.length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import expect from 'expect';
|
||||
import TestClient from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new TestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it('should store "access_token" and "user_id" if in response', async function() {
|
||||
const response = { user_id: 1, access_token: Date.now().toString(16) };
|
||||
|
||||
client.httpBackend.when('POST', '/login').respond(200, response);
|
||||
client.httpBackend.flush('/login', 1, 100);
|
||||
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
|
||||
|
||||
expect(client.client.getAccessToken()).toBe(response.access_token);
|
||||
expect(client.client.getUserId()).toBe(response.user_id);
|
||||
});
|
||||
});
|
||||
+198
-119
@@ -1,44 +1,52 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var MatrixClient = sdk.MatrixClient;
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
var userId = "@alice:bar";
|
||||
var identityServerUrl = "https://identity.server";
|
||||
var identityServerDomain = "identity.server";
|
||||
var client, store, scheduler;
|
||||
const userId = "@alice:bar";
|
||||
const identityServerUrl = "https://identity.server";
|
||||
const identityServerDomain = "identity.server";
|
||||
let client;
|
||||
let store;
|
||||
let scheduler;
|
||||
let clock;
|
||||
|
||||
var KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
var PUSH_RULES_RESPONSE = {
|
||||
const PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {}
|
||||
data: {},
|
||||
};
|
||||
|
||||
var FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
|
||||
var FILTER_RESPONSE = {
|
||||
const FILTER_RESPONSE = {
|
||||
method: "POST",
|
||||
path: FILTER_PATH,
|
||||
data: { filter_id: "f1lt3r" }
|
||||
data: { filter_id: "f1lt3r" },
|
||||
};
|
||||
|
||||
var SYNC_DATA = {
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: { events: [] },
|
||||
rooms: {}
|
||||
rooms: {},
|
||||
};
|
||||
|
||||
var SYNC_RESPONSE = {
|
||||
const SYNC_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: SYNC_DATA
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
var httpLookups = [
|
||||
let httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
// method: "GET",
|
||||
@@ -51,17 +59,18 @@ describe("MatrixClient", function() {
|
||||
// }
|
||||
// items are popped off when processed and block if no items left.
|
||||
];
|
||||
var pendingLookup = null;
|
||||
let acceptKeepalives;
|
||||
let pendingLookup = null;
|
||||
function httpReq(cb, method, path, qp, data, prefix) {
|
||||
if (path === KEEP_ALIVE_PATH) {
|
||||
return q();
|
||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
var next = httpLookups.shift();
|
||||
var logLine = (
|
||||
const next = httpLookups.shift();
|
||||
const logLine = (
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
console.log(logLine);
|
||||
logger.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (pendingLookup) {
|
||||
@@ -72,20 +81,20 @@ describe("MatrixClient", function() {
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path
|
||||
method + " " + path,
|
||||
);
|
||||
}
|
||||
pendingLookup = {
|
||||
promise: q.defer().promise,
|
||||
promise: Promise.defer().promise,
|
||||
method: method,
|
||||
path: path
|
||||
path: path,
|
||||
};
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response"
|
||||
(next.error ? "BAD" : "GOOD") + " response",
|
||||
);
|
||||
if (next.expectBody) {
|
||||
expect(next.expectBody).toEqual(data);
|
||||
@@ -101,31 +110,39 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
return q.reject({
|
||||
return Promise.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error
|
||||
data: next.error,
|
||||
});
|
||||
}
|
||||
return q(next.data);
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
return q.defer().promise;
|
||||
return Promise.defer().promise;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
jasmine.Clock.useMock();
|
||||
scheduler = jasmine.createSpyObj("scheduler", [
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction"
|
||||
]);
|
||||
store = jasmine.createSpyObj("store", [
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||
store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
|
||||
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
|
||||
]);
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
|
||||
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
|
||||
"getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
@@ -133,18 +150,17 @@ describe("MatrixClient", function() {
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId
|
||||
userId: userId,
|
||||
});
|
||||
// FIXME: We shouldn't be yanking _http like this.
|
||||
client._http = jasmine.createSpyObj("httpApi", [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent"
|
||||
]);
|
||||
client._http.authedRequest.andCallFake(httpReq);
|
||||
client._http.authedRequestWithPrefix.andCallFake(httpReq);
|
||||
client._http.requestWithPrefix.andCallFake(httpReq);
|
||||
client._http = [
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||
client._http.authedRequest.andCall(httpReq);
|
||||
client._http.request.andCall(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
acceptKeepalives = true;
|
||||
pendingLookup = null;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
@@ -153,60 +169,102 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
clock.uninstall();
|
||||
// need to re-stub the requests with NOPs because there are no guarantees
|
||||
// clients from previous tests will be GC'd before the next test. This
|
||||
// means they may call /events and then fail an expect() which will fail
|
||||
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
||||
// promises to stop the client from continuing to run.
|
||||
client._http.authedRequest.andCallFake(function() {
|
||||
return q.defer().promise;
|
||||
});
|
||||
client._http.authedRequestWithPrefix.andCallFake(function() {
|
||||
return q.defer().promise;
|
||||
client._http.authedRequest.andCall(function() {
|
||||
return Promise.defer().promise;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", function(done) {
|
||||
it("should not POST /filter if a matching filter already exists", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
var filterId = "ehfewf";
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.andReturn(filterId);
|
||||
var filter = new sdk.Filter(0, filterId);
|
||||
const filter = new sdk.Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
store.getFilter.andReturn(filter);
|
||||
client.startClient();
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
client.removeListener("sync", syncListener);
|
||||
resolve();
|
||||
} else if (state === "ERROR") {
|
||||
reject(new Error("sync error"));
|
||||
}
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
await syncPromise;
|
||||
});
|
||||
|
||||
describe("getSyncState", function() {
|
||||
|
||||
it("should return null if the client isn't started", function() {
|
||||
expect(client.getSyncState()).toBeNull();
|
||||
expect(client.getSyncState()).toBe(null);
|
||||
});
|
||||
|
||||
it("should return the same sync state as emitted sync events", function(done) {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
it("should return the same sync state as emitted sync events", async function() {
|
||||
const syncingPromise = new Promise((resolve) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
client.removeListener("sync", syncListener);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
await syncingPromise;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateFilter", function() {
|
||||
it("should POST createFilter if no id is present in localStorage", function() {
|
||||
});
|
||||
it("should use an existing filter if id is present in localStorage", function() {
|
||||
});
|
||||
it("should handle localStorage filterId missing from the server", function(done) {
|
||||
function getFilterName(userId, suffix) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
||||
}
|
||||
const invalidFilterId = 'invalidF1lt3r';
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: FILTER_PATH + '/' + invalidFilterId,
|
||||
error: {
|
||||
errcode: "M_UNKNOWN",
|
||||
name: "M_UNKNOWN",
|
||||
message: "No row found",
|
||||
data: { errcode: "M_UNKNOWN", error: "No row found" },
|
||||
httpStatus: 404,
|
||||
},
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
store.getFilterIdByName.andReturn(invalidFilterId);
|
||||
|
||||
const filterName = getFilterName(client.credentials.userId);
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
const filter = new sdk.Filter(client.credentials.userId);
|
||||
|
||||
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
|
||||
done();
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", function() {
|
||||
client.startClient();
|
||||
it("should return false if there is no request waiting", async function() {
|
||||
await client.startClient();
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
@@ -214,7 +272,7 @@ describe("MatrixClient", function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
@@ -223,7 +281,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jasmine.Clock.tick(1);
|
||||
clock.tick(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -237,19 +295,21 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("should work on /sync", function(done) {
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false"
|
||||
true, "retryImmediately returned false",
|
||||
);
|
||||
jasmine.Clock.tick(1);
|
||||
clock.tick(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
clock.tick(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -261,7 +321,7 @@ describe("MatrixClient", function() {
|
||||
it("should work on /pushrules", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
@@ -271,7 +331,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jasmine.Clock.tick(1);
|
||||
clock.tick(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -285,12 +345,11 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("emitted sync events", function() {
|
||||
|
||||
function syncChecker(expectedStates, done) {
|
||||
return function syncListener(state, old) {
|
||||
var expected = expectedStates.shift();
|
||||
console.log(
|
||||
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected
|
||||
const expected = expectedStates.shift();
|
||||
logger.log(
|
||||
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
|
||||
);
|
||||
if (!expected) {
|
||||
done();
|
||||
@@ -303,50 +362,59 @@ describe("MatrixClient", function() {
|
||||
done();
|
||||
}
|
||||
// standard retry time is 5 to 10 seconds
|
||||
jasmine.Clock.tick(10000);
|
||||
clock.tick(10000);
|
||||
};
|
||||
}
|
||||
|
||||
it("should transition null -> PREPARED after the first /sync", function(done) {
|
||||
var expectedStates = [];
|
||||
const expectedStates = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition null -> ERROR after a failed /filter", function(done) {
|
||||
var expectedStates = [];
|
||||
const expectedStates = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
expectedStates.push(["ERROR", null]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> PREPARED after /sync if prev failed",
|
||||
it("should transition ERROR -> CATCHUP after /sync if prev failed",
|
||||
function(done) {
|
||||
var expectedStates = [];
|
||||
const expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {},
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA,
|
||||
});
|
||||
|
||||
expectedStates.push(["ERROR", null]);
|
||||
expectedStates.push(["PREPARED", "ERROR"]);
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
var expectedStates = [];
|
||||
const expectedStates = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
@@ -354,23 +422,29 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
|
||||
var expectedStates = [];
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed",
|
||||
function(done) {
|
||||
var expectedStates = [];
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
@@ -383,7 +457,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
var expectedStates = [];
|
||||
const expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
@@ -394,18 +468,25 @@ describe("MatrixClient", function() {
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
|
||||
var expectedStates = [];
|
||||
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH,
|
||||
error: { errcode: "KEEPALIVE_FAIL" },
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
@@ -413,7 +494,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("inviteByEmail", function() {
|
||||
var roomId = "!foo:bar";
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
it("should send an invite HTTP POST", function() {
|
||||
httpLookups = [{
|
||||
@@ -423,17 +504,15 @@ describe("MatrixClient", function() {
|
||||
expectBody: {
|
||||
id_server: identityServerDomain,
|
||||
medium: "email",
|
||||
address: "alice@gmail.com"
|
||||
}
|
||||
address: "alice@gmail.com",
|
||||
},
|
||||
}];
|
||||
client.inviteByEmail(roomId, "alice@gmail.com");
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("guest rooms", function() {
|
||||
|
||||
it("should only do /sync calls (without filter/pushrules)", function(done) {
|
||||
httpLookups = []; // no /pushrules or /filter
|
||||
httpLookups.push({
|
||||
@@ -442,7 +521,7 @@ describe("MatrixClient", function() {
|
||||
data: SYNC_DATA,
|
||||
thenCall: function() {
|
||||
done();
|
||||
}
|
||||
},
|
||||
});
|
||||
client.setGuest(true);
|
||||
client.startClient();
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
"use strict";
|
||||
var PushProcessor = require("../../lib/pushprocessor");
|
||||
var MatrixEvent = MatrixEvent;
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const PushProcessor = require("../../lib/pushprocessor");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe('NotificationService', function() {
|
||||
var testUserId = "@ali:matrix.org";
|
||||
var testDisplayName = "Alice M";
|
||||
var testRoomId = "!fl1bb13:localhost";
|
||||
const testUserId = "@ali:matrix.org";
|
||||
const testDisplayName = "Alice M";
|
||||
const testRoomId = "!fl1bb13:localhost";
|
||||
|
||||
var testEvent;
|
||||
let testEvent;
|
||||
|
||||
var pushProcessor;
|
||||
let pushProcessor;
|
||||
|
||||
// These would be better if individual rules were configured in the tests themselves.
|
||||
var matrixClient = {
|
||||
const matrixClient = {
|
||||
getRoom: function() {
|
||||
return {
|
||||
currentState: {
|
||||
getMember: function() {
|
||||
return {
|
||||
name: testDisplayName
|
||||
name: testDisplayName,
|
||||
};
|
||||
},
|
||||
members: {}
|
||||
}
|
||||
getJoinedMemberCount: function() {
|
||||
return 0;
|
||||
},
|
||||
members: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
credentials: {
|
||||
userId: testUserId
|
||||
userId: testUserId,
|
||||
},
|
||||
pushRules: {
|
||||
"device": {},
|
||||
@@ -38,91 +43,91 @@ describe('NotificationService', function() {
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
"value": "default",
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
"set_tweak": "highlight",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"pattern": "ali",
|
||||
"rule_id": ".m.rule.contains_user_name"
|
||||
"rule_id": ".m.rule.contains_user_name",
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
"value": "default",
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
"set_tweak": "highlight",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"pattern": "coffee",
|
||||
"rule_id": "coffee"
|
||||
"rule_id": "coffee",
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
"value": "default",
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
"set_tweak": "highlight",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"pattern": "foo*bar",
|
||||
"rule_id": "foobar"
|
||||
"rule_id": "foobar",
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
"value": "default",
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
"set_tweak": "highlight",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"pattern": "p[io]ng",
|
||||
"rule_id": "pingpong"
|
||||
"rule_id": "pingpong",
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
"value": "default",
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
"set_tweak": "highlight",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"pattern": "I ate [0-9] pies",
|
||||
"rule_id": "pies"
|
||||
"rule_id": "pies",
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
"value": "default",
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
"set_tweak": "highlight",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"pattern": "b[!ai]ke",
|
||||
"rule_id": "bakebike"
|
||||
}
|
||||
"rule_id": "bakebike",
|
||||
},
|
||||
],
|
||||
"override": [
|
||||
{
|
||||
@@ -130,70 +135,70 @@ describe('NotificationService', function() {
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
"value": "default",
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
"set_tweak": "highlight",
|
||||
},
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "contains_display_name"
|
||||
}
|
||||
"kind": "contains_display_name",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.contains_display_name"
|
||||
"rule_id": ".m.rule.contains_display_name",
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
}
|
||||
"value": "default",
|
||||
},
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"is": "2",
|
||||
"kind": "room_member_count"
|
||||
}
|
||||
"kind": "room_member_count",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.room_one_to_one"
|
||||
}
|
||||
"rule_id": ".m.rule.room_one_to_one",
|
||||
},
|
||||
],
|
||||
"room": [],
|
||||
"sender": [],
|
||||
"underride": [
|
||||
{
|
||||
"actions": [
|
||||
"dont-notify"
|
||||
"dont-notify",
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "content.msgtype",
|
||||
"kind": "event_match",
|
||||
"pattern": "m.notice"
|
||||
}
|
||||
"pattern": "m.notice",
|
||||
},
|
||||
],
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.suppress_notices"
|
||||
"rule_id": ".m.rule.suppress_notices",
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
"value": false,
|
||||
},
|
||||
],
|
||||
"conditions": [],
|
||||
"enabled": true,
|
||||
"rule_id": ".m.rule.fallback"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"rule_id": ".m.rule.fallback",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -204,8 +209,8 @@ describe('NotificationService', function() {
|
||||
event: true,
|
||||
content: {
|
||||
body: "",
|
||||
msgtype: "m.text"
|
||||
}
|
||||
msgtype: "m.text",
|
||||
},
|
||||
});
|
||||
pushProcessor = new PushProcessor(matrixClient);
|
||||
});
|
||||
@@ -214,25 +219,25 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on a user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID with an @.', function() {
|
||||
testEvent.event.content.body = "Hello @ali, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a partial user ID without @.', function() {
|
||||
testEvent.event.content.body = "Hello ali, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive user ID.', function() {
|
||||
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -240,13 +245,13 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on a display name.', function() {
|
||||
testEvent.event.content.body = "Hello Alice M, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on a case-insensitive display name.', function() {
|
||||
testEvent.event.content.body = "Hello ALICE M, how are you?";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -254,25 +259,25 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on a bing word.', function() {
|
||||
testEvent.event.content.body = "I really like coffee";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on case-insensitive bing words.', function() {
|
||||
testEvent.event.content.body = "Coffee is great";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on wildcard (.*) bing words.', function() {
|
||||
testEvent.event.content.body = "It was foomahbar I think.";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character group ([abc]) bing words.', function() {
|
||||
testEvent.event.content.body = "Ping!";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "Pong!";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
@@ -281,13 +286,13 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on character range ([a-z]) bing words.', function() {
|
||||
testEvent.event.content.body = "I ate 6 pies";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character negation ([!a]) bing words.', function() {
|
||||
testEvent.event.content.body = "boke";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "bake";
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
@@ -298,7 +303,7 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should gracefully handle bad input.', function() {
|
||||
testEvent.event.content.body = { "foo": "bar" };
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,97 +1,100 @@
|
||||
"use strict";
|
||||
|
||||
var callbacks = require("../../lib/realtime-callbacks");
|
||||
var test_utils = require("../test-utils.js");
|
||||
import 'source-map-support/register';
|
||||
const callbacks = require("../../lib/realtime-callbacks");
|
||||
const testUtils = require("../test-utils.js");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
|
||||
describe("realtime-callbacks", function() {
|
||||
var clock = jasmine.Clock;
|
||||
var fakeDate;
|
||||
let clock;
|
||||
|
||||
function tick(millis) {
|
||||
// make sure we tick the fakedate first, otherwise nothing will happen!
|
||||
fakeDate += millis;
|
||||
clock.tick(millis);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
test_utils.beforeEach(this);
|
||||
clock.useMock();
|
||||
fakeDate = Date.now();
|
||||
callbacks.setNow(function() { return fakeDate; });
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
const fakeDate = clock.Date;
|
||||
callbacks.setNow(fakeDate.now.bind(fakeDate));
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
callbacks.setNow();
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
describe("setTimeout", function() {
|
||||
it("should call the callback after the timeout", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
const callback = expect.createSpy();
|
||||
callbacks.setTimeout(callback, 100);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should default to a zero timeout", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
const callback = expect.createSpy();
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should pass any parameters to the callback", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
const callback = expect.createSpy();
|
||||
callbacks.setTimeout(callback, 0, "a", "b", "c");
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalledWith("a", "b", "c");
|
||||
});
|
||||
|
||||
it("should set 'this' to the global object", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
callback.andCallFake(function() {
|
||||
expect(this).toBe(global);
|
||||
expect(this.console).toBeDefined();
|
||||
});
|
||||
let passed = false;
|
||||
const callback = function() {
|
||||
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
|
||||
passed = true;
|
||||
};
|
||||
callbacks.setTimeout(callback);
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
expect(passed).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle timeouts of several seconds", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
const callback = expect.createSpy();
|
||||
callbacks.setTimeout(callback, 2000);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
for (var i = 0; i < 4; i++) {
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
tick(500);
|
||||
}
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call multiple callbacks in the right order", function() {
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
var callback3 = jasmine.createSpy("callback3");
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
const callback3 = expect.createSpy();
|
||||
callbacks.setTimeout(callback2, 200);
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
callbacks.setTimeout(callback3, 300);
|
||||
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
@@ -99,81 +102,83 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should treat -ve timeouts the same as a zero timeout", function() {
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
|
||||
// check that cb1 is called before cb2
|
||||
callback1.andCallFake(function() {
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
callback1.andCall(function() {
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
callbacks.setTimeout(callback2, -100);
|
||||
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not get confused by chained calls", function() {
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
callback1.andCallFake(function() {
|
||||
const callback2 = expect.createSpy();
|
||||
const callback1 = expect.createSpy();
|
||||
callback1.andCall(function() {
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
// the fake timer won't actually run callbacks registered during
|
||||
// one tick until the next tick.
|
||||
tick(1);
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should be immune to exceptions", function() {
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
callback1.andCallFake(function() {
|
||||
const callback1 = expect.createSpy();
|
||||
callback1.andCall(function() {
|
||||
throw new Error("prepare to die");
|
||||
});
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
const callback2 = expect.createSpy();
|
||||
callbacks.setTimeout(callback1, 0);
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("cancelTimeout", function() {
|
||||
it("should cancel a pending timeout", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
var k = callbacks.setTimeout(callback);
|
||||
const callback = expect.createSpy();
|
||||
const k = callbacks.setTimeout(callback);
|
||||
callbacks.clearTimeout(k);
|
||||
tick(0);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not affect sooner timeouts", function() {
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
var k = callbacks.setTimeout(callback2, 200);
|
||||
const k = callbacks.setTimeout(callback2, 200);
|
||||
callbacks.clearTimeout(k);
|
||||
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
|
||||
tick(150);
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+114
-56
@@ -1,22 +1,25 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var RoomMember = sdk.RoomMember;
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("RoomMember", function() {
|
||||
var roomId = "!foo:bar";
|
||||
var userA = "@alice:bar";
|
||||
var userB = "@bertha:bar";
|
||||
var userC = "@clarissa:bar";
|
||||
var member;
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bertha:bar";
|
||||
const userC = "@clarissa:bar";
|
||||
let member;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
member = new RoomMember(roomId, userA);
|
||||
});
|
||||
|
||||
describe("getAvatarUrl", function() {
|
||||
var hsUrl = "https://my.home.server";
|
||||
const hsUrl = "https://my.home.server";
|
||||
|
||||
it("should return the URL from m.room.member preferentially", function() {
|
||||
member.events.member = utils.mkEvent({
|
||||
@@ -27,31 +30,31 @@ describe("RoomMember", function() {
|
||||
user: userA,
|
||||
content: {
|
||||
membership: "join",
|
||||
avatar_url: "mxc://flibble/wibble"
|
||||
}
|
||||
avatar_url: "mxc://flibble/wibble",
|
||||
},
|
||||
});
|
||||
var url = member.getAvatarUrl(hsUrl);
|
||||
const url = member.getAvatarUrl(hsUrl);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.member event", function() {
|
||||
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.member and allowDefault=false",
|
||||
function() {
|
||||
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPowerLevelEvent", function() {
|
||||
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
|
||||
var event = utils.mkEvent({
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
@@ -59,16 +62,16 @@ describe("RoomMember", function() {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@bertha:bar": 200,
|
||||
"@invalid:user": 10 // shouldn't barf on this.
|
||||
}
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
},
|
||||
},
|
||||
event: true
|
||||
event: true,
|
||||
});
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(20);
|
||||
expect(member.powerLevelNorm).toEqual(10);
|
||||
|
||||
var memberB = new RoomMember(roomId, userB);
|
||||
const memberB = new RoomMember(roomId, userB);
|
||||
memberB.setPowerLevelEvent(event);
|
||||
expect(memberB.powerLevel).toEqual(200);
|
||||
expect(memberB.powerLevelNorm).toEqual(100);
|
||||
@@ -76,7 +79,7 @@ describe("RoomMember", function() {
|
||||
|
||||
it("should emit 'RoomMember.powerLevel' if the power level changes.",
|
||||
function() {
|
||||
var event = utils.mkEvent({
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
@@ -84,12 +87,12 @@ describe("RoomMember", function() {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@bertha:bar": 200,
|
||||
"@invalid:user": 10 // shouldn't barf on this.
|
||||
}
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
},
|
||||
},
|
||||
event: true
|
||||
event: true,
|
||||
});
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
@@ -105,7 +108,7 @@ describe("RoomMember", function() {
|
||||
|
||||
it("should honour power levels of zero.",
|
||||
function() {
|
||||
var event = utils.mkEvent({
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
@@ -113,11 +116,11 @@ describe("RoomMember", function() {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": 0,
|
||||
}
|
||||
},
|
||||
},
|
||||
event: true
|
||||
event: true,
|
||||
});
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
@@ -138,21 +141,21 @@ describe("RoomMember", function() {
|
||||
describe("setTypingEvent", function() {
|
||||
it("should set 'typing'", function() {
|
||||
member.typing = false;
|
||||
var memberB = new RoomMember(roomId, userB);
|
||||
const memberB = new RoomMember(roomId, userB);
|
||||
memberB.typing = true;
|
||||
var memberC = new RoomMember(roomId, userC);
|
||||
const memberC = new RoomMember(roomId, userC);
|
||||
memberC.typing = true;
|
||||
|
||||
var event = utils.mkEvent({
|
||||
const event = utils.mkEvent({
|
||||
type: "m.typing",
|
||||
user: userA,
|
||||
room: roomId,
|
||||
content: {
|
||||
user_ids: [
|
||||
userA, userC
|
||||
]
|
||||
userA, userC,
|
||||
],
|
||||
},
|
||||
event: true
|
||||
event: true,
|
||||
});
|
||||
member.setTypingEvent(event);
|
||||
memberB.setTypingEvent(event);
|
||||
@@ -165,17 +168,17 @@ describe("RoomMember", function() {
|
||||
|
||||
it("should emit 'RoomMember.typing' if the typing state changes",
|
||||
function() {
|
||||
var event = utils.mkEvent({
|
||||
const event = utils.mkEvent({
|
||||
type: "m.typing",
|
||||
room: roomId,
|
||||
content: {
|
||||
user_ids: [
|
||||
userA, userC
|
||||
]
|
||||
userA, userC,
|
||||
],
|
||||
},
|
||||
event: true
|
||||
event: true,
|
||||
});
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.typing", function(ev, mem) {
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(event);
|
||||
@@ -189,21 +192,30 @@ describe("RoomMember", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOutOfBand", function() {
|
||||
it("should be set by markOutOfBand", function() {
|
||||
const member = new RoomMember();
|
||||
expect(member.isOutOfBand()).toEqual(false);
|
||||
member.markOutOfBand();
|
||||
expect(member.isOutOfBand()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setMembershipEvent", function() {
|
||||
var joinEvent = utils.mkMembership({
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: "Alice"
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
var inviteEvent = utils.mkMembership({
|
||||
const inviteEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "invite",
|
||||
user: userB,
|
||||
skey: userA,
|
||||
room: roomId
|
||||
room: roomId,
|
||||
});
|
||||
|
||||
it("should set 'membership' and assign the event to 'events.member'.",
|
||||
@@ -218,36 +230,38 @@ describe("RoomMember", function() {
|
||||
|
||||
it("should set 'name' based on user_id, displayname and room state",
|
||||
function() {
|
||||
var roomState = {
|
||||
const roomState = {
|
||||
getStateEvents: function(type) {
|
||||
if (type !== "m.room.member") { return []; }
|
||||
if (type !== "m.room.member") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userB
|
||||
user: userB,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userC, name: "Alice"
|
||||
user: userC, name: "Alice",
|
||||
}),
|
||||
joinEvent
|
||||
joinEvent,
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
}
|
||||
},
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual("Alice"); // prefer displayname
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).not.toEqual("Alice"); // it should disambig.
|
||||
expect(member.name).toNotEqual("Alice"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
expect(member.name.indexOf(userA)).toNotEqual(-1);
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.membership", function(ev, mem) {
|
||||
emitCount += 1;
|
||||
expect(mem).toEqual(member);
|
||||
@@ -260,7 +274,7 @@ describe("RoomMember", function() {
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.name' if the name changes", function() {
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.name", function(ev, mem) {
|
||||
emitCount += 1;
|
||||
expect(mem).toEqual(member);
|
||||
@@ -272,7 +286,51 @@ describe("RoomMember", function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should set 'name' to user_id if it is just whitespace", function() {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: " \u200b ",
|
||||
});
|
||||
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual(userA); // it should fallback because all whitespace
|
||||
});
|
||||
|
||||
it("should disambiguate users on a fuzzy displayname match", function() {
|
||||
const joinEvent = utils.mkMembership({
|
||||
event: true,
|
||||
mship: "join",
|
||||
user: userA,
|
||||
room: roomId,
|
||||
name: "Alíce\u200b", // note diacritic and zero width char
|
||||
});
|
||||
|
||||
const roomState = {
|
||||
getStateEvents: function(type) {
|
||||
if (type !== "m.room.member") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userC, name: "Alice",
|
||||
}),
|
||||
joinEvent,
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
},
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).toNotEqual("Alíce"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).toNotEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
+329
-120
@@ -1,35 +1,41 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var RoomState = sdk.RoomState;
|
||||
var RoomMember = sdk.RoomMember;
|
||||
var utils = require("../test-utils");
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const RoomState = sdk.RoomState;
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("RoomState", function() {
|
||||
var roomId = "!foo:bar";
|
||||
var userA = "@alice:bar";
|
||||
var userB = "@bob:bar";
|
||||
var state;
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bob:bar";
|
||||
const userC = "@cleo:bar";
|
||||
const userLazy = "@lazy:bar";
|
||||
|
||||
let state;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
state = new RoomState(roomId);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ // userA joined
|
||||
event: true, mship: "join", user: userA, room: roomId
|
||||
event: true, mship: "join", user: userA, room: roomId,
|
||||
}),
|
||||
utils.mkMembership({ // userB joined
|
||||
event: true, mship: "join", user: userB, room: roomId
|
||||
event: true, mship: "join", user: userB, room: roomId,
|
||||
}),
|
||||
utils.mkEvent({ // Room name is "Room name goes here"
|
||||
type: "m.room.name", user: userA, room: roomId, event: true, content: {
|
||||
name: "Room name goes here"
|
||||
}
|
||||
name: "Room name goes here",
|
||||
},
|
||||
}),
|
||||
utils.mkEvent({ // Room creation
|
||||
type: "m.room.create", user: userA, room: roomId, event: true, content: {
|
||||
creator: userA
|
||||
}
|
||||
})
|
||||
creator: userA,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -40,11 +46,11 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
it("should return a member for each m.room.member event", function() {
|
||||
var members = state.getMembers();
|
||||
const members = state.getMembers();
|
||||
expect(members.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(members[0].userId)).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(members[1].userId)).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(members[0].userId)).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(members[1].userId)).toNotEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,19 +60,19 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
it("should return a member if they exist", function() {
|
||||
expect(state.getMember(userB)).toBeDefined();
|
||||
expect(state.getMember(userB)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return a member which changes as state changes", function() {
|
||||
var member = state.getMember(userB);
|
||||
const member = state.getMember(userB);
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.name).toEqual(userB);
|
||||
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({
|
||||
room: roomId, user: userB, mship: "leave", event: true,
|
||||
name: "BobGone"
|
||||
})
|
||||
name: "BobGone",
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(member.membership).toEqual("leave");
|
||||
@@ -75,20 +81,20 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
describe("getSentinelMember", function() {
|
||||
it("should return null if there is no member", function() {
|
||||
expect(state.getSentinelMember("@no-one:here")).toEqual(null);
|
||||
it("should return a member with the user id as name", function() {
|
||||
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here");
|
||||
});
|
||||
|
||||
it("should return a member which doesn't change when the state is updated",
|
||||
function() {
|
||||
var preLeaveUser = state.getSentinelMember(userA);
|
||||
const preLeaveUser = state.getSentinelMember(userA);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({
|
||||
room: roomId, user: userA, mship: "leave", event: true,
|
||||
name: "AliceIsGone"
|
||||
})
|
||||
name: "AliceIsGone",
|
||||
}),
|
||||
]);
|
||||
var postLeaveUser = state.getSentinelMember(userA);
|
||||
const postLeaveUser = state.getSentinelMember(userA);
|
||||
|
||||
expect(preLeaveUser.membership).toEqual("join");
|
||||
expect(preLeaveUser.name).toEqual(userA);
|
||||
@@ -111,33 +117,33 @@ describe("RoomState", function() {
|
||||
|
||||
it("should return a list of matching events if no state_key was specified",
|
||||
function() {
|
||||
var events = state.getStateEvents("m.room.member");
|
||||
const events = state.getStateEvents("m.room.member");
|
||||
expect(events.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).toNotEqual(-1);
|
||||
});
|
||||
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
function() {
|
||||
var event = state.getStateEvents("m.room.member", userA);
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toEqual({
|
||||
membership: "join"
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setStateEvents", function() {
|
||||
it("should emit 'RoomState.members' for each m.room.member event", function() {
|
||||
var memberEvents = [
|
||||
const memberEvents = [
|
||||
utils.mkMembership({
|
||||
user: "@cleo:bar", mship: "invite", room: roomId, event: true
|
||||
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: "@daisy:bar", mship: "join", room: roomId, event: true
|
||||
})
|
||||
user: "@daisy:bar", mship: "join", room: roomId, event: true,
|
||||
}),
|
||||
];
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.members", function(ev, st, mem) {
|
||||
expect(ev).toEqual(memberEvents[emitCount]);
|
||||
expect(st).toEqual(state);
|
||||
@@ -149,16 +155,17 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
it("should emit 'RoomState.newMember' for each new member added", function() {
|
||||
var memberEvents = [
|
||||
const memberEvents = [
|
||||
utils.mkMembership({
|
||||
user: "@cleo:bar", mship: "invite", room: roomId, event: true
|
||||
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: "@daisy:bar", mship: "join", room: roomId, event: true
|
||||
})
|
||||
user: "@daisy:bar", mship: "join", room: roomId, event: true,
|
||||
}),
|
||||
];
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.newMember", function(ev, st, mem) {
|
||||
expect(state.getMember(mem.userId)).toEqual(mem);
|
||||
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
|
||||
expect(mem.membership).toBeFalsy(); // not defined yet
|
||||
emitCount += 1;
|
||||
@@ -168,21 +175,21 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
it("should emit 'RoomState.events' for each state event", function() {
|
||||
var events = [
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
user: "@cleo:bar", mship: "invite", room: roomId, event: true
|
||||
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
|
||||
}),
|
||||
utils.mkEvent({
|
||||
user: userB, room: roomId, type: "m.room.topic", event: true,
|
||||
content: {
|
||||
topic: "boo!"
|
||||
}
|
||||
topic: "boo!",
|
||||
},
|
||||
}),
|
||||
utils.mkMessage({ // Not a state event
|
||||
user: userA, room: roomId, event: true
|
||||
})
|
||||
user: userA, room: roomId, event: true,
|
||||
}),
|
||||
];
|
||||
var emitCount = 0;
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.events", function(ev, st) {
|
||||
expect(ev).toEqual(events[emitCount]);
|
||||
expect(st).toEqual(state);
|
||||
@@ -198,39 +205,38 @@ describe("RoomState", function() {
|
||||
state.members[userA] = utils.mock(RoomMember);
|
||||
state.members[userB] = utils.mock(RoomMember);
|
||||
|
||||
var powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25
|
||||
}
|
||||
});
|
||||
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
|
||||
powerLevelEvent
|
||||
);
|
||||
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
|
||||
powerLevelEvent
|
||||
);
|
||||
});
|
||||
|
||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
|
||||
function() {
|
||||
var userC = "@cleo:bar";
|
||||
var memberEvent = utils.mkMembership({
|
||||
mship: "join", user: userC, room: roomId, event: true
|
||||
});
|
||||
var powerLevelEvent = utils.mkEvent({
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
|
||||
powerLevelEvent,
|
||||
);
|
||||
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
|
||||
powerLevelEvent,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
|
||||
function() {
|
||||
const memberEvent = utils.mkMembership({
|
||||
mship: "join", user: userC, room: roomId, event: true,
|
||||
});
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {},
|
||||
},
|
||||
});
|
||||
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
@@ -238,7 +244,7 @@ describe("RoomState", function() {
|
||||
|
||||
// TODO: We do this because we don't DI the RoomMember constructor
|
||||
// so we can't inject a mock :/ so we have to infer.
|
||||
expect(state.members[userC]).toBeDefined();
|
||||
expect(state.members[userC]).toBeTruthy();
|
||||
expect(state.members[userC].powerLevel).toEqual(10);
|
||||
});
|
||||
|
||||
@@ -247,24 +253,132 @@ describe("RoomState", function() {
|
||||
state.members[userA] = utils.mock(RoomMember);
|
||||
state.members[userB] = utils.mock(RoomMember);
|
||||
|
||||
var memberEvent = utils.mkMembership({
|
||||
user: userB, mship: "leave", room: roomId, event: true
|
||||
const memberEvent = utils.mkMembership({
|
||||
user: userB, mship: "leave", room: roomId, event: true,
|
||||
});
|
||||
state.setStateEvents([memberEvent]);
|
||||
|
||||
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
|
||||
expect(state.members[userA].setMembershipEvent).toNotHaveBeenCalled();
|
||||
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(
|
||||
memberEvent, state
|
||||
memberEvent, state,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setOutOfBandMembers", function() {
|
||||
it("should add a new member", function() {
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userLazy, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const member = state.getMember(userLazy);
|
||||
expect(member.userId).toEqual(userLazy);
|
||||
expect(member.isOutOfBand()).toEqual(true);
|
||||
});
|
||||
|
||||
it("should have no effect when not in correct status", function() {
|
||||
state.setOutOfBandMembers([utils.mkMembership({
|
||||
user: userLazy, mship: "join", room: roomId, event: true,
|
||||
})]);
|
||||
expect(state.getMember(userLazy)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should emit newMember when adding a member", function() {
|
||||
const userLazy = "@oob:hs";
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userLazy, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
let eventReceived = false;
|
||||
state.once('RoomState.newMember', (_, __, member) => {
|
||||
expect(member.userId).toEqual(userLazy);
|
||||
eventReceived = true;
|
||||
});
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
expect(eventReceived).toEqual(true);
|
||||
});
|
||||
|
||||
it("should never overwrite existing members", function() {
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(false);
|
||||
});
|
||||
|
||||
it("should emit members when updating a member", function() {
|
||||
const doesntExistYetUserId = "@doesntexistyet:hs";
|
||||
const oobMemberEvent = utils.mkMembership({
|
||||
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
let eventReceived = false;
|
||||
state.once('RoomState.members', (_, __, member) => {
|
||||
expect(member.userId).toEqual(doesntExistYetUserId);
|
||||
eventReceived = true;
|
||||
});
|
||||
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
expect(eventReceived).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clone", function() {
|
||||
it("should contain same information as original", function() {
|
||||
// include OOB members in copy
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([utils.mkMembership({
|
||||
user: userLazy, mship: "join", room: roomId, event: true,
|
||||
})]);
|
||||
const copy = state.clone();
|
||||
// check individual members
|
||||
[userA, userB, userLazy].forEach((userId) => {
|
||||
const member = state.getMember(userId);
|
||||
const memberCopy = copy.getMember(userId);
|
||||
expect(member.name).toEqual(memberCopy.name);
|
||||
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
|
||||
});
|
||||
// check member keys
|
||||
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
|
||||
// check join count
|
||||
expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount());
|
||||
});
|
||||
|
||||
it("should mark old copy as not waiting for out of band anymore", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
const copy = state.clone();
|
||||
copy.setOutOfBandMembers([utils.mkMembership({
|
||||
user: userA, mship: "join", room: roomId, event: true,
|
||||
})]);
|
||||
// should have no effect as it should be marked in status finished just like copy
|
||||
state.setOutOfBandMembers([utils.mkMembership({
|
||||
user: userLazy, mship: "join", room: roomId, event: true,
|
||||
})]);
|
||||
expect(state.getMember(userLazy)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return copy independent of original", function() {
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([utils.mkMembership({
|
||||
user: userLazy, mship: "join", room: roomId, event: true,
|
||||
})]);
|
||||
|
||||
expect(state.getMember(userLazy)).toBeFalsy();
|
||||
expect(state.getJoinedMemberCount()).toEqual(2);
|
||||
expect(copy.getJoinedMemberCount()).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTypingEvent", function() {
|
||||
it("should call setTypingEvent on each RoomMember", function() {
|
||||
var typingEvent = utils.mkEvent({
|
||||
const typingEvent = utils.mkEvent({
|
||||
type: "m.typing", room: roomId, event: true, content: {
|
||||
user_ids: [userA]
|
||||
}
|
||||
user_ids: [userA],
|
||||
},
|
||||
});
|
||||
// mock up the room members
|
||||
state.members[userA] = utils.mock(RoomMember);
|
||||
@@ -272,22 +386,15 @@ describe("RoomState", function() {
|
||||
state.setTypingEvent(typingEvent);
|
||||
|
||||
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
|
||||
typingEvent
|
||||
typingEvent,
|
||||
);
|
||||
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
|
||||
typingEvent
|
||||
typingEvent,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maySendStateEvent", function() {
|
||||
it("should say non-joined members may not send state",
|
||||
function() {
|
||||
expect(state.maySendStateEvent(
|
||||
'm.room.name', "@nobody:nowhere"
|
||||
)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should say any member may send state with no power level event",
|
||||
function() {
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
@@ -296,15 +403,15 @@ describe("RoomState", function() {
|
||||
it("should say members with power >=50 may send state with power level event " +
|
||||
"but no state default",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
// state_default: 50, "intentionally left blank"
|
||||
events_default: 25,
|
||||
users: {
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 50;
|
||||
|
||||
@@ -316,15 +423,15 @@ describe("RoomState", function() {
|
||||
|
||||
it("should obey state_default",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
users: {
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 30;
|
||||
powerLevelEvent.content.users[userB] = 29;
|
||||
@@ -337,18 +444,18 @@ describe("RoomState", function() {
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
events: {
|
||||
"m.room.other_thing": 76
|
||||
"m.room.other_thing": 76,
|
||||
},
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 80;
|
||||
powerLevelEvent.content.users[userB] = 50;
|
||||
@@ -363,15 +470,117 @@ describe("RoomState", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("maySendEvent", function() {
|
||||
it("should say non-joined members may not send events",
|
||||
function() {
|
||||
expect(state.maySendEvent(
|
||||
'm.room.message', "@nobody:nowhere"
|
||||
)).toEqual(false);
|
||||
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
|
||||
describe("getJoinedMemberCount", function() {
|
||||
beforeEach(() => {
|
||||
state = new RoomState(roomId);
|
||||
});
|
||||
|
||||
it("should update after adding joined member", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInvitedMemberCount", function() {
|
||||
beforeEach(() => {
|
||||
state = new RoomState(roomId);
|
||||
});
|
||||
|
||||
it("should update after adding invited member", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userA, room: roomId}),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setJoinedMemberCount", function() {
|
||||
beforeEach(() => {
|
||||
state = new RoomState(roomId);
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setJoinedMemberCount(100);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state, " +
|
||||
"also after clone", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
]);
|
||||
state.setJoinedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setInvitedMemberCount", function() {
|
||||
beforeEach(() => {
|
||||
state = new RoomState(roomId);
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userB, room: roomId}),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setInvitedMemberCount(100);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
|
||||
it("should, once used, override counting members from state, " +
|
||||
"also after clone", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userB, room: roomId}),
|
||||
]);
|
||||
state.setInvitedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maySendEvent", function() {
|
||||
it("should say any member may send events with no power level event",
|
||||
function() {
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
@@ -380,15 +589,15 @@ describe("RoomState", function() {
|
||||
|
||||
it("should obey events_default",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
users: {
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 26;
|
||||
powerLevelEvent.content.users[userB] = 24;
|
||||
@@ -404,18 +613,18 @@ describe("RoomState", function() {
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
events: {
|
||||
"m.room.other_thing": 33
|
||||
"m.room.other_thing": 33,
|
||||
},
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 40;
|
||||
powerLevelEvent.content.users[userB] = 30;
|
||||
|
||||
+602
-405
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user