Compare commits
1081 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7357952ec | |||
| b796246d9d | |||
| 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 | |||
| d9318a60e4 | |||
| 2fd569a61a | |||
| 1bd5d12665 | |||
| 02de5e96ba | |||
| bc56213010 | |||
| 34919d1b96 | |||
| cc08de9c64 | |||
| 6432a64442 | |||
| 1f18dabca0 | |||
| f74b49de4b | |||
| 6aeb265c19 | |||
| f10467e81f | |||
| 6001077c34 | |||
| ad6eec329d | |||
| d9867ba458 | |||
| 6dc7e624d3 | |||
| 24957a1445 | |||
| e2d67db5d4 | |||
| 6c59966339 | |||
| b4223d3790 | |||
| 9f6d9208f2 | |||
| 35d45f0280 | |||
| c288e6c7ec | |||
| bb946c65d1 | |||
| 13fe22bc86 | |||
| f139e6e6c2 | |||
| 364fe3ba47 | |||
| 8b9b37cc91 | |||
| 7895d1daa0 | |||
| 93a9f76f69 | |||
| 4e2edfc771 | |||
| f4d53e25cc | |||
| da324c020b | |||
| f63015e4c4 | |||
| 61cf53deee | |||
| 57ff963fae | |||
| 32744b23a0 | |||
| 6c25110682 | |||
| 188802c5d3 | |||
| a47e59f02c | |||
| 59d7935934 | |||
| ba616d2a25 | |||
| dc07038a27 | |||
| dd064ba0a1 | |||
| 3baea89c34 | |||
| 1412646a55 | |||
| c00a830cbb | |||
| fa28297add | |||
| 58a68106bc | |||
| ebd2ef6f95 | |||
| 809492d45d | |||
| 385c5d5469 | |||
| 9713ffedf2 | |||
| ecb31b5aaf | |||
| cee9a954ec | |||
| fc55858aa3 | |||
| b689dbb9c0 | |||
| 7dbc03942a | |||
| 425039c5b5 | |||
| 03d0aecc26 | |||
| abf903246c | |||
| 3fd601bda4 | |||
| 1896c0b62f | |||
| aa36571981 | |||
| abbe9d2bc7 | |||
| dff84c46f0 | |||
| 7614c6677c | |||
| 3d2a970457 | |||
| 50d60f5dd3 | |||
| 90c919e7e4 | |||
| 583ddc3e57 | |||
| 7ff1cf4e4a | |||
| 1556cc4479 | |||
| 32f7ca55df | |||
| ae3551ad78 | |||
| d0e90cd8c9 | |||
| cb6566a198 | |||
| 573a9140d3 | |||
| 6a02f39f0b | |||
| 7604bcc49a | |||
| 40c73e2079 | |||
| 7f113de790 | |||
| accb589892 | |||
| 4347f432b3 | |||
| 6d905563fc | |||
| e943bc46d8 | |||
| 7896b06bd7 | |||
| 0f153fdaf7 | |||
| 2e4a8f4fa5 | |||
| 53b9154fe2 | |||
| ce59b72308 | |||
| 4bf24f0fe4 | |||
| 23a38ae8f2 | |||
| 0ab3446d81 | |||
| adaf4bf92b | |||
| 60519c4e6b | |||
| 21a62f37de | |||
| 9feeb0c580 | |||
| 7c3104b2ae | |||
| 5ccbc0bbc6 | |||
| 9c47400a0c | |||
| 9cb032ec44 | |||
| 8c6e2591d9 | |||
| 0c8c7cf77a | |||
| 52edcc49c5 | |||
| 5eede573c4 | |||
| e9d60a252b | |||
| 17ec7daf23 | |||
| b18a4ee16b | |||
| f38f983a46 | |||
| e85c1e1231 | |||
| 1381a0226d | |||
| 4bec72a2bd | |||
| 11f02d2e24 | |||
| 09d08a5092 | |||
| 8c7d041628 | |||
| 0421e69f14 | |||
| 8b35ddae0a | |||
| 7243367a64 | |||
| fb388f5d2d | |||
| 0757a1dd1c | |||
| 06486f7ad0 | |||
| a9d8c58ea0 | |||
| beec36d484 | |||
| 37ea7edaa0 | |||
| 2c40932080 | |||
| 3777b3e211 | |||
| 2f7d7308a1 | |||
| 0e606c6fe2 | |||
| 3af35c8209 | |||
| a2aed99f56 | |||
| 523a684d3f | |||
| dc386bab46 | |||
| 69079a2f9a | |||
| df33f7aceb | |||
| d87e5471fa | |||
| 90101c0340 | |||
| 950fce80c8 | |||
| 9135c50b83 | |||
| 83bad6ee0d | |||
| 3404751eb9 | |||
| 0282021e09 | |||
| 526e1d59e9 | |||
| dbc3a9a500 | |||
| cff7c8a59f | |||
| 64b8047f01 | |||
| 11e4760935 | |||
| c21d5634bb | |||
| dc56d7f784 | |||
| 1ff1064295 | |||
| a493a0ddb3 | |||
| 7573171d05 | |||
| 989e7cf78b | |||
| 384c474800 | |||
| 1d2c705e13 | |||
| c469ff4c8d | |||
| c7575f3f16 | |||
| 415251dd70 | |||
| 458cc55dec | |||
| d2adb30ded | |||
| bbe41aa7b4 | |||
| ece9391878 | |||
| 64640287cf | |||
| 57f88b00ba | |||
| e618ad9589 | |||
| 69c4d7b66e | |||
| d7b3b91eec | |||
| 88cc63e2a2 | |||
| cdea96fa0a | |||
| cfc10fa82d | |||
| 9d8973bf1f | |||
| 7f95237e02 | |||
| e1415d9829 | |||
| 19a12b3c79 | |||
| bec41e4f94 | |||
| 5f177aeec4 | |||
| b422916452 | |||
| 10378c0e7f | |||
| fba4d5fb0a | |||
| 77101823f5 | |||
| 15bc608368 | |||
| dfc4b34d09 | |||
| ad9daecbd4 | |||
| d29302716d | |||
| 6c7d13f8ce | |||
| e15a2d138c | |||
| 8bc9c19278 | |||
| dd86fade11 | |||
| ba1991aa8f | |||
| f4fd8d9ba6 | |||
| 02be0f659a | |||
| c7be310bdf | |||
| 55d8f56f98 | |||
| ab35fff9e8 | |||
| fdbc7a3112 | |||
| 3c6bd4774d | |||
| a2861c5781 | |||
| eaf3fe16eb | |||
| 963eaf7ec7 | |||
| e6e5b9b748 | |||
| 9ad031c857 | |||
| a0d465cb34 | |||
| 2dcf5227f0 | |||
| 518e92027c | |||
| ebc95667b8 | |||
| ad5d07caf8 | |||
| b2d7abc0a1 | |||
| cc475e6392 | |||
| e4c6717bd5 | |||
| 53f813207e | |||
| 873fde27ac | |||
| 8d9d638953 | |||
| 2f93490054 | |||
| e22efc9dd5 | |||
| e7ac80cf2b | |||
| 4436087777 | |||
| f7bc11361c | |||
| a68b61dafe | |||
| 84c9876b3a | |||
| de864c489a | |||
| 2c277f7d96 | |||
| d0560f594d | |||
| 60b6310494 | |||
| abd27f9b75 | |||
| 3d316959f9 | |||
| 8aa3b79501 | |||
| f35409700a | |||
| b009739b9e | |||
| f007af741e | |||
| 3db4d9488b | |||
| 6b0fa84697 | |||
| 98b0cf2560 | |||
| 372759b6e4 | |||
| ec29b4ffeb | |||
| 95494933fd | |||
| 6fff29c07b | |||
| 6f7ed93b87 | |||
| 8e903c0531 | |||
| b90984a7f6 | |||
| 57006b7366 | |||
| db9ba52873 | |||
| 08b49c733a | |||
| 0f38764709 | |||
| 6040b50ceb | |||
| b88a207bde | |||
| 07bbe358ea | |||
| 39a5765888 | |||
| 9f91995f4e | |||
| 85f2754300 | |||
| 5833654aa6 | |||
| 899ff6cea2 | |||
| 3752429b65 | |||
| d13fbd0e3e | |||
| 5e18c84e53 | |||
| fc1d5c86f9 | |||
| c3ea913ae8 | |||
| f2336aaedf | |||
| 16ab2fd82a | |||
| 295fda027c | |||
| 28e6f00766 | |||
| b1988de753 | |||
| 0302eefd3c | |||
| 8ad5605e49 | |||
| 4b5539fe53 | |||
| b0becbc8d5 | |||
| 4ae353d3d3 | |||
| b4b8b4bfb8 | |||
| 909a8a0648 | |||
| 234c227fd5 | |||
| 78eded3bbd | |||
| f324e4c72f | |||
| 9328a12ccb | |||
| 51380f8116 | |||
| cfd96969fc | |||
| 0034bdf4ad | |||
| 00af1ce7d2 | |||
| 76f84c54db | |||
| bb4766c8c6 | |||
| e287e7591b | |||
| 48f7aca121 | |||
| a14f9e6d1c | |||
| 5fefcd8ce3 | |||
| 76f1d24c7b | |||
| 066dd77aba | |||
| 45a3bf63b2 | |||
| 75f2efffac | |||
| 0584ec3319 | |||
| 38e81ba61a | |||
| 164e4814af | |||
| abf908b14f | |||
| 1deb2e27d8 | |||
| 848ffe8a40 | |||
| 79c10c1b68 | |||
| 7e6eb89524 | |||
| 3d3e52b104 | |||
| 05326984df | |||
| e97e3c673f | |||
| f0ae46afc9 | |||
| aaf4371fae | |||
| 7728009ef3 | |||
| 46912431cc | |||
| 6a19e08381 | |||
| 43f392955d | |||
| 41d2076bd4 | |||
| 670d230f2e | |||
| 7970f3f5a5 | |||
| 567716c4f7 | |||
| 518e41c078 | |||
| bd600f65fb | |||
| 363b08c4d8 | |||
| 2150bdc444 | |||
| 5886b3358d | |||
| 8b887d8559 | |||
| 7278c38fa6 | |||
| 24ae4a8d1a | |||
| 923b9cad39 | |||
| e9f6e41550 | |||
| 2950417f70 | |||
| 39f641a851 | |||
| 95fff38dbb | |||
| 785326376a | |||
| 1baf14861c | |||
| 8e47fe2968 | |||
| 88827fab84 | |||
| ab0a06eea7 | |||
| 6a6db36088 | |||
| 6f3bdcfbb6 | |||
| 4c6d0a5128 | |||
| 5eff278454 | |||
| c8d1e210a3 | |||
| 8614632e54 | |||
| c3796c61cd | |||
| 6224aff882 | |||
| e506a1b2de | |||
| b40a6d1481 | |||
| 5c8f73019e | |||
| 3cfc4f8ba5 | |||
| 4d46251b15 | |||
| 977e33f1bd | |||
| daa0e6291e | |||
| 620417ed1b | |||
| 02196416e4 | |||
| 5ca4bc6b85 | |||
| 76c79ec299 | |||
| fc730d4637 | |||
| 41d5917bb6 | |||
| 122867e8ee | |||
| f3e5e03009 | |||
| 1b43e5ed98 | |||
| 9e65f12ddd | |||
| be297ddbcd | |||
| 8ee1d17ff7 | |||
| a2185fefc1 | |||
| f172272dd3 | |||
| 8a77b29d17 | |||
| 0a7efe3e8b | |||
| 2af947fc1a | |||
| 1499087098 | |||
| 672e96b90e | |||
| 3d5e2937e2 | |||
| bbf3b2637a | |||
| 04a1c4f1a2 | |||
| 5297855ad3 | |||
| a221674680 | |||
| 8db95f42fb | |||
| b42e5d5fcf | |||
| b1e2090eef | |||
| 8716185f4a | |||
| 3c2fad7c8d | |||
| 60a243f160 | |||
| a87cefa035 | |||
| 101d3952d3 | |||
| a01501b42c | |||
| d37369b463 | |||
| 0c3abcccf2 | |||
| 48d1bc3158 | |||
| 15e8784daf | |||
| 840b8f0bc0 | |||
| 7a4cc62280 | |||
| f8ec35691f | |||
| c5e7df8975 | |||
| 7bdab05785 | |||
| 197144dcda | |||
| ff990914b2 | |||
| 4bc2869522 | |||
| 1f1d743678 | |||
| 787c0ebabc | |||
| d559ad794a | |||
| 24655ac60e | |||
| 5fd0ea2f6f | |||
| eaf7b03bb1 | |||
| d375804cd6 | |||
| a24a9d35c4 | |||
| a0d81fccdb | |||
| b4e4aaff00 | |||
| 3a73b54e4a | |||
| 5ec0fce2a4 | |||
| 8b7497374f | |||
| 8cb180525e | |||
| 6df9d08dc1 | |||
| b3c06dd723 | |||
| 2a88b8db4e | |||
| 31c29b7e5e | |||
| 865db906e3 | |||
| 0b52a7e7c9 | |||
| 80f7220a7b | |||
| 2e664adb32 | |||
| 14ef9348be | |||
| 3c170bf063 | |||
| 3e67406a30 | |||
| d6075bb5bd | |||
| d8e56dad1b | |||
| 67872206ff | |||
| 43e7173c30 | |||
| e0ddd65922 | |||
| dfb2fa821d | |||
| fce7248ed5 | |||
| e68ab7d54a | |||
| 706966ffe9 | |||
| e3b4cb03e1 | |||
| 8011aab561 | |||
| a8d24798e6 | |||
| e4c38ac78c | |||
| 5fa6f0037f | |||
| a0df2a70cd | |||
| 0bab00c47c | |||
| f48fb34818 | |||
| 8810ff2256 | |||
| 17efc5163f | |||
| 3ce07a020d | |||
| 71abef0117 | |||
| a79270b8f8 | |||
| 87db054e22 | |||
| ae06dd2ab8 | |||
| 88c7293838 | |||
| 051e83582b | |||
| 57072bc4f4 | |||
| e8f77256de | |||
| 51fe73bc27 | |||
| 97003f7382 | |||
| c10218a1fa | |||
| 5c59a2ea3e | |||
| 0b79ac1386 | |||
| 4fe95f18b9 | |||
| db5ca49ee2 | |||
| d7158b575f | |||
| 678d70528e | |||
| 02b33766ee | |||
| 9bd45cf7c7 | |||
| c64aebdb17 | |||
| 387ad09c5f | |||
| ea3bd1450e | |||
| 8f4bd9c693 | |||
| cdb4bc5107 | |||
| 446faed9b5 | |||
| d36c928d95 | |||
| 3a3f25c1bc | |||
| 73e65bc18b | |||
| 445491c4ad | |||
| f12499c6bf | |||
| 8c6c65ab6c | |||
| b85e267fdb | |||
| c669d21af7 | |||
| 3e4cef89fd | |||
| a06d1f62d7 | |||
| 2802092231 | |||
| a419e241a6 | |||
| 7aa4bd7f46 | |||
| 2eec76bc1d | |||
| 13fcff9688 | |||
| 1174147d64 | |||
| de53b292a2 | |||
| b50d61428c | |||
| 4bc5343b67 | |||
| da560ffeff | |||
| 59965b1c59 | |||
| 431f4a4797 | |||
| 40c3fe558c | |||
| a12cd8d4a0 | |||
| ac7a469582 | |||
| 2e9376614f | |||
| cd9d1daf17 | |||
| bfa8dd0007 | |||
| 7e35ef258f | |||
| 8bd43a8d53 | |||
| 8b87c0045d | |||
| 77356f0007 | |||
| 4cd6f615b3 | |||
| 65ef1dfd75 | |||
| 5719d513b7 | |||
| b90697264c | |||
| 406a2bb001 | |||
| 3115043b94 | |||
| bfda04daea | |||
| 46504b8b9f | |||
| f48c9175e5 | |||
| bd4d8433ab | |||
| a00e318d73 | |||
| fcf1abb185 | |||
| 13cab79e04 | |||
| fc6ce20e14 | |||
| 9c49d26525 | |||
| d6299b634c | |||
| a6f64b5f03 | |||
| 465635444f | |||
| eedff29acb | |||
| 7c43d15ea5 | |||
| de32ac0c44 | |||
| 3d9d31d6b1 | |||
| b219836b3e | |||
| 26d9fed537 | |||
| d6ba39f292 | |||
| 8576ebce8f | |||
| f08152a1d8 | |||
| 6af2197183 | |||
| 4c7e6807d2 | |||
| 11f0513c62 | |||
| 3d57b4ce6a | |||
| 243bdd78f4 | |||
| b622960b32 | |||
| 06f927aa22 | |||
| f7ffed4b98 | |||
| 0576e4ca0c | |||
| 529fb23555 | |||
| 3543abf7bd | |||
| a5847485b9 | |||
| 2b659656cc | |||
| ac3aa5538f | |||
| c65f32f6a6 | |||
| 86a162c818 | |||
| 1987726a95 | |||
| d2537cd00c | |||
| 61db191835 | |||
| b7ac6a2e33 | |||
| c0178c3e80 | |||
| e58fb29722 | |||
| a1300ec095 | |||
| 73e0216f78 | |||
| d16dfdaee3 | |||
| 02a605f368 | |||
| 71d5756223 | |||
| 2866743ce6 | |||
| e91a5e3793 | |||
| 7f5ad041cc | |||
| 92ea275275 | |||
| 0c114a2ab3 | |||
| e3757880ee | |||
| 88d680ef77 | |||
| e1b3cf027c | |||
| d0a725d1cc | |||
| 07dbd26ba4 | |||
| c93f56e4ec | |||
| c0866b9787 | |||
| 14a9f6c444 | |||
| 588870b479 | |||
| f74bb3c145 | |||
| 7095753410 | |||
| 4f851dc431 | |||
| e89cc336b1 | |||
| 959c588658 | |||
| 7c887c1a5d | |||
| 56bcf9796a | |||
| ee270314f8 | |||
| bda76afe4b | |||
| 4d426a3f31 | |||
| 9d33248c6e | |||
| 46329ceb94 | |||
| 2160f0bc08 | |||
| b231f19ec6 | |||
| 6eb896e7a3 | |||
| d7874315c3 | |||
| c95b27683f | |||
| b0655d0431 | |||
| ad24596d3f | |||
| 80a6cf34e2 | |||
| c13b1800b9 | |||
| b4bb0f011d | |||
| b9ace61ccb | |||
| 21273582a4 | |||
| 53bbabea4f | |||
| 170a78a420 | |||
| 8771ced8e4 | |||
| dc7d2698b7 | |||
| 3d4694a92f | |||
| 8b2f94a6b2 | |||
| 6736164d98 | |||
| 77266fe221 | |||
| 14a48c1182 | |||
| 5f6e52f367 | |||
| e71a87c62c | |||
| b963f177cc | |||
| c3097979f2 | |||
| 21e56d2f53 | |||
| 455ce26741 | |||
| d241f5b3eb | |||
| d34f8eda1a | |||
| 483095c3da | |||
| 856c34016d | |||
| ad80d4f059 | |||
| 0da547a239 | |||
| 16278892d8 | |||
| 8500f404a9 | |||
| 5d782a317c | |||
| af435204a0 | |||
| b4c353e65f | |||
| e42f6c0cad | |||
| bc512a6e4c | |||
| 9cf7edc48d | |||
| 904539df58 | |||
| c9df9c33a8 | |||
| 5c3bfa6a83 | |||
| 149ed04a4f | |||
| e98eaaee6e | |||
| 4b93d801ae | |||
| 5a1cc4c2e7 | |||
| 8016a70bc4 | |||
| 70536d5676 | |||
| 27ce0970c5 | |||
| 49f6634d73 | |||
| 142ee81e66 | |||
| 3b21998d96 | |||
| 0fb307d09b | |||
| c1160d3419 | |||
| 48253f0ff0 | |||
| c3c7ee5453 | |||
| c6aac8cbd9 | |||
| 1b43bc78d0 | |||
| 93a091c7e8 | |||
| 083dde3557 | |||
| 4adc5f2c85 | |||
| ced14819e4 | |||
| 0b42d85c5b | |||
| c4a35020f1 | |||
| 11f052bcc6 | |||
| 0ea11ea806 | |||
| 7cad5a0479 | |||
| 83c53f6a79 | |||
| ae13ed7ded | |||
| b17385120a | |||
| cc0d8da416 | |||
| c796702eba | |||
| 2675442ced | |||
| aa3e6514c6 | |||
| be6d64fbfd | |||
| 4cbab72369 | |||
| 0227b1c68d | |||
| 4c051202af | |||
| 981b9e0595 | |||
| 9e719ba31e | |||
| c65f576f8d | |||
| 2c805bbece | |||
| 02b836698c | |||
| 25112ede58 | |||
| 5888c8a56c | |||
| 1cee7bf397 | |||
| cab7a71a94 | |||
| d7c63e3487 | |||
| bff749fd50 | |||
| 5c286352cb | |||
| 9ec3504c72 | |||
| 26b3e32ca2 | |||
| 4e2c83cc08 | |||
| 17def14eba | |||
| f260de573b | |||
| 4fd45ab278 | |||
| 4a2e9eb927 | |||
| dd8adef9ed | |||
| 9164debf03 | |||
| 534bef8632 | |||
| d8c43d02ba | |||
| ae3738f822 | |||
| be621e1aa7 | |||
| 343d63a28a | |||
| 0a28d6e950 | |||
| b493a62afa | |||
| 37a8c9bd72 | |||
| a9c4345159 | |||
| 5f1153b43f | |||
| 2c213f88d9 | |||
| a236219111 | |||
| 2f9958cca9 | |||
| f26154d0ac | |||
| 5ae87b7c95 | |||
| 219103a4e2 | |||
| 4ec7b9bb3f | |||
| bad8b7fb76 | |||
| a101857cb6 | |||
| a52f92830a | |||
| 40d113a423 | |||
| 7ec8421d19 | |||
| 9048efeb65 | |||
| 43fc200dae | |||
| 6679e93afc |
+5
-1
@@ -6,4 +6,8 @@ coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
/dist
|
||||
|
||||
# version file and tarball created by 'npm pack'
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"nonew": true,
|
||||
"curly": true,
|
||||
"forin": true,
|
||||
"freeze": true,
|
||||
"freeze": false,
|
||||
"undef": true,
|
||||
"unused": "vars"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- node # Latest stable version of nodejs.
|
||||
+585
-7
@@ -1,3 +1,581 @@
|
||||
Changes in [0.7.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.2) (2016-12-15)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1...v0.7.2)
|
||||
|
||||
* Bump to Olm 2.0
|
||||
[\#309](https://github.com/matrix-org/matrix-js-sdk/pull/309)
|
||||
* Sanity check payload length before encrypting
|
||||
[\#307](https://github.com/matrix-org/matrix-js-sdk/pull/307)
|
||||
* Remove dead _sendPingToDevice function
|
||||
[\#308](https://github.com/matrix-org/matrix-js-sdk/pull/308)
|
||||
* Add setRoomDirectoryVisibilityAppService
|
||||
[\#306](https://github.com/matrix-org/matrix-js-sdk/pull/306)
|
||||
* Update release script to do signed releases
|
||||
[\#305](https://github.com/matrix-org/matrix-js-sdk/pull/305)
|
||||
* e2e: Wait for pending device lists
|
||||
[\#304](https://github.com/matrix-org/matrix-js-sdk/pull/304)
|
||||
* Start a new megolm session when devices are blacklisted
|
||||
[\#303](https://github.com/matrix-org/matrix-js-sdk/pull/303)
|
||||
* E2E: Download our own devicelist on startup
|
||||
[\#302](https://github.com/matrix-org/matrix-js-sdk/pull/302)
|
||||
|
||||
Changes in [0.7.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1) (2016-12-09)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1-rc.1...v0.7.1)
|
||||
|
||||
No changes
|
||||
|
||||
|
||||
Changes in [0.7.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1-rc.1) (2016-12-05)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.0...v0.7.1-rc.1)
|
||||
|
||||
* Avoid NPE when no sessionStore is given
|
||||
[\#300](https://github.com/matrix-org/matrix-js-sdk/pull/300)
|
||||
* Improve decryption error messages
|
||||
[\#299](https://github.com/matrix-org/matrix-js-sdk/pull/299)
|
||||
* Revert "Use native Array.isArray when available."
|
||||
[\#283](https://github.com/matrix-org/matrix-js-sdk/pull/283)
|
||||
* Use native Array.isArray when available.
|
||||
[\#282](https://github.com/matrix-org/matrix-js-sdk/pull/282)
|
||||
|
||||
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
|
||||
|
||||
* Avoid a packetstorm of device queries on startup
|
||||
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
|
||||
* E2E: Check devices to share keys with on each send
|
||||
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
|
||||
* Apply unknown-keyshare mitigations
|
||||
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
|
||||
* distinguish unknown users from deviceless users
|
||||
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
|
||||
* Allow starting client with initialSyncLimit = 0
|
||||
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
|
||||
* Make timeline-window _unpaginate public and rename to unpaginate
|
||||
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
|
||||
* Send a STOPPED sync updated after call to stopClient
|
||||
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
|
||||
* Fix bug in verifying megolm event senders
|
||||
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
|
||||
* Handle decryption of events after they arrive
|
||||
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
|
||||
* Fix examples.
|
||||
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
|
||||
* Add a travis.yml
|
||||
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
|
||||
* Encrypt all events, including 'm.call.*'
|
||||
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
|
||||
* Ignore reshares of known megolm sessions
|
||||
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
|
||||
* Log to the console on unknown session
|
||||
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
|
||||
* Make it easier for SDK users to wrap prevailing the 'request' function
|
||||
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
|
||||
|
||||
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
|
||||
|
||||
* Change release script to pass version by environment variable
|
||||
|
||||
|
||||
Changes in [0.6.4-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.2) (2016-11-02)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.1...v0.6.4-rc.2)
|
||||
|
||||
* Add getRoomTags method to client
|
||||
[\#236](https://github.com/matrix-org/matrix-js-sdk/pull/236)
|
||||
|
||||
Changes in [0.6.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.1) (2016-11-02)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.3...v0.6.4-rc.1)
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
* Bundled version of the JS SDK are no longer versioned along with
|
||||
source files in the dist/ directory. As of this release, they
|
||||
will be included in the release tarball, but not the source
|
||||
repository.
|
||||
|
||||
Other Changes
|
||||
-------------
|
||||
* More fixes to the release script
|
||||
[\#272](https://github.com/matrix-org/matrix-js-sdk/pull/272)
|
||||
* Update the release process to use github releases
|
||||
[\#271](https://github.com/matrix-org/matrix-js-sdk/pull/271)
|
||||
* Don't package the world when we release
|
||||
[\#270](https://github.com/matrix-org/matrix-js-sdk/pull/270)
|
||||
* Add ability to set a filter prior to the first /sync
|
||||
[\#269](https://github.com/matrix-org/matrix-js-sdk/pull/269)
|
||||
* Sign one-time keys, and verify their signatures
|
||||
[\#243](https://github.com/matrix-org/matrix-js-sdk/pull/243)
|
||||
* Check for duplicate message indexes for group messages
|
||||
[\#241](https://github.com/matrix-org/matrix-js-sdk/pull/241)
|
||||
* Rotate megolm sessions
|
||||
[\#240](https://github.com/matrix-org/matrix-js-sdk/pull/240)
|
||||
* Check recipient and sender in Olm messages
|
||||
[\#239](https://github.com/matrix-org/matrix-js-sdk/pull/239)
|
||||
* Consistency checks for E2E device downloads
|
||||
[\#237](https://github.com/matrix-org/matrix-js-sdk/pull/237)
|
||||
* Support User-Interactive auth for delete device
|
||||
[\#235](https://github.com/matrix-org/matrix-js-sdk/pull/235)
|
||||
* Utility to help with interactive auth
|
||||
[\#234](https://github.com/matrix-org/matrix-js-sdk/pull/234)
|
||||
* Fix sync breaking when an invalid filterId is in localStorage
|
||||
[\#228](https://github.com/matrix-org/matrix-js-sdk/pull/228)
|
||||
|
||||
Changes in [0.6.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.3) (2016-10-12)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.2...v0.6.3)
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
* Add a 'RECONNECTING' state to the sync states. This is an additional state
|
||||
between 'SYNCING' and 'ERROR', so most clients should not notice.
|
||||
|
||||
Other Changes
|
||||
----------------
|
||||
* Fix params getting replaced on register calls
|
||||
[\#233](https://github.com/matrix-org/matrix-js-sdk/pull/233)
|
||||
* Fix potential 30s delay on reconnect
|
||||
[\#232](https://github.com/matrix-org/matrix-js-sdk/pull/232)
|
||||
* uploadContent: Attempt some consistency between browser and node
|
||||
[\#230](https://github.com/matrix-org/matrix-js-sdk/pull/230)
|
||||
* Fix error handling on uploadContent
|
||||
[\#229](https://github.com/matrix-org/matrix-js-sdk/pull/229)
|
||||
* Fix uploadContent for node.js
|
||||
[\#226](https://github.com/matrix-org/matrix-js-sdk/pull/226)
|
||||
* Don't emit ERROR until a keepalive poke fails
|
||||
[\#223](https://github.com/matrix-org/matrix-js-sdk/pull/223)
|
||||
* Function to get the fallback url for interactive auth
|
||||
[\#224](https://github.com/matrix-org/matrix-js-sdk/pull/224)
|
||||
* Revert "Handle the first /sync failure differently."
|
||||
[\#222](https://github.com/matrix-org/matrix-js-sdk/pull/222)
|
||||
|
||||
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
|
||||
|
||||
* Check dependencies aren't on develop in release.sh
|
||||
[\#221](https://github.com/matrix-org/matrix-js-sdk/pull/221)
|
||||
* Fix checkTurnServers leak on logout
|
||||
[\#220](https://github.com/matrix-org/matrix-js-sdk/pull/220)
|
||||
* Fix leak of file upload objects
|
||||
[\#219](https://github.com/matrix-org/matrix-js-sdk/pull/219)
|
||||
* crypto: remove duplicate code
|
||||
[\#218](https://github.com/matrix-org/matrix-js-sdk/pull/218)
|
||||
* Add API for 3rd party location lookup
|
||||
[\#217](https://github.com/matrix-org/matrix-js-sdk/pull/217)
|
||||
* Handle the first /sync failure differently.
|
||||
[\#216](https://github.com/matrix-org/matrix-js-sdk/pull/216)
|
||||
|
||||
Changes in [0.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.1) (2016-09-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.0...v0.6.1)
|
||||
|
||||
* Fix the ed25519 key checking
|
||||
[\#215](https://github.com/matrix-org/matrix-js-sdk/pull/215)
|
||||
* Add MatrixClient.getEventSenderDeviceInfo()
|
||||
[\#214](https://github.com/matrix-org/matrix-js-sdk/pull/214)
|
||||
|
||||
Changes in [0.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.0) (2016-09-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.6...v0.6.0)
|
||||
|
||||
* Pull user device list on join
|
||||
[\#212](https://github.com/matrix-org/matrix-js-sdk/pull/212)
|
||||
* Fix sending of oh_hais on bad sessions
|
||||
[\#213](https://github.com/matrix-org/matrix-js-sdk/pull/213)
|
||||
* Support /publicRooms pagination
|
||||
[\#211](https://github.com/matrix-org/matrix-js-sdk/pull/211)
|
||||
* Update the olm library version to 1.3.0
|
||||
[\#205](https://github.com/matrix-org/matrix-js-sdk/pull/205)
|
||||
* Comment what the logic in uploadKeys does
|
||||
[\#209](https://github.com/matrix-org/matrix-js-sdk/pull/209)
|
||||
* Include keysProved and keysClaimed in the local echo for events we send.
|
||||
[\#210](https://github.com/matrix-org/matrix-js-sdk/pull/210)
|
||||
* Check if we need to upload new one-time keys every 10 minutes
|
||||
[\#208](https://github.com/matrix-org/matrix-js-sdk/pull/208)
|
||||
* Reset oneTimeKey to null on each loop iteration.
|
||||
[\#207](https://github.com/matrix-org/matrix-js-sdk/pull/207)
|
||||
* Add getKeysProved and getKeysClaimed methods to MatrixEvent.
|
||||
[\#206](https://github.com/matrix-org/matrix-js-sdk/pull/206)
|
||||
* Send a 'm.new_device' when we get a message for an unknown group session
|
||||
[\#204](https://github.com/matrix-org/matrix-js-sdk/pull/204)
|
||||
* Introduce EventTimelineSet, filtered timelines and global notif timeline.
|
||||
[\#196](https://github.com/matrix-org/matrix-js-sdk/pull/196)
|
||||
* Wrap the crypto event handlers in try/catch blocks
|
||||
[\#203](https://github.com/matrix-org/matrix-js-sdk/pull/203)
|
||||
* Show warnings on to-device decryption fail
|
||||
[\#202](https://github.com/matrix-org/matrix-js-sdk/pull/202)
|
||||
* s/Displayname/DisplayName/
|
||||
[\#201](https://github.com/matrix-org/matrix-js-sdk/pull/201)
|
||||
* OH HAI
|
||||
[\#200](https://github.com/matrix-org/matrix-js-sdk/pull/200)
|
||||
* Share the current ratchet with new members
|
||||
[\#199](https://github.com/matrix-org/matrix-js-sdk/pull/199)
|
||||
* Move crypto bits into a subdirectory
|
||||
[\#198](https://github.com/matrix-org/matrix-js-sdk/pull/198)
|
||||
* Refactor event handling in Crypto
|
||||
[\#197](https://github.com/matrix-org/matrix-js-sdk/pull/197)
|
||||
* Don't create Olm sessions proactively
|
||||
[\#195](https://github.com/matrix-org/matrix-js-sdk/pull/195)
|
||||
* Use to-device events for key sharing
|
||||
[\#194](https://github.com/matrix-org/matrix-js-sdk/pull/194)
|
||||
* README: callbacks deprecated
|
||||
[\#193](https://github.com/matrix-org/matrix-js-sdk/pull/193)
|
||||
* Fix sender verification for megolm messages
|
||||
[\#192](https://github.com/matrix-org/matrix-js-sdk/pull/192)
|
||||
* Use `ciphertext` instead of `body` in megolm events
|
||||
[\#191](https://github.com/matrix-org/matrix-js-sdk/pull/191)
|
||||
* Add debug methods to get the state of OlmSessions
|
||||
[\#189](https://github.com/matrix-org/matrix-js-sdk/pull/189)
|
||||
* MatrixClient.getStoredDevicesForUser
|
||||
[\#190](https://github.com/matrix-org/matrix-js-sdk/pull/190)
|
||||
* Olm-related cleanups
|
||||
[\#188](https://github.com/matrix-org/matrix-js-sdk/pull/188)
|
||||
* Update to fixed olmlib
|
||||
[\#187](https://github.com/matrix-org/matrix-js-sdk/pull/187)
|
||||
* always play audio out of the remoteAudioElement if it exists.
|
||||
[\#186](https://github.com/matrix-org/matrix-js-sdk/pull/186)
|
||||
* Fix exceptions where HTMLMediaElement loads and plays race
|
||||
[\#185](https://github.com/matrix-org/matrix-js-sdk/pull/185)
|
||||
* Reset megolm session when people join/leave the room
|
||||
[\#183](https://github.com/matrix-org/matrix-js-sdk/pull/183)
|
||||
* Fix exceptions when dealing with redactions
|
||||
[\#184](https://github.com/matrix-org/matrix-js-sdk/pull/184)
|
||||
|
||||
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
|
||||
|
||||
* Put all of the megolm keys in one room message
|
||||
[\#182](https://github.com/matrix-org/matrix-js-sdk/pull/182)
|
||||
* Reinstate device blocking for simple Olm
|
||||
[\#181](https://github.com/matrix-org/matrix-js-sdk/pull/181)
|
||||
* support for unpacking megolm keys
|
||||
[\#180](https://github.com/matrix-org/matrix-js-sdk/pull/180)
|
||||
* Send out megolm keys when we start a megolm session
|
||||
[\#179](https://github.com/matrix-org/matrix-js-sdk/pull/179)
|
||||
* Change the result structure for ensureOlmSessionsForUsers
|
||||
[\#178](https://github.com/matrix-org/matrix-js-sdk/pull/178)
|
||||
* Factor out a function for doing olm encryption
|
||||
[\#177](https://github.com/matrix-org/matrix-js-sdk/pull/177)
|
||||
* Move DeviceInfo and DeviceVerification to separate module
|
||||
[\#175](https://github.com/matrix-org/matrix-js-sdk/pull/175)
|
||||
* Make encryption asynchronous
|
||||
[\#176](https://github.com/matrix-org/matrix-js-sdk/pull/176)
|
||||
* Added ability to set and get status_msg for presence.
|
||||
[\#167](https://github.com/matrix-org/matrix-js-sdk/pull/167)
|
||||
* Megolm: don't dereference nullable object
|
||||
[\#174](https://github.com/matrix-org/matrix-js-sdk/pull/174)
|
||||
* Implement megolm encryption/decryption
|
||||
[\#173](https://github.com/matrix-org/matrix-js-sdk/pull/173)
|
||||
* Update our push rules when they come down stream
|
||||
[\#170](https://github.com/matrix-org/matrix-js-sdk/pull/170)
|
||||
* Factor Olm encryption/decryption out to new classes
|
||||
[\#172](https://github.com/matrix-org/matrix-js-sdk/pull/172)
|
||||
* Make DeviceInfo more useful, and refactor crypto methods to use it
|
||||
[\#171](https://github.com/matrix-org/matrix-js-sdk/pull/171)
|
||||
* Move login and register methods into base-apis
|
||||
[\#169](https://github.com/matrix-org/matrix-js-sdk/pull/169)
|
||||
* Remove defaultDeviceDisplayName from MatrixClient options
|
||||
[\#168](https://github.com/matrix-org/matrix-js-sdk/pull/168)
|
||||
|
||||
Changes in [0.5.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.5) (2016-08-11)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.4...v0.5.5)
|
||||
|
||||
* Add room.getAliases() and room.getCanonicalAlias
|
||||
* Add API calls `/register/email/requestToken`, `/account/password/email/requestToken` and `/account/3pid/email/requestToken`
|
||||
* Add `User.currentlyActive` and `User.lastPresenceTs` events for changes in fields on the User object
|
||||
* Add `logout` and `deactivateAccount`
|
||||
|
||||
* Make sure we actually stop the sync loop on logout
|
||||
[\#166](https://github.com/matrix-org/matrix-js-sdk/pull/166)
|
||||
* Zero is a valid power level
|
||||
[\#164](https://github.com/matrix-org/matrix-js-sdk/pull/164)
|
||||
* Verify e2e keys on download
|
||||
[\#163](https://github.com/matrix-org/matrix-js-sdk/pull/163)
|
||||
* Factor crypto stuff out of MatrixClient
|
||||
[\#162](https://github.com/matrix-org/matrix-js-sdk/pull/162)
|
||||
* Refactor device key upload
|
||||
[\#161](https://github.com/matrix-org/matrix-js-sdk/pull/161)
|
||||
* Wrappers for devices API
|
||||
[\#158](https://github.com/matrix-org/matrix-js-sdk/pull/158)
|
||||
* Add deactivate account function
|
||||
[\#160](https://github.com/matrix-org/matrix-js-sdk/pull/160)
|
||||
* client.listDeviceKeys: Expose device display name
|
||||
[\#159](https://github.com/matrix-org/matrix-js-sdk/pull/159)
|
||||
* Add `logout`
|
||||
[\#157](https://github.com/matrix-org/matrix-js-sdk/pull/157)
|
||||
* Fix email registration
|
||||
[\#156](https://github.com/matrix-org/matrix-js-sdk/pull/156)
|
||||
* Factor out MatrixClient methods to MatrixBaseApis
|
||||
[\#155](https://github.com/matrix-org/matrix-js-sdk/pull/155)
|
||||
* Fix some broken tests
|
||||
[\#154](https://github.com/matrix-org/matrix-js-sdk/pull/154)
|
||||
* make jenkins fail the build if the tests fail
|
||||
[\#153](https://github.com/matrix-org/matrix-js-sdk/pull/153)
|
||||
* deviceId-related fixes
|
||||
[\#152](https://github.com/matrix-org/matrix-js-sdk/pull/152)
|
||||
* /login, /register: Add device_id and initial_device_display_name
|
||||
[\#151](https://github.com/matrix-org/matrix-js-sdk/pull/151)
|
||||
* Support global account_data
|
||||
[\#150](https://github.com/matrix-org/matrix-js-sdk/pull/150)
|
||||
* Add more events to User
|
||||
[\#149](https://github.com/matrix-org/matrix-js-sdk/pull/149)
|
||||
* Add API calls for other requestToken endpoints
|
||||
[\#148](https://github.com/matrix-org/matrix-js-sdk/pull/148)
|
||||
* Add register-specific request token endpoint
|
||||
[\#147](https://github.com/matrix-org/matrix-js-sdk/pull/147)
|
||||
* Set a valid SPDX license identifier in package.json
|
||||
[\#139](https://github.com/matrix-org/matrix-js-sdk/pull/139)
|
||||
* Configure encryption on m.room.encryption events
|
||||
[\#145](https://github.com/matrix-org/matrix-js-sdk/pull/145)
|
||||
* Implement device blocking
|
||||
[\#146](https://github.com/matrix-org/matrix-js-sdk/pull/146)
|
||||
* Clearer doc for setRoomDirectoryVisibility
|
||||
[\#144](https://github.com/matrix-org/matrix-js-sdk/pull/144)
|
||||
* crypto: use memberlist to derive recipient list
|
||||
[\#143](https://github.com/matrix-org/matrix-js-sdk/pull/143)
|
||||
* Support for marking devices as unverified
|
||||
[\#142](https://github.com/matrix-org/matrix-js-sdk/pull/142)
|
||||
* Add Olm as an optionalDependency
|
||||
[\#141](https://github.com/matrix-org/matrix-js-sdk/pull/141)
|
||||
* Add room.getAliases() and room.getCanonicalAlias()
|
||||
[\#140](https://github.com/matrix-org/matrix-js-sdk/pull/140)
|
||||
* Change how MatrixEvent manages encrypted events
|
||||
[\#138](https://github.com/matrix-org/matrix-js-sdk/pull/138)
|
||||
* Catch exceptions when encrypting events
|
||||
[\#137](https://github.com/matrix-org/matrix-js-sdk/pull/137)
|
||||
* Support for marking devices as verified
|
||||
[\#136](https://github.com/matrix-org/matrix-js-sdk/pull/136)
|
||||
* Various matrix-client refactorings and fixes
|
||||
[\#134](https://github.com/matrix-org/matrix-js-sdk/pull/134)
|
||||
|
||||
Changes in [0.5.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.4) (2016-06-02)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.3...v0.5.4)
|
||||
|
||||
* Correct fix for https://github.com/vector-im/vector-web/issues/1039
|
||||
* Make release.sh work on OSX
|
||||
|
||||
|
||||
Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.3) (2016-06-02)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.2...v0.5.3)
|
||||
|
||||
* Add support for the openid interface
|
||||
[\#133](https://github.com/matrix-org/matrix-js-sdk/pull/133)
|
||||
* Bugfix for HTTP upload content when running on node
|
||||
[\#129](https://github.com/matrix-org/matrix-js-sdk/pull/129)
|
||||
* Ignore missing profile (displayname and avatar_url) fields on
|
||||
presence events, rather than overwriting existing valid profile
|
||||
data from membership events or elsewhere.
|
||||
Fixes https://github.com/vector-im/vector-web/issues/1039
|
||||
|
||||
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.1...v0.5.2)
|
||||
|
||||
* Track the absolute time that presence events are received, so that the
|
||||
relative lastActiveAgo value is meaningful.
|
||||
[\#128](https://github.com/matrix-org/matrix-js-sdk/pull/128)
|
||||
* Refactor the addition of events to rooms
|
||||
[\#127](https://github.com/matrix-org/matrix-js-sdk/pull/127)
|
||||
* Clean up test shutdown
|
||||
[\#126](https://github.com/matrix-org/matrix-js-sdk/pull/126)
|
||||
* Add methods to get (and set) pushers
|
||||
[\#125](https://github.com/matrix-org/matrix-js-sdk/pull/125)
|
||||
* URL previewing support
|
||||
[\#122](https://github.com/matrix-org/matrix-js-sdk/pull/122)
|
||||
* Avoid paginating forever in private rooms
|
||||
[\#124](https://github.com/matrix-org/matrix-js-sdk/pull/124)
|
||||
* Fix a bug where we recreated sync filters
|
||||
[\#123](https://github.com/matrix-org/matrix-js-sdk/pull/123)
|
||||
* Implement HTTP timeouts in realtime
|
||||
[\#121](https://github.com/matrix-org/matrix-js-sdk/pull/121)
|
||||
|
||||
Changes in [0.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.1) (2016-03-30)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.0...v0.5.1)
|
||||
|
||||
* Only count joined members for the member count in notifications.
|
||||
[\#119](https://github.com/matrix-org/matrix-js-sdk/pull/119)
|
||||
* Add maySendEvent to match maySendStateEvent
|
||||
[\#118](https://github.com/matrix-org/matrix-js-sdk/pull/118)
|
||||
|
||||
Changes in [0.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.0) (2016-03-22)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.2...v0.5.0)
|
||||
|
||||
**BREAKING CHANGES**:
|
||||
* `opts.pendingEventOrdering`==`end` is no longer supported in the arguments to
|
||||
`MatrixClient.startClient()`. Instead we provide a `detached` option, which
|
||||
puts pending events into a completely separate list in the Room, accessible
|
||||
via `Room.getPendingEvents()`.
|
||||
[\#111](https://github.com/matrix-org/matrix-js-sdk/pull/111)
|
||||
|
||||
Other improvements:
|
||||
* Log the stack when we get a sync error
|
||||
[\#109](https://github.com/matrix-org/matrix-js-sdk/pull/109)
|
||||
* Refactor transmitted-messages code
|
||||
[\#110](https://github.com/matrix-org/matrix-js-sdk/pull/110)
|
||||
* Add a method to the js sdk to look up 3pids on the ID server.
|
||||
[\#113](https://github.com/matrix-org/matrix-js-sdk/pull/113)
|
||||
* Support for cancelling pending events
|
||||
[\#112](https://github.com/matrix-org/matrix-js-sdk/pull/112)
|
||||
* API to stop peeking
|
||||
[\#114](https://github.com/matrix-org/matrix-js-sdk/pull/114)
|
||||
* update store user metadata based on membership events rather than presence
|
||||
[\#116](https://github.com/matrix-org/matrix-js-sdk/pull/116)
|
||||
* Include a counter in generated transaction IDs
|
||||
[\#115](https://github.com/matrix-org/matrix-js-sdk/pull/115)
|
||||
* get/setRoomVisibility API
|
||||
[\#117](https://github.com/matrix-org/matrix-js-sdk/pull/117)
|
||||
|
||||
Changes in [0.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.4.2) (2016-03-17)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.1...v0.4.2)
|
||||
|
||||
* Try again if a pagination request gives us no new messages
|
||||
[\#98](https://github.com/matrix-org/matrix-js-sdk/pull/98)
|
||||
* Add a delay before we start polling the connectivity check endpoint
|
||||
[\#99](https://github.com/matrix-org/matrix-js-sdk/pull/99)
|
||||
* Clean up a codepath that was only used for crypto messages
|
||||
[\#101](https://github.com/matrix-org/matrix-js-sdk/pull/101)
|
||||
* Add maySendStateEvent method, ported from react-sdk (but fixed).
|
||||
[\#94](https://github.com/matrix-org/matrix-js-sdk/pull/94)
|
||||
* Add Session.logged_out event
|
||||
[\#100](https://github.com/matrix-org/matrix-js-sdk/pull/100)
|
||||
* make presence work when peeking.
|
||||
[\#103](https://github.com/matrix-org/matrix-js-sdk/pull/103)
|
||||
* Add RoomState.mayClientSendStateEvent()
|
||||
[\#104](https://github.com/matrix-org/matrix-js-sdk/pull/104)
|
||||
* Fix displaynames for member join events
|
||||
[\#108](https://github.com/matrix-org/matrix-js-sdk/pull/108)
|
||||
|
||||
Changes in 0.4.1
|
||||
================
|
||||
|
||||
Improvements:
|
||||
* Check that `/sync` filters are correct before reusing them, and recreate
|
||||
them if not (https://github.com/matrix-org/matrix-js-sdk/pull/85).
|
||||
* Fire a `Room.timelineReset` event when a room's timeline is reset by a gappy
|
||||
`/sync` (https://github.com/matrix-org/matrix-js-sdk/pull/87,
|
||||
https://github.com/matrix-org/matrix-js-sdk/pull/93).
|
||||
* Make `TimelineWindow.load()` faster in the simple case of loading the live
|
||||
timeline (https://github.com/matrix-org/matrix-js-sdk/pull/88).
|
||||
* Update room-name calculation code to use the name of the sender of the
|
||||
invite when invited to a room
|
||||
(https://github.com/matrix-org/matrix-js-sdk/pull/89).
|
||||
* Don't reset the timeline when we join a room after peeking into it
|
||||
(https://github.com/matrix-org/matrix-js-sdk/pull/91).
|
||||
* Fire `Room.localEchoUpdated` events as local echoes progress through their
|
||||
transmission process (https://github.com/matrix-org/matrix-js-sdk/pull/95,
|
||||
https://github.com/matrix-org/matrix-js-sdk/pull/97).
|
||||
* Avoid getting stuck in a pagination loop when the server sends us only
|
||||
messages we've already seen
|
||||
(https://github.com/matrix-org/matrix-js-sdk/pull/96).
|
||||
|
||||
New methods:
|
||||
* Add `MatrixClient.setPushRuleActions` to set the actions for a push
|
||||
notification rule (https://github.com/matrix-org/matrix-js-sdk/pull/90)
|
||||
* Add `RoomState.maySendStateEvent` which determines if a given user has
|
||||
permission to send a state event
|
||||
(https://github.com/matrix-org/matrix-js-sdk/pull/94)
|
||||
|
||||
Changes in 0.4.0
|
||||
================
|
||||
|
||||
**BREAKING CHANGES**:
|
||||
* `RoomMember.getAvatarUrl()` and `MatrixClient.mxcUrlToHttp()` now return the
|
||||
empty string when given anything other than an mxc:// URL. This ensures that
|
||||
clients never inadvertantly reference content directly, leaking information
|
||||
to third party servers. The `allowDirectLinks` option is provided if the client
|
||||
wants to allow such links.
|
||||
* Add a 'bindEmail' option to register()
|
||||
|
||||
Improvements:
|
||||
* Support third party invites
|
||||
* More appropriate naming for third party invite rooms
|
||||
* Poll the 'versions' endpoint to re-establish connectivity
|
||||
* Catch exceptions when syncing
|
||||
* Room tag support
|
||||
* Generate implicit read receipts
|
||||
* Support CAS login
|
||||
* Guest access support
|
||||
* Never return non-mxc URLs by default
|
||||
* Ability to cancel file uploads
|
||||
* Use the Matrix C/S API v2 with r0 prefix
|
||||
* Account data support
|
||||
* Support non-contiguous event timelines
|
||||
* Support new unread counts
|
||||
* Local echo for read-receipts
|
||||
|
||||
|
||||
New methods:
|
||||
* Add method to fetch URLs not on the home or identity server
|
||||
* Method to get the last receipt for a user
|
||||
* Method to get all known users
|
||||
* Method to delete an alias
|
||||
|
||||
|
||||
Changes in 0.3.0
|
||||
================
|
||||
|
||||
* `MatrixClient.getAvatarUrlForMember` has been removed and replaced with
|
||||
`RoomMember.getAvatarUrl`. Arguments remain the same except the homeserver
|
||||
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
|
||||
|
||||
```javascript
|
||||
// before
|
||||
var url = client.getAvatarUrlForMember(member, width, height, resize, allowDefault)
|
||||
// after
|
||||
var url = member.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
|
||||
```
|
||||
* `MatrixClient.getAvatarUrlForRoom` has been removed and replaced with
|
||||
`Room.getAvatarUrl`. Arguments remain the same except the homeserver
|
||||
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
|
||||
|
||||
```javascript
|
||||
// before
|
||||
var url = client.getAvatarUrlForRoom(room, width, height, resize, allowDefault)
|
||||
// after
|
||||
var url = room.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
|
||||
```
|
||||
|
||||
* `s/Room.getMembersWithMemership/Room.getMembersWithMem`b`ership/g`
|
||||
|
||||
New methods:
|
||||
* Added support for sending receipts via
|
||||
`MatrixClient.sendReceipt(event, receiptType, callback)` and
|
||||
`MatrixClient.sendReadReceipt(event, callback)`.
|
||||
* Added support for receiving receipts via
|
||||
`Room.getReceiptsForEvent(event)` and `Room.getUsersReadUpTo(event)`. Receipts
|
||||
can be directly added to a `Room` using `Room.addReceipt(event)` though the
|
||||
`MatrixClient` does this for you.
|
||||
* Added support for muting local video and audio via the new methods
|
||||
`MatrixCall.setMicrophoneMuted()`, `MatrixCall.isMicrophoneMuted(muted)`,
|
||||
`MatrixCall.isLocalVideoMuted()` and `Matrix.setLocalVideoMuted(muted)`.
|
||||
* Added **experimental** support for screen-sharing in Chrome via
|
||||
`MatrixCall.placeScreenSharingCall(remoteVideoElement, localVideoElement)`.
|
||||
* Added ability to perform server-side searches using
|
||||
`MatrixClient.searchMessageText(opts)` and `MatrixClient.search(opts)`.
|
||||
|
||||
Improvements:
|
||||
* Improve the performance of initial sync processing from `O(n^2)` to `O(n)`.
|
||||
* `Room.name` will now take into account `m.room.canonical_alias` events.
|
||||
* `MatrixClient.startClient` now takes an Object `opts` rather than a Number in
|
||||
a backwards-compatible way. This `opts` allows syncing configuration options
|
||||
to be specified including `includeArchivedRooms` and `resolveInvitesToProfiles`.
|
||||
* `Room` objects which represent room invitations will now have state populated
|
||||
from `invite_room_state` if it is included in the `m.room.member` event.
|
||||
* `Room.getAvatarUrl` will now take into account `m.room.avatar` events.
|
||||
|
||||
Changes in 0.2.2
|
||||
================
|
||||
|
||||
@@ -22,6 +600,10 @@ New methods:
|
||||
Changes in 0.2.1
|
||||
================
|
||||
|
||||
**BREAKING CHANGES**
|
||||
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
|
||||
`(roomIdOrAlias, opts, callback)`.
|
||||
|
||||
Bug fixes:
|
||||
* The `Content-Type` of file uploads is now explicitly set, without relying
|
||||
on the browser to do it for us.
|
||||
@@ -33,10 +615,6 @@ Improvements:
|
||||
* There is now a try/catch block around the `request` function which will
|
||||
reject/errback appropriately if an exception is thrown synchronously in it.
|
||||
|
||||
Breaking changes:
|
||||
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
|
||||
`(roomIdOrAlias, opts, callback)`.
|
||||
|
||||
New methods:
|
||||
* `MatrixClient.createAlias(alias, roomId)`
|
||||
* `MatrixClient.getRoomIdForAlias(alias)`
|
||||
@@ -57,7 +635,7 @@ Modified methods:
|
||||
Changes in 0.2.0
|
||||
================
|
||||
|
||||
Breaking changes:
|
||||
**BREAKING CHANGES**:
|
||||
* `MatrixClient.setPowerLevel` now expects a `MatrixEvent` and not an `Object`
|
||||
for the `event` parameter.
|
||||
|
||||
@@ -88,7 +666,7 @@ New methods:
|
||||
* `MatrixClient.mxcUrlToHttp(url, w, h, method)`
|
||||
* `MatrixClient.getAvatarUrlForRoom(room, w, h, method)`
|
||||
* `MatrixClient.uploadContent(file, callback)`
|
||||
* `Room.getMembersWithMemership(membership)`
|
||||
* `Room.getMembersWithMembership(membership)`
|
||||
* `MatrixScheduler.getQueueForEvent(event)`
|
||||
* `MatrixScheduler.removeEventFromQueue(event)`
|
||||
* `$DATA_STORE.setSyncToken(token)`
|
||||
@@ -123,7 +701,7 @@ Bug fixes:
|
||||
Changes in 0.1.1
|
||||
================
|
||||
|
||||
Breaking changes:
|
||||
**BREAKING CHANGES**:
|
||||
* `Room.calculateRoomName` is now private. Use `Room.recalculate` instead, and
|
||||
access the calculated name via `Room.name`.
|
||||
* `new MatrixClient(...)` no longer creates a `MatrixInMemoryStore` if
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
matrix-js-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
|
||||
@@ -10,11 +10,12 @@ 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.
|
||||
|
||||
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
|
||||
----------
|
||||
@@ -78,7 +79,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 +89,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 +153,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 +172,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 +204,10 @@ Output:
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
```
|
||||
My Room
|
||||
@@ -211,7 +217,7 @@ Output:
|
||||
(join) Bob
|
||||
(invite) @charlie:localhost
|
||||
```
|
||||
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
@@ -226,7 +232,7 @@ host the API reference from the source files like this:
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
```
|
||||
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
|
||||
Contributing
|
||||
@@ -256,7 +262,7 @@ To run tests (Jasmine)::
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
|
||||
|
||||
To run linting:
|
||||
```
|
||||
$ npm run lint
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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`.
|
||||
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
-1
@@ -1 +0,0 @@
|
||||
Release builds and development builds will reside here.
|
||||
@@ -1 +1 @@
|
||||
../../../dist/browser-matrix-dev.js
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -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() {
|
||||
|
||||
@@ -44,7 +44,15 @@ window.onload = function() {
|
||||
disableButtons(true, true, true);
|
||||
};
|
||||
|
||||
client.on("syncComplete", function () {
|
||||
matrixClient.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
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# pre-commit: script to run checks on a working copy before commit
|
||||
#
|
||||
# To use, symlink it into .git/hooks:
|
||||
# ln -s ../../git-hooks/pre-commit .git/hooks
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# create a temp dir
|
||||
tmpdir=`mktemp -d`
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
# get a copy of the index
|
||||
git checkout-index --prefix="$tmpdir/" -a
|
||||
|
||||
# keep node_modules/.bin on the path
|
||||
rootdir=`git rev-parse --show-toplevel`
|
||||
export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
npm run lint
|
||||
@@ -1,3 +1,6 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("request"));
|
||||
module.exports = matrixcs;
|
||||
|
||||
var utils = require("./lib/utils");
|
||||
utils.runPolyfills();
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash -l
|
||||
|
||||
export NVM_DIR="/home/jenkins/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
nvm use 0.10
|
||||
npm install
|
||||
|
||||
RC=0
|
||||
|
||||
function fail {
|
||||
echo $@ >&2
|
||||
RC=1
|
||||
}
|
||||
|
||||
npm test || fail "npm test finished with return code $?"
|
||||
|
||||
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml ||
|
||||
fail "jshint 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 $?"
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-js-sdk-*.tgz
|
||||
|
||||
npm pack ||
|
||||
fail "npm pack finished with return code $?"
|
||||
|
||||
npm run gendoc || fail "JSDoc failed with code $?"
|
||||
|
||||
exit $RC
|
||||
+1158
File diff suppressed because it is too large
Load Diff
+1916
-1386
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
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 content-repo
|
||||
*/
|
||||
var utils = require("./utils");
|
||||
|
||||
/** Content Repo utility functions */
|
||||
module.exports = {
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} mxc The mxc:// URI.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return the emptry string
|
||||
* for such URLs.
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
getHttpUriForMxc: function(baseUrl, mxc, width, height,
|
||||
resizeMethod, allowDirectLinks) {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return '';
|
||||
}
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
if (allowDirectLinks) {
|
||||
return mxc;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
var serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
var prefix = "/_matrix/media/v1/download/";
|
||||
var params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = width;
|
||||
}
|
||||
if (height) {
|
||||
params.height = height;
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/v1/thumbnail/";
|
||||
}
|
||||
|
||||
var fragmentOffset = serverAndMediaId.indexOf("#"),
|
||||
fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels. Default: 96.
|
||||
* @param {Number} height The desired height of the image in pixels. Default: 96.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
*/
|
||||
getIdenticonUri: function(baseUrl, identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return null;
|
||||
}
|
||||
if (!width) { width = 96; }
|
||||
if (!height) { height = 96; }
|
||||
var params = {
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
|
||||
$ident: identiconString
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,767 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/**
|
||||
* olm.js wrapper
|
||||
*
|
||||
* @module crypto/OlmDevice
|
||||
*/
|
||||
var Olm = require("olm");
|
||||
var utils = require("../utils");
|
||||
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
var MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
|
||||
|
||||
function checkPayloadLength(payloadString) {
|
||||
if (payloadString === undefined) {
|
||||
throw new Error("payloadString undefined");
|
||||
}
|
||||
|
||||
if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
|
||||
// might as well fail early here rather than letting the olm library throw
|
||||
// a cryptic memory allocation error.
|
||||
//
|
||||
// Note that even if we manage to do the encryption, the message send may fail,
|
||||
// because by the time we've wrapped the ciphertext in the event object, it may
|
||||
// exceed 65K. But at least we won't just fail with "abort()" in that case.
|
||||
throw new Error("Message too long (" + payloadString.length + " bytes). " +
|
||||
"The maximum for an encrypted message is " +
|
||||
MAX_PLAINTEXT_LENGTH + " bytes.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @alias module:crypto/OlmDevice
|
||||
*
|
||||
* @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 {
|
||||
_initialise_account(this._sessionStore, this._pickleKey, account);
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
|
||||
this.deviceCurve25519Key = e2eKeys.curve25519;
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
|
||||
// we don't bother stashing outboundgroupsessions in the sessionstore -
|
||||
// instead we keep them here.
|
||||
this._outboundGroupSessionStore = {};
|
||||
|
||||
// Store a set of decrypted message indexes for each group session.
|
||||
// This partially mitigates a replay attack where a MITM resends a group
|
||||
// message into the room.
|
||||
//
|
||||
// TODO: If we ever remove an event from memory we will also need to remove
|
||||
// it from this map. Otherwise if we download the event from the server we
|
||||
// will think that it is a duplicate.
|
||||
//
|
||||
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
||||
// Values are true.
|
||||
this._inboundGroupSessionMessageIndexes = {};
|
||||
}
|
||||
|
||||
function _initialise_account(sessionStore, pickleKey, account) {
|
||||
var e2eAccount = sessionStore.getEndToEndAccount();
|
||||
if (e2eAccount !== null) {
|
||||
account.unpickle(pickleKey, e2eAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
account.create();
|
||||
var pickled = account.pickle(pickleKey);
|
||||
sessionStore.storeEndToEndAccount(pickled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {array} The version of Olm.
|
||||
*/
|
||||
OlmDevice.getOlmVersion = function() {
|
||||
return Olm.get_library_version();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 {{payload: string, session_id: string}} decrypted payload, and
|
||||
* session id of new session
|
||||
*
|
||||
* @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 {
|
||||
payload: payloadString,
|
||||
session_id: session.session_id(),
|
||||
};
|
||||
} 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the right olm session id for encrypting messages to the given identity key
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @return {string?} session id, or null if no established session
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
|
||||
var sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
if (sessionIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// Use the session with the lowest ID.
|
||||
sessionIds.sort();
|
||||
return sessionIds[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information on the active Olm sessions for a device.
|
||||
* <p>
|
||||
* Returns an array, with an entry for each active session. The first entry in
|
||||
* the result will be the one used for outgoing messages. Each entry contains
|
||||
* the keys 'hasReceivedMessage' (true if the session has received an incoming
|
||||
* message and is therefore past the pre-key stage), and 'sessionId'.
|
||||
*
|
||||
* @param {string} deviceIdentityKey Curve25519 identity key for the device
|
||||
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
||||
*/
|
||||
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
|
||||
var sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
|
||||
sessionIds.sort();
|
||||
|
||||
var info = [];
|
||||
|
||||
function getSessionInfo(session) {
|
||||
return {
|
||||
hasReceivedMessage: session.has_received_message()
|
||||
};
|
||||
}
|
||||
|
||||
for (var i = 0; i < sessionIds.length; i++) {
|
||||
var sessionId = sessionIds[i];
|
||||
var res = this._getSession(deviceIdentityKey, sessionId, getSessionInfo);
|
||||
res.sessionId = sessionId;
|
||||
info.push(res);
|
||||
}
|
||||
return info;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
checkPayloadLength(payloadString);
|
||||
|
||||
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 {string} decrypted payload.
|
||||
*/
|
||||
OlmDevice.prototype.decryptMessage = function(
|
||||
theirDeviceIdentityKey, sessionId, message_type, ciphertext
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var payloadString = session.decrypt(message_type, ciphertext);
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
|
||||
return payloadString;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if an incoming messages is a prekey message matching 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 {boolean} true if the received message is a prekey message which matches
|
||||
* the given session.
|
||||
*/
|
||||
OlmDevice.prototype.matchesSession = function(
|
||||
theirDeviceIdentityKey, sessionId, message_type, ciphertext
|
||||
) {
|
||||
if (message_type !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
return session.matches_inbound(ciphertext);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Outbound group session
|
||||
// ======================
|
||||
|
||||
/**
|
||||
* store an OutboundGroupSession in _outboundGroupSessionStore
|
||||
*
|
||||
* @param {Olm.OutboundGroupSession} session
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveOutboundGroupSession = function(session) {
|
||||
var pickledSession = session.pickle(this._pickleKey);
|
||||
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
|
||||
* given function
|
||||
*
|
||||
* @param {string} sessionId
|
||||
* @param {function} func
|
||||
* @return {object} result of func
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
|
||||
var pickled = this._outboundGroupSessionStore[sessionId];
|
||||
if (pickled === null) {
|
||||
throw new Error("Unknown outbound group session " + sessionId);
|
||||
}
|
||||
|
||||
var session = new Olm.OutboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, pickled);
|
||||
return func(session);
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new outbound group session
|
||||
*
|
||||
* @return {string} sessionId for the outbound session.
|
||||
*/
|
||||
OlmDevice.prototype.createOutboundGroupSession = function() {
|
||||
var session = new Olm.OutboundGroupSession();
|
||||
try {
|
||||
session.create();
|
||||
this._saveOutboundGroupSession(session);
|
||||
return session.session_id();
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message with an outbound group session
|
||||
*
|
||||
* @param {string} sessionId the id of the outboundgroupsession
|
||||
* @param {string} payloadString payload to be encrypted and sent
|
||||
*
|
||||
* @return {string} ciphertext
|
||||
*/
|
||||
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
|
||||
var self = this;
|
||||
|
||||
checkPayloadLength(payloadString);
|
||||
|
||||
return this._getOutboundGroupSession(sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
self._saveOutboundGroupSession(session);
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the session keys for an outbound group session
|
||||
*
|
||||
* @param {string} sessionId the id of the outbound group session
|
||||
*
|
||||
* @return {{chain_index: number, key: string}} current chain index, and
|
||||
* base64-encoded secret key.
|
||||
*/
|
||||
OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
|
||||
return this._getOutboundGroupSession(sessionId, function(session) {
|
||||
return {
|
||||
chain_index: session.message_index(),
|
||||
key: session.session_key(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Inbound group session
|
||||
// =====================
|
||||
|
||||
/**
|
||||
* store an InboundGroupSession in the session store
|
||||
*
|
||||
* @param {string} roomId
|
||||
* @param {string} senderCurve25519Key
|
||||
* @param {string} sessionId
|
||||
* @param {Olm.InboundGroupSession} session
|
||||
* @param {object} keysClaimed Other keys the sender claims.
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveInboundGroupSession = function(
|
||||
roomId, senderCurve25519Key, sessionId, session, keysClaimed
|
||||
) {
|
||||
var r = {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this._pickleKey),
|
||||
keysClaimed: keysClaimed,
|
||||
};
|
||||
|
||||
this._sessionStore.storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, JSON.stringify(r)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* extract an InboundGroupSession from the session store and call the given function
|
||||
*
|
||||
* @param {string} roomId
|
||||
* @param {string} senderKey
|
||||
* @param {string} sessionId
|
||||
* @param {function(Olm.InboundGroupSession, Object<string, string>): T} func
|
||||
* function to call. Second argument is the map of keys claimed by the session.
|
||||
*
|
||||
* @return {null} the sessionId is unknown
|
||||
*
|
||||
* @return {T} result of func
|
||||
*
|
||||
* @private
|
||||
* @template {T}
|
||||
*/
|
||||
OlmDevice.prototype._getInboundGroupSession = function(
|
||||
roomId, senderKey, sessionId, func
|
||||
) {
|
||||
var r = this._sessionStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId
|
||||
);
|
||||
|
||||
if (r === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
r = JSON.parse(r);
|
||||
|
||||
// check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (roomId !== r.room_id) {
|
||||
throw new Error(
|
||||
"Mismatched room_id for inbound group session (expected " + r.room_id +
|
||||
", was " + roomId + ")"
|
||||
);
|
||||
}
|
||||
|
||||
var session = new Olm.InboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, r.session);
|
||||
return func(session, r.keysClaimed || {});
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an inbound group session to the session store
|
||||
*
|
||||
* @param {string} roomId room in which this session will be used
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {string} sessionKey base64-encoded secret key
|
||||
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
|
||||
*/
|
||||
OlmDevice.prototype.addInboundGroupSession = function(
|
||||
roomId, senderKey, sessionId, sessionKey, keysClaimed
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
/* if we already have this session, consider updating it */
|
||||
function updateSession(session) {
|
||||
console.log("Update for megolm session " + senderKey + "/" + sessionId);
|
||||
// for now we just ignore updates. TODO: implement something here
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var r = this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, updateSession
|
||||
);
|
||||
|
||||
if (r !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// new session.
|
||||
var session = new Olm.InboundGroupSession();
|
||||
try {
|
||||
session.create(sessionKey);
|
||||
if (sessionId != session.session_id()) {
|
||||
throw new Error(
|
||||
"Mismatched group session ID from senderKey: " + senderKey
|
||||
);
|
||||
}
|
||||
self._saveInboundGroupSession(
|
||||
roomId, senderKey, sessionId, session, keysClaimed
|
||||
);
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrypt a received message with an inbound group session
|
||||
*
|
||||
* @param {string} roomId room in which the message was received
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {string} body base64-encoded body of the encrypted message
|
||||
*
|
||||
* @return {null} the sessionId is unknown
|
||||
*
|
||||
* @return {{result: string, keysProved: Object<string, string>, keysClaimed:
|
||||
* Object<string, string>}} result
|
||||
*/
|
||||
OlmDevice.prototype.decryptGroupMessage = function(
|
||||
roomId, senderKey, sessionId, body
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
function decrypt(session, keysClaimed) {
|
||||
var res = session.decrypt(body);
|
||||
|
||||
var plaintext = res.plaintext;
|
||||
if (plaintext === undefined) {
|
||||
// Compatibility for older olm versions.
|
||||
plaintext = res;
|
||||
} else {
|
||||
// Check if we have seen this message index before to detect replay attacks.
|
||||
var messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
|
||||
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
|
||||
throw new Error(
|
||||
"Duplicate message index, possible replay attack: " +
|
||||
messageIndexKey
|
||||
);
|
||||
}
|
||||
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
|
||||
}
|
||||
|
||||
// the sender must have had the senderKey to persuade us to save the
|
||||
// session.
|
||||
var keysProved = {curve25519: senderKey};
|
||||
|
||||
self._saveInboundGroupSession(
|
||||
roomId, senderKey, sessionId, session, keysClaimed
|
||||
);
|
||||
return {
|
||||
result: plaintext,
|
||||
keysClaimed: keysClaimed,
|
||||
keysProved: keysProved,
|
||||
};
|
||||
}
|
||||
|
||||
return this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, decrypt
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Utilities
|
||||
// =========
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
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. Defines the base classes of the encryption implementations
|
||||
*
|
||||
* @module crypto/algorithms/base
|
||||
*/
|
||||
var utils = require("../../utils");
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. A map from string to {@link
|
||||
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
|
||||
*/
|
||||
module.exports.ENCRYPTION_CLASSES = {};
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. Map from string to {@link
|
||||
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
||||
*/
|
||||
module.exports.DECRYPTION_CLASSES = {};
|
||||
|
||||
/**
|
||||
* base type for encryption implementations
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
* @param {string} params.userId The UserID for the local user
|
||||
* @param {string} params.deviceId The identifier for this device.
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
* @param {object} params.config The body of the m.room.encryption event
|
||||
*/
|
||||
var EncryptionAlgorithm = function(params) {
|
||||
this._userId = params.userId;
|
||||
this._deviceId = params.deviceId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._baseApis = params.baseApis;
|
||||
this._roomId = params.roomId;
|
||||
};
|
||||
/** */
|
||||
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
|
||||
|
||||
/**
|
||||
* Encrypt a message event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.EncryptionAlgorithm#encryptMessage
|
||||
* @abstract
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called when the membership of a member of the room changes.
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
EncryptionAlgorithm.prototype.onRoomMembership = function(
|
||||
event, member, oldMembership
|
||||
) {};
|
||||
|
||||
/**
|
||||
* base type for decryption implementations
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
* @param {string} params.userId The UserID for the local user
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||
* from. Null for to-device events.
|
||||
*/
|
||||
var DecryptionAlgorithm = function(params) {
|
||||
this._userId = params.userId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._roomId = params.roomId;
|
||||
};
|
||||
/** */
|
||||
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
|
||||
|
||||
/**
|
||||
* Decrypt an event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
|
||||
* @abstract
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {null} if the event referred to an unknown megolm session
|
||||
* @return {module:crypto.DecryptionResult} decryption result
|
||||
*
|
||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle a key event
|
||||
*
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
|
||||
// ignore by default
|
||||
};
|
||||
|
||||
/**
|
||||
* Exception thrown when decryption fails
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} msg message describing the problem
|
||||
* @extends Error
|
||||
*/
|
||||
module.exports.DecryptionError = function(msg) {
|
||||
this.message = msg;
|
||||
};
|
||||
utils.inherits(module.exports.DecryptionError, Error);
|
||||
|
||||
/**
|
||||
* Registers an encryption/decryption class for a particular algorithm
|
||||
*
|
||||
* @param {string} algorithm algorithm tag to register for
|
||||
*
|
||||
* @param {class} encryptor {@link
|
||||
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
|
||||
* implementation
|
||||
*
|
||||
* @param {class} decryptor {@link
|
||||
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
|
||||
* implementation
|
||||
*/
|
||||
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
|
||||
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
|
||||
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/**
|
||||
* @module crypto/algorithms
|
||||
*/
|
||||
|
||||
var base = require("./base");
|
||||
|
||||
require("./olm");
|
||||
require("./megolm");
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.ENCRYPTION_CLASSES
|
||||
*/
|
||||
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.DECRYPTION_CLASSES
|
||||
*/
|
||||
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.DecryptionError
|
||||
*/
|
||||
module.exports.DecryptionError = base.DecryptionError;
|
||||
@@ -0,0 +1,586 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto/algorithms/megolm
|
||||
*/
|
||||
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("../../utils");
|
||||
var olmlib = require("../olmlib");
|
||||
var base = require("./base");
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} sessionId
|
||||
*
|
||||
* @property {string} sessionId
|
||||
* @property {Number} useCount number of times this session has been used
|
||||
* @property {Number} creationTime when the session was created (ms since the epoch)
|
||||
*
|
||||
* @property {object} sharedWithDevices
|
||||
* devices with which we have shared the session key
|
||||
* userId -> {deviceId -> msgindex}
|
||||
*/
|
||||
function OutboundSessionInfo(sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharedWithDevices = {};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if it's time to rotate the session
|
||||
*
|
||||
* @param {Number} rotationPeriodMsgs
|
||||
* @param {Number} rotationPeriodMs
|
||||
* @return {Boolean}
|
||||
*/
|
||||
OutboundSessionInfo.prototype.needsRotation = function(
|
||||
rotationPeriodMsgs, rotationPeriodMs
|
||||
) {
|
||||
var sessionLifetime = new Date().getTime() - this.creationTime;
|
||||
|
||||
if (this.useCount >= rotationPeriodMsgs ||
|
||||
sessionLifetime >= rotationPeriodMs
|
||||
) {
|
||||
console.log(
|
||||
"Rotating megolm session after " + this.useCount +
|
||||
" messages, " + sessionLifetime + "ms"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine if this session has been shared with devices which it shouldn't
|
||||
* have been.
|
||||
*
|
||||
* @param {Object} devicesInRoom userId -> {deviceId -> object}
|
||||
* devices we should shared the session with.
|
||||
*
|
||||
* @return {Boolean} true if we have shared the session with devices which aren't
|
||||
* in devicesInRoom.
|
||||
*/
|
||||
OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
devicesInRoom
|
||||
) {
|
||||
|
||||
for (var userId in this.sharedWithDevices) {
|
||||
if (!this.sharedWithDevices.hasOwnProperty(userId)) { continue; }
|
||||
|
||||
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||
console.log("Starting new session because we shared with " + userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var deviceId in this.sharedWithDevices[userId]) {
|
||||
if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
|
||||
console.log(
|
||||
"Starting new session because we shared with " +
|
||||
userId + ":" + deviceId
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*/
|
||||
function MegolmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
|
||||
// the most recent attempt to set up a session. This is used to serialise
|
||||
// the session setups, so that we have a race-free view of which session we
|
||||
// are using, and which devices we have shared the keys with. It resolves
|
||||
// with an OutboundSessionInfo (or undefined, for the first message in the
|
||||
// room).
|
||||
this._setupPromise = q();
|
||||
|
||||
// default rotation periods
|
||||
this._sessionRotationPeriodMsgs = 100;
|
||||
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
||||
|
||||
if (params.config.rotation_period_ms !== undefined) {
|
||||
this._sessionRotationPeriodMs = params.config.rotation_period_ms;
|
||||
}
|
||||
|
||||
if (params.config.rotation_period_msgs !== undefined) {
|
||||
this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs;
|
||||
}
|
||||
}
|
||||
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
var self = this;
|
||||
|
||||
var session;
|
||||
|
||||
// takes the previous OutboundSessionInfo, and considers whether to create
|
||||
// a new one. Also shares the key with any (new) devices in the room.
|
||||
// Updates `session` to hold the final OutboundSessionInfo.
|
||||
//
|
||||
// returns a promise which resolves once the keyshare is successful.
|
||||
function prepareSession(oldSession) {
|
||||
session = oldSession;
|
||||
|
||||
// need to make a brand new session?
|
||||
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
|
||||
self._sessionRotationPeriodMs)
|
||||
) {
|
||||
console.log("Starting new megolm session because we need to rotate.");
|
||||
session = null;
|
||||
}
|
||||
|
||||
// determine if we have shared with anyone we shouldn't have
|
||||
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
|
||||
session = null;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
session = self._prepareNewSession();
|
||||
}
|
||||
|
||||
// now check if we need to share with any devices
|
||||
var shareMap = {};
|
||||
|
||||
for (var userId in devicesInRoom) {
|
||||
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var userDevices = devicesInRoom[userId];
|
||||
|
||||
for (var deviceId in userDevices) {
|
||||
if (!userDevices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var deviceInfo = userDevices[deviceId];
|
||||
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == self._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother sending to ourself
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!session.sharedWithDevices[userId] ||
|
||||
session.sharedWithDevices[userId][deviceId] === undefined
|
||||
) {
|
||||
shareMap[userId] = shareMap[userId] || [];
|
||||
shareMap[userId].push(deviceInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self._shareKeyWithDevices(
|
||||
session, shareMap
|
||||
);
|
||||
}
|
||||
|
||||
// helper which returns the session prepared by prepareSession
|
||||
function returnSession() { return session; }
|
||||
|
||||
// first wait for the previous share to complete
|
||||
var prom = this._setupPromise.then(prepareSession);
|
||||
|
||||
// _setupPromise resolves to `session` whether or not the share succeeds
|
||||
this._setupPromise = prom.then(returnSession, returnSession);
|
||||
|
||||
// but we return a promise which only resolves if the share was successful.
|
||||
return prom.then(returnSession);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = function() {
|
||||
var session_id = this._olmDevice.createOutboundGroupSession();
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
||||
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
|
||||
);
|
||||
|
||||
return new OutboundSessionInfo(session_id);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves once the key sharing
|
||||
* message has been sent.
|
||||
*/
|
||||
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
|
||||
var self = this;
|
||||
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
|
||||
var payload = {
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: session.sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
}
|
||||
};
|
||||
|
||||
var contentMap = {};
|
||||
|
||||
return olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser
|
||||
).then(function(devicemap) {
|
||||
var haveTargets = false;
|
||||
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var devicesToShareWith = devicesByUser[userId];
|
||||
var sessionResults = devicemap[userId];
|
||||
|
||||
for (var i = 0; i < devicesToShareWith.length; i++) {
|
||||
var deviceInfo = devicesToShareWith[i];
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
|
||||
var sessionResult = sessionResults[deviceId];
|
||||
if (!sessionResult.sessionId) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
//
|
||||
// we could send them a to_device message anyway, as a
|
||||
// signal that they have missed out on the key sharing
|
||||
// message because of the lack of keys, but there's not
|
||||
// much point in that really; it will mostly serve to clog
|
||||
// up to_device inboxes.
|
||||
//
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
// so just skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"sharing keys with device " + userId + ":" + deviceId
|
||||
);
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
self._userId,
|
||||
self._deviceId,
|
||||
self._olmDevice,
|
||||
userId,
|
||||
deviceInfo,
|
||||
payload
|
||||
);
|
||||
|
||||
if (!contentMap[userId]) {
|
||||
contentMap[userId] = {};
|
||||
}
|
||||
|
||||
contentMap[userId][deviceId] = encryptedContent;
|
||||
haveTargets = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!haveTargets) {
|
||||
return q();
|
||||
}
|
||||
|
||||
// TODO: retries
|
||||
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
}).then(function() {
|
||||
// Add the devices we have shared with to session.sharedWithDevices.
|
||||
//
|
||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
if (!session.sharedWithDevices[userId]) {
|
||||
session.sharedWithDevices[userId] = {};
|
||||
}
|
||||
var devicesToShareWith = devicesByUser[userId];
|
||||
for (var i = 0; i < devicesToShareWith.length; i++) {
|
||||
var deviceInfo = devicesToShareWith[i];
|
||||
session.sharedWithDevices[userId][deviceInfo.deviceId] =
|
||||
key.chain_index;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
var self = this;
|
||||
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||
return self._ensureOutboundSession(devicesInRoom);
|
||||
}).then(function(session) {
|
||||
var payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
content: content
|
||||
};
|
||||
|
||||
var ciphertext = self._olmDevice.encryptGroupMessage(
|
||||
session.sessionId, JSON.stringify(payloadJson)
|
||||
);
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext,
|
||||
session_id: session.sessionId,
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
device_id: self._deviceId,
|
||||
};
|
||||
|
||||
session.useCount++;
|
||||
return encryptedContent;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of unblocked devices for all users in the room
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to a map
|
||||
* from userId to deviceId to deviceInfo
|
||||
*/
|
||||
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
|
||||
// XXX what about rooms where invitees can see the content?
|
||||
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
// We are happy to use a cached version here: we assume that if we already
|
||||
// have a list of the user's devices, then we already share an e2e room
|
||||
// with them, which means that they will have announced any new devices via
|
||||
// an m.new_device.
|
||||
return this._crypto.downloadKeys(roomMembers, false).then(function(devices) {
|
||||
// remove any blocked devices
|
||||
for (var userId in devices) {
|
||||
if (!devices.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var userDevices = devices[userId];
|
||||
for (var deviceId in userDevices) {
|
||||
if (!userDevices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
if (userDevices[deviceId].isBlocked()) {
|
||||
delete userDevices[deviceId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Megolm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
*/
|
||||
function MegolmDecryption(params) {
|
||||
base.DecryptionAlgorithm.call(this, params);
|
||||
|
||||
// events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
// senderKey|sessionId to list of MatrixEvents
|
||||
this._pendingEvents = {};
|
||||
}
|
||||
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* @return {null} The event referred to an unknown megolm session
|
||||
* @return {module:crypto.DecryptionResult} decryption result
|
||||
*
|
||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||
var content = event.getWireContent();
|
||||
|
||||
if (!content.sender_key || !content.session_id ||
|
||||
!content.ciphertext
|
||||
) {
|
||||
throw new base.DecryptionError("Missing fields in input");
|
||||
}
|
||||
|
||||
var res;
|
||||
try {
|
||||
res = this._olmDevice.decryptGroupMessage(
|
||||
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
this._addEventToPendingList(event);
|
||||
}
|
||||
throw new base.DecryptionError(e);
|
||||
}
|
||||
|
||||
if (res === null) {
|
||||
// We've got a message for a session we don't have.
|
||||
this._addEventToPendingList(event);
|
||||
throw new base.DecryptionError(
|
||||
"The sender's device has not sent us the keys for this message."
|
||||
);
|
||||
}
|
||||
|
||||
var payload = JSON.parse(res.result);
|
||||
|
||||
// belt-and-braces check that the room id matches that indicated by the HS
|
||||
// (this is somewhat redundant, since the megolm session is scoped to the
|
||||
// room, so neither the sender nor a MITM can lie about the room_id).
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
throw new base.DecryptionError(
|
||||
"Message intended for room " + payload.room_id
|
||||
);
|
||||
}
|
||||
|
||||
event.setClearData(payload, res.keysProved, res.keysClaimed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add an event to the list of those we couldn't decrypt the first time we
|
||||
* saw them.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event
|
||||
*/
|
||||
MegolmDecryption.prototype._addEventToPendingList = function(event) {
|
||||
var content = event.getWireContent();
|
||||
var k = content.sender_key + "|" + content.session_id;
|
||||
if (!this._pendingEvents[k]) {
|
||||
this._pendingEvents[k] = [];
|
||||
}
|
||||
this._pendingEvents[k].push(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
console.log("Adding key from ", event);
|
||||
var content = event.getContent();
|
||||
|
||||
if (!content.room_id ||
|
||||
!content.session_id ||
|
||||
!content.session_key
|
||||
) {
|
||||
console.error("key event is missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, event.getSenderKey(), content.session_id,
|
||||
content.session_key, event.getKeysClaimed()
|
||||
);
|
||||
|
||||
var k = event.getSenderKey() + "|" + content.session_id;
|
||||
var pending = this._pendingEvents[k];
|
||||
if (pending) {
|
||||
// have another go at decrypting events sent with this session.
|
||||
delete this._pendingEvents[k];
|
||||
|
||||
for (var i = 0; i < pending.length; i++) {
|
||||
try {
|
||||
this.decryptEvent(pending[i]);
|
||||
console.log("successful re-decryption of", pending[i]);
|
||||
} catch (e) {
|
||||
console.log("Still can't decrypt", pending[i], e.stack || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
base.registerAlgorithm(
|
||||
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
|
||||
);
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto/algorithms/olm
|
||||
*/
|
||||
var q = require('q');
|
||||
|
||||
var utils = require("../../utils");
|
||||
var olmlib = require("../olmlib");
|
||||
var DeviceInfo = require("../deviceinfo");
|
||||
var DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
|
||||
var base = require("./base");
|
||||
|
||||
/**
|
||||
* Olm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*/
|
||||
function OlmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
this._sessionPrepared = false;
|
||||
this._prepPromise = null;
|
||||
}
|
||||
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @private
|
||||
|
||||
* @param {string[]} roomMembers list of currently-joined users in the room
|
||||
* @return {module:client.Promise} Promise which resolves when setup is complete
|
||||
*/
|
||||
OlmEncryption.prototype._ensureSession = function(roomMembers) {
|
||||
if (this._prepPromise) {
|
||||
// prep already in progress
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
if (this._sessionPrepared) {
|
||||
// prep already done
|
||||
return q();
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
|
||||
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
|
||||
}).then(function() {
|
||||
self._sessionPrepared = true;
|
||||
}).finally(function() {
|
||||
self._prepPromise = null;
|
||||
});
|
||||
return this._prepPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
// 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 self = this;
|
||||
return this._ensureSession(users).then(function() {
|
||||
var payloadFields = {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
content: content,
|
||||
};
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = self._crypto.getStoredDevicesForUser(userId);
|
||||
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var deviceInfo = devices[j];
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == self._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother sending to ourself
|
||||
continue;
|
||||
}
|
||||
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
|
||||
// don't bother setting up sessions with blocked users
|
||||
continue;
|
||||
}
|
||||
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
self._userId, self._deviceId, self._olmDevice,
|
||||
userId, deviceInfo, payloadFields
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedContent;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Olm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
*/
|
||||
function OlmDecryption(params) {
|
||||
base.DecryptionAlgorithm.call(this, params);
|
||||
}
|
||||
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
OlmDecryption.prototype.decryptEvent = function(event) {
|
||||
var content = event.getWireContent();
|
||||
var deviceKey = content.sender_key;
|
||||
var ciphertext = content.ciphertext;
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new base.DecryptionError("Missing ciphertext");
|
||||
}
|
||||
|
||||
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
|
||||
throw new base.DecryptionError("Not included in recipients");
|
||||
}
|
||||
var message = ciphertext[this._olmDevice.deviceCurve25519Key];
|
||||
var payloadString;
|
||||
|
||||
try {
|
||||
payloadString = this._decryptMessage(deviceKey, message);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"Failed to decrypt Olm event (id=" +
|
||||
event.getId() + ") from " + deviceKey +
|
||||
": " + e.message
|
||||
);
|
||||
throw new base.DecryptionError("Bad Encrypted Message");
|
||||
}
|
||||
|
||||
var payload = JSON.parse(payloadString);
|
||||
|
||||
// check that we were the intended recipient, to avoid unknown-key attack
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
if (payload.recipient != this._userId) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": Intended recipient " +
|
||||
payload.recipient + " does not match our id " + this._userId
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message was intented for " + payload.recipient
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.recipient_keys.ed25519 !=
|
||||
this._olmDevice.deviceEd25519Key) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": Intended recipient ed25519 key " +
|
||||
payload.recipient_keys.ed25519 + " did not match ours"
|
||||
);
|
||||
throw new base.DecryptionError("Message not intended for this device");
|
||||
}
|
||||
|
||||
// check that the original sender matches what the homeserver told us, to
|
||||
// avoid people masquerading as others.
|
||||
// (this check is also provided via the sender's embedded ed25519 key,
|
||||
// which is checked elsewhere).
|
||||
if (payload.sender != event.getSender()) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": original sender " + payload.sender +
|
||||
" does not match reported sender " + event.getSender()
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message forwarded from " + payload.sender
|
||||
);
|
||||
}
|
||||
|
||||
// Olm events intended for a room have a room_id.
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": original room " + payload.room_id +
|
||||
" does not match reported room " + event.room_id
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message intended for room " + payload.room_id
|
||||
);
|
||||
}
|
||||
|
||||
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to decrypt an Olm message
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
|
||||
* @param {object} message message object, with 'type' and 'body' fields
|
||||
*
|
||||
* @return {string} payload, if decrypted successfully.
|
||||
*/
|
||||
OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, message) {
|
||||
var sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
|
||||
// try each session in turn.
|
||||
var decryptionErrors = {};
|
||||
for (var i = 0; i < sessionIds.length; i++) {
|
||||
var sessionId = sessionIds[i];
|
||||
try {
|
||||
var payload = this._olmDevice.decryptMessage(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body
|
||||
);
|
||||
console.log(
|
||||
"Decrypted Olm message from " + theirDeviceIdentityKey +
|
||||
" with session " + sessionId
|
||||
);
|
||||
return payload;
|
||||
} catch (e) {
|
||||
var foundSession = this._olmDevice.matchesSession(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body
|
||||
);
|
||||
|
||||
if (foundSession) {
|
||||
// decryption failed, but it was a prekey message matching this
|
||||
// session, so it should have worked.
|
||||
throw new Error(
|
||||
"Error decrypting prekey message with existing session id " +
|
||||
sessionId + ": " + e.message
|
||||
);
|
||||
}
|
||||
|
||||
// otherwise it's probably a message for another session; carry on, but
|
||||
// keep a record of the error
|
||||
decryptionErrors[sessionId] = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type !== 0) {
|
||||
// not a prekey message, so it should have matched an existing session, but it
|
||||
// didn't work.
|
||||
|
||||
if (sessionIds.length === 0) {
|
||||
throw new Error("No existing sessions");
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Error decrypting non-prekey message with existing sessions: " +
|
||||
JSON.stringify(decryptionErrors)
|
||||
);
|
||||
}
|
||||
|
||||
// prekey message which doesn't match any existing sessions: make a new
|
||||
// session.
|
||||
|
||||
var res;
|
||||
try {
|
||||
res = this._olmDevice.createInboundSession(
|
||||
theirDeviceIdentityKey, message.type, message.body
|
||||
);
|
||||
} catch (e) {
|
||||
decryptionErrors["(new)"] = e.message;
|
||||
throw new Error(
|
||||
"Error decrypting prekey message: " +
|
||||
JSON.stringify(decryptionErrors)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"created new inbound Olm session ID " +
|
||||
res.session_id + " with " + theirDeviceIdentityKey
|
||||
);
|
||||
return res.payload;
|
||||
};
|
||||
|
||||
|
||||
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
|
||||
/**
|
||||
* @module crypto/deviceinfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about a user's device
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/deviceinfo
|
||||
*
|
||||
* @property {string} deviceId the ID of this device
|
||||
*
|
||||
* @property {string[]} algorithms list of algorithms supported by this device
|
||||
*
|
||||
* @property {Object.<string,string>} keys a map from
|
||||
* <key type>:<id> -> <base64-encoded key>>
|
||||
*
|
||||
* @property {module:crypto/deviceinfo.DeviceVerification} verified
|
||||
* whether the device has been verified by the user
|
||||
*
|
||||
* @property {Object} unsigned additional data from the homeserver
|
||||
*
|
||||
* @param {string} deviceId id of the device
|
||||
*/
|
||||
function DeviceInfo(deviceId) {
|
||||
// you can't change the deviceId
|
||||
Object.defineProperty(this, 'deviceId', {
|
||||
enumerable: true,
|
||||
value: deviceId,
|
||||
});
|
||||
|
||||
this.algorithms = [];
|
||||
this.keys = {};
|
||||
this.verified = DeviceVerification.UNVERIFIED;
|
||||
this.unsigned = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* rehydrate a DeviceInfo from the session store
|
||||
*
|
||||
* @param {object} obj raw object from session store
|
||||
* @param {string} deviceId id of the device
|
||||
*
|
||||
* @return {module:crypto~DeviceInfo} new DeviceInfo
|
||||
*/
|
||||
DeviceInfo.fromStorage = function(obj, deviceId) {
|
||||
var res = new DeviceInfo(deviceId);
|
||||
for (var prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare a DeviceInfo for JSON serialisation in the session store
|
||||
*
|
||||
* @return {object} deviceinfo with non-serialised members removed
|
||||
*/
|
||||
DeviceInfo.prototype.toStorage = function() {
|
||||
return {
|
||||
algorithms: this.algorithms,
|
||||
keys: this.keys,
|
||||
verified: this.verified,
|
||||
unsigned: this.unsigned,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the fingerprint for this device (ie, the Ed25519 key)
|
||||
*
|
||||
* @return {string} base64-encoded fingerprint of this device
|
||||
*/
|
||||
DeviceInfo.prototype.getFingerprint = function() {
|
||||
return this.keys["ed25519:" + this.deviceId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the identity key for this device (ie, the Curve25519 key)
|
||||
*
|
||||
* @return {string} base64-encoded identity key of this device
|
||||
*/
|
||||
DeviceInfo.prototype.getIdentityKey = function() {
|
||||
return this.keys["curve25519:" + this.deviceId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the configured display name for this device, if any
|
||||
*
|
||||
* @return {string?} displayname
|
||||
*/
|
||||
DeviceInfo.prototype.getDisplayName = function() {
|
||||
return this.unsigned.device_display_name || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this device is blocked
|
||||
*
|
||||
* @return {Boolean} true if blocked
|
||||
*/
|
||||
DeviceInfo.prototype.isBlocked = function() {
|
||||
return this.verified == DeviceVerification.BLOCKED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this device is verified
|
||||
*
|
||||
* @return {Boolean} true if verified
|
||||
*/
|
||||
DeviceInfo.prototype.isVerified = function() {
|
||||
return this.verified == DeviceVerification.VERIFIED;
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum
|
||||
*/
|
||||
DeviceInfo.DeviceVerification = {
|
||||
VERIFIED: 1,
|
||||
UNVERIFIED: 0,
|
||||
BLOCKED: -1,
|
||||
};
|
||||
|
||||
var DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
/** */
|
||||
module.exports = DeviceInfo;
|
||||
+1244
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module olmlib
|
||||
*
|
||||
* Utilities common to olm encryption algorithms
|
||||
*/
|
||||
|
||||
var q = require('q');
|
||||
var anotherjson = require('another-json');
|
||||
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for olm
|
||||
*/
|
||||
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for megolm
|
||||
*/
|
||||
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an event payload for an Olm device
|
||||
*
|
||||
* @param {Object<string, string>} resultsObject The `ciphertext` property
|
||||
* of the m.room.encrypted event to which to add our result
|
||||
*
|
||||
* @param {string} ourUserId
|
||||
* @param {string} ourDeviceId
|
||||
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
|
||||
* @param {string} recipientUserId
|
||||
* @param {module:crypto/deviceinfo} recipientDevice
|
||||
* @param {object} payloadFields fields to include in the encrypted payload
|
||||
*/
|
||||
module.exports.encryptMessageForDevice = function(
|
||||
resultsObject,
|
||||
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
|
||||
payloadFields
|
||||
) {
|
||||
var deviceKey = recipientDevice.getIdentityKey();
|
||||
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
|
||||
if (sessionId === null) {
|
||||
// If we don't have a session for a device then
|
||||
// we can't encrypt a message for it.
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Using sessionid " + sessionId + " for device " +
|
||||
recipientUserId + ":" + recipientDevice.deviceId
|
||||
);
|
||||
|
||||
var payload = {
|
||||
sender: ourUserId,
|
||||
sender_device: ourDeviceId,
|
||||
|
||||
// Include the Ed25519 key so that the recipient knows what
|
||||
// device this message came from.
|
||||
// We don't need to include the curve25519 key since the
|
||||
// recipient will already know this from the olm headers.
|
||||
// When combined with the device keys retrieved from the
|
||||
// homeserver signed by the ed25519 key this proves that
|
||||
// the curve25519 key and the ed25519 key are owned by
|
||||
// the same device.
|
||||
keys: {
|
||||
"ed25519": olmDevice.deviceEd25519Key,
|
||||
},
|
||||
|
||||
// include the recipient device details in the payload,
|
||||
// to avoid unknown key attacks, per
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
recipient: recipientUserId,
|
||||
recipient_keys: {
|
||||
"ed25519": recipientDevice.getFingerprint(),
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: technically, a bunch of that stuff only needs to be included for
|
||||
// pre-key messages: after that, both sides know exactly which devices are
|
||||
// involved in the session. If we're looking to reduce data transfer in the
|
||||
// future, we could elide them for subsequent messages.
|
||||
|
||||
utils.extend(payload, payloadFields);
|
||||
|
||||
resultsObject[deviceKey] = olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, JSON.stringify(payload)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given devices.
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
module.exports.ensureOlmSessionsForDevices = function(
|
||||
olmDevice, baseApis, devicesByUser
|
||||
) {
|
||||
var devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
var result = {};
|
||||
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
|
||||
result[userId] = {};
|
||||
var devices = devicesByUser[userId];
|
||||
for (var j = 0; j < devices.length; j++) {
|
||||
var deviceInfo = devices[j];
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
var sessionId = olmDevice.getSessionIdForDevice(key);
|
||||
if (sessionId === null) {
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
result[userId][deviceId] = {
|
||||
device: deviceInfo,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesWithoutSession.length === 0) {
|
||||
return q(result);
|
||||
}
|
||||
|
||||
// TODO: this has a race condition - if we try to send another message
|
||||
// while we are claiming a key, we will end up claiming two and setting up
|
||||
// two sessions.
|
||||
//
|
||||
// That should eventually resolve itself, but it's poor form.
|
||||
|
||||
var oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
return baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm
|
||||
).then(function(res) {
|
||||
var otk_res = res.one_time_keys || {};
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
|
||||
var userRes = otk_res[userId] || {};
|
||||
var devices = devicesByUser[userId];
|
||||
for (var j = 0; j < devices.length; j++) {
|
||||
var deviceInfo = devices[j];
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
if (result[userId][deviceId].sessionId) {
|
||||
// we already have a result for this device
|
||||
continue;
|
||||
}
|
||||
|
||||
var deviceRes = userRes[deviceId] || {};
|
||||
var oneTimeKey = null;
|
||||
for (var keyId in deviceRes) {
|
||||
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
}
|
||||
}
|
||||
|
||||
if (!oneTimeKey) {
|
||||
console.warn(
|
||||
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sid = _verifyKeyAndStartSession(
|
||||
olmDevice, oneTimeKey, userId, deviceInfo
|
||||
);
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
try {
|
||||
_verifySignature(
|
||||
olmDevice, oneTimeKey, userId, deviceId,
|
||||
deviceInfo.getFingerprint()
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Unable to verify signature on one-time key for device " +
|
||||
userId + ":" + deviceId + ":", e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
var sid;
|
||||
try {
|
||||
sid = olmDevice.createOutboundSession(
|
||||
deviceInfo.getIdentityKey(), oneTimeKey.key
|
||||
);
|
||||
} catch (e) {
|
||||
// possibly a bad key
|
||||
console.error("Error starting session with device " +
|
||||
userId + ":" + deviceId + ": " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("Started new sessionid " + sid +
|
||||
" for device " + userId + ":" + deviceId);
|
||||
return sid;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify the signature on an object
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
|
||||
*
|
||||
* @param {Object} obj object to check signature on. Note that this will be
|
||||
* stripped of its 'signatures' and 'unsigned' properties.
|
||||
*
|
||||
* @param {string} signingUserId ID of the user whose signature should be checked
|
||||
*
|
||||
* @param {string} signingDeviceId ID of the device whose signature should be checked
|
||||
*
|
||||
* @param {string} signingKey base64-ed ed25519 public key
|
||||
*/
|
||||
var _verifySignature = module.exports.verifySignature = function(
|
||||
olmDevice, obj, signingUserId, signingDeviceId, signingKey
|
||||
) {
|
||||
var signKeyId = "ed25519:" + signingDeviceId;
|
||||
var signatures = obj.signatures || {};
|
||||
var userSigs = signatures[signingUserId] || {};
|
||||
var signature = userSigs[signKeyId];
|
||||
if (!signature) {
|
||||
throw Error("No signature");
|
||||
}
|
||||
|
||||
// prepare the canonical json: remove unsigned and signatures, and stringify with
|
||||
// anotherjson
|
||||
delete obj.unsigned;
|
||||
delete obj.signatures;
|
||||
var json = anotherjson.stringify(obj);
|
||||
|
||||
olmDevice.verifySignature(
|
||||
signingKey, json, signature
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
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";
|
||||
/**
|
||||
* @module filter-component
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if a value matches a given field value, which may be a * terminated
|
||||
* wildcard pattern.
|
||||
* @param {String} actual_value The value to be compared
|
||||
* @param {String} filter_value The filter pattern to be compared
|
||||
* @return {bool} true if the actual_value matches the filter_value
|
||||
*/
|
||||
function _matches_wildcard(actual_value, filter_value) {
|
||||
if (filter_value.endsWith("*")) {
|
||||
var type_prefix = filter_value.slice(0, -1);
|
||||
return actual_value.substr(0, type_prefix.length) === type_prefix;
|
||||
}
|
||||
else {
|
||||
return actual_value === filter_value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FilterComponent is a section of a Filter definition which defines the
|
||||
* types, rooms, senders filters etc to be applied to a particular type of resource.
|
||||
* This is all ported over from synapse's Filter object.
|
||||
*
|
||||
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
|
||||
* 'Filters' are referred to as 'FilterCollections'.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||
*/
|
||||
function FilterComponent(filter_json) {
|
||||
this.filter_json = filter_json;
|
||||
|
||||
this.types = filter_json.types || null;
|
||||
this.not_types = filter_json.not_types || [];
|
||||
|
||||
this.rooms = filter_json.rooms || null;
|
||||
this.not_rooms = filter_json.not_rooms || [];
|
||||
|
||||
this.senders = filter_json.senders || null;
|
||||
this.not_senders = filter_json.not_senders || [];
|
||||
|
||||
this.contains_url = filter_json.contains_url || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks with the filter component matches the given event
|
||||
* @param {MatrixEvent} event event to be checked against the filter
|
||||
* @return {bool} true if the event matches the filter
|
||||
*/
|
||||
FilterComponent.prototype.check = function(event) {
|
||||
return this._checkFields(
|
||||
event.getRoomId(),
|
||||
event.getSender(),
|
||||
event.getType(),
|
||||
event.getContent() ? event.getContent().url !== undefined : false
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether the filter component matches the given event fields.
|
||||
* @param {String} room_id the room_id for the event being checked
|
||||
* @param {String} sender the sender of the event being checked
|
||||
* @param {String} event_type the type of the event being checked
|
||||
* @param {String} contains_url whether the event contains a content.url field
|
||||
* @return {bool} true if the event fields match the filter
|
||||
*/
|
||||
FilterComponent.prototype._checkFields =
|
||||
function(room_id, sender, event_type, contains_url)
|
||||
{
|
||||
var literal_keys = {
|
||||
"rooms": function(v) { return room_id === v; },
|
||||
"senders": function(v) { return sender === v; },
|
||||
"types": function(v) { return _matches_wildcard(event_type, v); },
|
||||
};
|
||||
|
||||
var self = this;
|
||||
Object.keys(literal_keys).forEach(function(name) {
|
||||
var match_func = literal_keys[name];
|
||||
var not_name = "not_" + name;
|
||||
var disallowed_values = self[not_name];
|
||||
if (disallowed_values.map(match_func)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var allowed_values = self[name];
|
||||
if (allowed_values) {
|
||||
if (!allowed_values.map(match_func)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var contains_url_filter = this.filter_json.contains_url;
|
||||
if (contains_url_filter !== undefined) {
|
||||
if (contains_url_filter !== contains_url) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters a list of events down to those which match this filter component
|
||||
* @param {MatrixEvent[]} events Events to be checked againt the filter component
|
||||
* @return {MatrixEvent[]} events which matched the filter component
|
||||
*/
|
||||
FilterComponent.prototype.filter = function(events) {
|
||||
return events.filter(this.check, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the limit field for a given filter component, providing a default of
|
||||
* 10 if none is otherwise specified. Cargo-culted from Synapse.
|
||||
* @return {Number} the limit for this filter component.
|
||||
*/
|
||||
FilterComponent.prototype.limit = function() {
|
||||
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
|
||||
};
|
||||
|
||||
/** The FilterComponent class */
|
||||
module.exports = FilterComponent;
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
var FilterComponent = require("./filter-component");
|
||||
|
||||
/**
|
||||
* @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 ID of this filter on your homeserver (if known)
|
||||
* @return {?Number} The filter ID
|
||||
*/
|
||||
Filter.prototype.getFilterId = function() {
|
||||
return this.filterId;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// This is all ported from synapse's FilterCollection()
|
||||
|
||||
// definitions look something like:
|
||||
// {
|
||||
// "room": {
|
||||
// "rooms": ["!abcde:example.com"],
|
||||
// "not_rooms": ["!123456:example.com"],
|
||||
// "state": {
|
||||
// "types": ["m.room.*"],
|
||||
// "not_rooms": ["!726s6s6q:example.com"],
|
||||
// },
|
||||
// "timeline": {
|
||||
// "limit": 10,
|
||||
// "types": ["m.room.message"],
|
||||
// "not_rooms": ["!726s6s6q:example.com"],
|
||||
// "not_senders": ["@spam:example.com"]
|
||||
// "contains_url": true
|
||||
// },
|
||||
// "ephemeral": {
|
||||
// "types": ["m.receipt", "m.typing"],
|
||||
// "not_rooms": ["!726s6s6q:example.com"],
|
||||
// "not_senders": ["@spam:example.com"]
|
||||
// }
|
||||
// },
|
||||
// "presence": {
|
||||
// "types": ["m.presence"],
|
||||
// "not_senders": ["@alice:example.com"]
|
||||
// },
|
||||
// "event_format": "client",
|
||||
// "event_fields": ["type", "content", "sender"]
|
||||
// }
|
||||
|
||||
var room_filter_json = definition.room;
|
||||
|
||||
// consider the top level rooms/not_rooms filter
|
||||
var room_filter_fields = {};
|
||||
if (room_filter_json) {
|
||||
if (room_filter_json.rooms) {
|
||||
room_filter_fields.rooms = room_filter_json.rooms;
|
||||
}
|
||||
if (room_filter_json.rooms) {
|
||||
room_filter_fields.not_rooms = room_filter_json.not_rooms;
|
||||
}
|
||||
|
||||
this._include_leave = room_filter_json.include_leave || false;
|
||||
}
|
||||
|
||||
this._room_filter = new FilterComponent(room_filter_fields);
|
||||
this._room_timeline_filter = new FilterComponent(
|
||||
room_filter_json ? (room_filter_json.timeline || {}) : {}
|
||||
);
|
||||
|
||||
// don't bother porting this from synapse yet:
|
||||
// this._room_state_filter =
|
||||
// new FilterComponent(room_filter_json.state || {});
|
||||
// this._room_ephemeral_filter =
|
||||
// new FilterComponent(room_filter_json.ephemeral || {});
|
||||
// this._room_account_data_filter =
|
||||
// new FilterComponent(room_filter_json.account_data || {});
|
||||
// this._presence_filter =
|
||||
// new FilterComponent(definition.presence || {});
|
||||
// this._account_data_filter =
|
||||
// new FilterComponent(definition.account_data || {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the room.timeline filter component of the filter
|
||||
* @return {FilterComponent} room timeline filter component
|
||||
*/
|
||||
Filter.prototype.getRoomTimelineFilterComponent = function() {
|
||||
return this._room_timeline_filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter the list of events based on whether they are allowed in a timeline
|
||||
* based on this filter
|
||||
* @param {MatrixEvent[]} events the list of events being filtered
|
||||
* @return {MatrixEvent[]} the list of events which match the filter
|
||||
*/
|
||||
Filter.prototype.filterRoomTimeline = function(events) {
|
||||
return this._room_timeline_filter.filter(this._room_filter.filter(events));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
+470
-164
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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.
|
||||
@@ -6,6 +21,11 @@
|
||||
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)
|
||||
@@ -13,122 +33,56 @@ TODO:
|
||||
*/
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for version 1 of the Client-Server HTTP API.
|
||||
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
|
||||
*/
|
||||
module.exports.PREFIX_V1 = "/_matrix/client/api/v1";
|
||||
module.exports.PREFIX_R0 = "/_matrix/client/r0";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for version 2 alpha of the Client-Server
|
||||
* HTTP API.
|
||||
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
|
||||
*/
|
||||
module.exports.PREFIX_V2_ALPHA = "/_matrix/client/v2_alpha";
|
||||
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/api/v1'. See PREFIX_V1 and PREFIX_V2_ALPHA 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.
|
||||
* '/_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 an
|
||||
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
|
||||
*
|
||||
* @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(opts) {
|
||||
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 = {
|
||||
|
||||
// URI functions
|
||||
// =============
|
||||
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
* @param {string} mxc The mxc:// URI.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
getHttpUriForMxc: function(mxc, width, height, resizeMethod) {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return mxc;
|
||||
}
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
return mxc;
|
||||
}
|
||||
var serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
var prefix = "/_matrix/media/v1/download/";
|
||||
var params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = width;
|
||||
}
|
||||
if (height) {
|
||||
params.height = height;
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/v1/thumbnail/";
|
||||
}
|
||||
|
||||
var fragmentOffset = serverAndMediaId.indexOf("#"),
|
||||
fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return this.opts.baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels.
|
||||
* @param {Number} height The desired height of the image in pixels.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
*/
|
||||
getIdenticonUri: function(identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return;
|
||||
}
|
||||
if (!width) { width = 96; }
|
||||
if (!height) { height = 96; }
|
||||
var params = {
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
|
||||
$ident: identiconString
|
||||
});
|
||||
return this.opts.baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
|
||||
@@ -147,22 +101,87 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
|
||||
/**
|
||||
* 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},
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
* a Buffer, String or ReadStream.
|
||||
*
|
||||
* @param {object} opts options object
|
||||
*
|
||||
* @param {string=} opts.name Name to give the file on the server. Defaults
|
||||
* to <tt>file.name</tt>.
|
||||
*
|
||||
* @param {string=} opts.type Content-type for the upload. Defaults to
|
||||
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
|
||||
*
|
||||
* @param {boolean=} opts.rawResponse Return the raw body, rather than
|
||||
* parsing the JSON. Defaults to false (except on node.js, where it
|
||||
* defaults to true for backwards compatibility).
|
||||
*
|
||||
* @param {boolean=} opts.onlyContentUri Just return the content URI,
|
||||
* rather than the whole body. Defaults to false (except on browsers,
|
||||
* where it defaults to true for backwards compatibility). Ignored if
|
||||
* opts.rawResponse is true.
|
||||
*
|
||||
* @param {Function=} opts.callback Deprecated. Optional. The callback to
|
||||
* invoke on success/failure. See the promise return values for more
|
||||
* information.
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
uploadContent: function(file, callback) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
uploadContent: function(file, opts) {
|
||||
if (utils.isFunction(opts)) {
|
||||
// opts used to be callback
|
||||
opts = {
|
||||
callback: opts,
|
||||
};
|
||||
} else if (opts === undefined) {
|
||||
opts = {};
|
||||
}
|
||||
var defer = q.defer();
|
||||
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||||
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
var contentType = opts.type || file.type || 'application/octet-stream';
|
||||
var fileName = opts.name || file.name;
|
||||
|
||||
// we used to recommend setting file.stream to the thing to upload on
|
||||
// nodejs.
|
||||
var body = file.stream ? file.stream : file;
|
||||
|
||||
// backwards-compatibility hacks where we used to do different things
|
||||
// between browser and node.
|
||||
var rawResponse = opts.rawResponse;
|
||||
if (rawResponse === undefined) {
|
||||
if (global.XMLHttpRequest) {
|
||||
rawResponse = false;
|
||||
} else {
|
||||
console.warn(
|
||||
"Returning the raw JSON from uploadContent(). Future " +
|
||||
"versions of the js-sdk will change this default, to " +
|
||||
"return the parsed object. Set opts.rawResponse=false " +
|
||||
"to change this behaviour now."
|
||||
);
|
||||
rawResponse = true;
|
||||
}
|
||||
}
|
||||
|
||||
var onlyContentUri = opts.onlyContentUri;
|
||||
if (!rawResponse && onlyContentUri === undefined) {
|
||||
if (global.XMLHttpRequest) {
|
||||
console.warn(
|
||||
"Returning only the content-uri from uploadContent(). " +
|
||||
"Future versions of the js-sdk will change this " +
|
||||
"default, to return the whole response object. Set " +
|
||||
"opts.onlyContentUri=false to change this behaviour now."
|
||||
);
|
||||
onlyContentUri = true;
|
||||
} else {
|
||||
onlyContentUri = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -170,64 +189,129 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
// use XMLHttpRequest directly.
|
||||
// (browser-request doesn't support progress either, which is also kind
|
||||
// of important here)
|
||||
|
||||
var upload = { loaded: 0, total: 0 };
|
||||
var promise;
|
||||
|
||||
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
|
||||
// we're setting opts.json=false so that it doesn't JSON-encode the
|
||||
// request, which also means it doesn't JSON-decode the response. Either
|
||||
// way, we have to JSON-parse the response ourselves.
|
||||
var bodyParser = null;
|
||||
if (!rawResponse) {
|
||||
bodyParser = function(rawBody) {
|
||||
var body = JSON.parse(rawBody);
|
||||
if (onlyContentUri) {
|
||||
body = body.content_uri;
|
||||
if (body === undefined) {
|
||||
throw Error('Bad response');
|
||||
}
|
||||
}
|
||||
return body;
|
||||
};
|
||||
}
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
var defer = q.defer();
|
||||
var xhr = new global.XMLHttpRequest();
|
||||
var cb = requestCallback(defer, callback, this.opts.onlyData);
|
||||
upload.xhr = xhr;
|
||||
var cb = requestCallback(defer, opts.callback, this.opts.onlyData);
|
||||
|
||||
var timeout_fn = function() {
|
||||
xhr.abort();
|
||||
cb(new Error('Timeout'));
|
||||
};
|
||||
|
||||
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
|
||||
// set an initial timeout of 30s; we'll advance it each time we get
|
||||
// a progress notification
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
clearTimeout(xhr.timeout_timer);
|
||||
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.content_uri === undefined) {
|
||||
cb(new Error('Bad response'));
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
var resp;
|
||||
try {
|
||||
if (!xhr.responseText) {
|
||||
throw new Error('No response body.');
|
||||
}
|
||||
resp = xhr.responseText;
|
||||
if (bodyParser) {
|
||||
resp = bodyParser(resp);
|
||||
}
|
||||
} catch (err) {
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(undefined, xhr, resp.content_uri);
|
||||
cb(undefined, xhr, resp);
|
||||
break;
|
||||
}
|
||||
};
|
||||
xhr.upload.addEventListener("progress", function(ev) {
|
||||
clearTimeout(xhr.timeout_timer);
|
||||
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
defer.notify(ev);
|
||||
});
|
||||
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||||
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
|
||||
url += "&filename=" + encodeURIComponent(file.name);
|
||||
url += "&filename=" + encodeURIComponent(fileName);
|
||||
|
||||
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);
|
||||
xhr.setRequestHeader("Content-Type", contentType);
|
||||
xhr.send(body);
|
||||
promise = defer.promise;
|
||||
|
||||
// dirty hack (as per _request) to allow the upload to be cancelled.
|
||||
promise.abort = xhr.abort.bind(xhr);
|
||||
} else {
|
||||
var queryParams = {
|
||||
filename: file.name,
|
||||
access_token: this.opts.accessToken
|
||||
filename: fileName,
|
||||
};
|
||||
file.stream.pipe(
|
||||
this.opts.request({
|
||||
uri: url,
|
||||
qs: queryParams,
|
||||
method: "POST"
|
||||
}, requestCallback(defer, callback, this.opts.onlyData))
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, "POST", "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/v1",
|
||||
headers: {"Content-Type": contentType},
|
||||
json: false,
|
||||
bodyParser: bodyParser,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return defer.promise;
|
||||
var self = this;
|
||||
|
||||
// remove the upload from the list on completion
|
||||
var promise0 = promise.finally(function() {
|
||||
for (var i = 0; i < self.uploads.length; ++i) {
|
||||
if (self.uploads[i] === upload) {
|
||||
self.uploads.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// copy our dirty abort() method to the new promise
|
||||
promise0.abort = promise.abort;
|
||||
|
||||
upload.promise = promise0;
|
||||
this.uploads.push(upload);
|
||||
|
||||
return promise0;
|
||||
},
|
||||
|
||||
cancelUpload: function(promise) {
|
||||
if (promise.abort) {
|
||||
promise.abort();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getCurrentUploads: function() {
|
||||
return this.uploads;
|
||||
},
|
||||
|
||||
idServerRequest: function(callback, method, path, params, prefix) {
|
||||
@@ -257,7 +341,12 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
opts,
|
||||
requestCallback(defer, callback, this.opts.onlyData)
|
||||
);
|
||||
return defer.promise;
|
||||
// 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);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -267,9 +356,22 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @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=} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
*
|
||||
* @param {Object=} opts additional options
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @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>
|
||||
@@ -277,10 +379,28 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @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) {
|
||||
if (!queryParams) { queryParams = {}; }
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
return this.request(callback, method, path, queryParams, data);
|
||||
authedRequest: function(callback, method, path, queryParams, data, opts) {
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
if (!queryParams.access_token) {
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
}
|
||||
|
||||
var request_promise = this.request(
|
||||
callback, method, path, queryParams, data, opts
|
||||
);
|
||||
|
||||
var self = this;
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -290,9 +410,22 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @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=} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
*
|
||||
* @param {Object=} opts additional options
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @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>
|
||||
@@ -300,9 +433,13 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @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) {
|
||||
return this.requestWithPrefix(
|
||||
callback, method, path, queryParams, data, this.opts.prefix
|
||||
request: function(callback, method, path, queryParams, data, opts) {
|
||||
opts = opts || {};
|
||||
var prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
|
||||
return this.requestOtherUrl(
|
||||
callback, method, fullUri, queryParams, data, opts
|
||||
);
|
||||
},
|
||||
|
||||
@@ -320,21 +457,25 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @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.
|
||||
*
|
||||
* @deprecated prefer authedRequest with opts.prefix
|
||||
*/
|
||||
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
|
||||
prefix) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
return this._request(callback, method, fullUri, queryParams, data);
|
||||
prefix, localTimeoutMs) {
|
||||
return this.authedRequest(
|
||||
callback, method, path, queryParams, data, {
|
||||
localTimeoutMs: localTimeoutMs,
|
||||
prefix: prefix,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -351,6 +492,49 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @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.
|
||||
*
|
||||
* @deprecated prefer request with opts.prefix
|
||||
*/
|
||||
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
|
||||
localTimeoutMs) {
|
||||
return this.request(
|
||||
callback, method, path, queryParams, data, {
|
||||
localTimeoutMs: localTimeoutMs,
|
||||
prefix: prefix,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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). If unspecified, there will be no query params.
|
||||
*
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
*
|
||||
* @param {Object=} opts additional options
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {sting=} opts.prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
|
||||
*
|
||||
* @param {Object=} opts.headers map of additional request headers
|
||||
*
|
||||
* @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>
|
||||
@@ -358,43 +542,144 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @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) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
requestOtherUrl: function(callback, method, uri, queryParams, data,
|
||||
opts) {
|
||||
if (opts === undefined || opts === null) {
|
||||
opts = {};
|
||||
} else if (isFinite(opts)) {
|
||||
// opts used to be localTimeoutMs
|
||||
opts = {
|
||||
localTimeoutMs: opts
|
||||
};
|
||||
}
|
||||
return this._request(callback, method, fullUri, queryParams, data);
|
||||
|
||||
return this._request(
|
||||
callback, method, uri, queryParams, data, opts
|
||||
);
|
||||
},
|
||||
|
||||
_request: function(callback, method, uri, queryParams, data) {
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {function} callback
|
||||
* @param {string} method
|
||||
* @param {string} uri
|
||||
* @param {object} queryParams
|
||||
* @param {object|string} data
|
||||
* @param {object=} opts
|
||||
*
|
||||
* @param {boolean} [opts.json =true] Json-encode data before sending, and
|
||||
* decode response on receipt. (We will still json-decode error
|
||||
* responses, even if this is false.)
|
||||
*
|
||||
* @param {object=} opts.headers extra request headers
|
||||
*
|
||||
* @param {number=} opts.localTimeoutMs client-side timeout for the
|
||||
* request. No timeout if undefined.
|
||||
*
|
||||
* @param {function=} opts.bodyParser function to parse the body of the
|
||||
* response before passing it to the promise and callback.
|
||||
*
|
||||
* @return {module:client.Promise} a promise which resolves to either the
|
||||
* response object (if this.opts.onlyData is truthy), or the parsed
|
||||
* body. Rejects
|
||||
*/
|
||||
_request: function(callback, method, uri, queryParams, data, opts) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
}
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
var self = this;
|
||||
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 json = opts.json === undefined ? true : opts.json;
|
||||
|
||||
var defer = q.defer();
|
||||
|
||||
var timeoutId;
|
||||
var timedOut = false;
|
||||
var req;
|
||||
var localTimeoutMs = opts.localTimeoutMs;
|
||||
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 {
|
||||
this.opts.request(
|
||||
req = this.opts.request(
|
||||
{
|
||||
uri: uri,
|
||||
method: method,
|
||||
withCredentials: false,
|
||||
qs: queryParams,
|
||||
body: data,
|
||||
json: true,
|
||||
json: json,
|
||||
timeout: localTimeoutMs,
|
||||
headers: opts.headers || {},
|
||||
_matrix_opts: this.opts
|
||||
},
|
||||
requestCallback(defer, callback, this.opts.onlyData)
|
||||
function(err, response, body) {
|
||||
if (localTimeoutMs) {
|
||||
callbacks.clearTimeout(timeoutId);
|
||||
if (timedOut) {
|
||||
return; // already rejected promise
|
||||
}
|
||||
}
|
||||
|
||||
// if json is falsy, we won't parse any error response, so need
|
||||
// to do so before turning it into a MatrixError
|
||||
var parseErrorJson = !json;
|
||||
var handlerFn = requestCallback(
|
||||
defer, callback, self.opts.onlyData,
|
||||
parseErrorJson,
|
||||
opts.bodyParser
|
||||
);
|
||||
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);
|
||||
@@ -402,7 +687,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
callback(ex);
|
||||
}
|
||||
}
|
||||
return defer.promise;
|
||||
return reqPromise;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -413,14 +698,34 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
*
|
||||
* If onlyData is true, the defer/callback is invoked with the body of the
|
||||
* response, otherwise the result code.
|
||||
*
|
||||
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
|
||||
*
|
||||
*/
|
||||
var requestCallback = function(defer, userDefinedCallback, onlyData) {
|
||||
var requestCallback = function(
|
||||
defer, userDefinedCallback, onlyData,
|
||||
parseErrorJson, bodyParser
|
||||
) {
|
||||
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) {
|
||||
try {
|
||||
if (response.statusCode >= 400) {
|
||||
if (parseErrorJson) {
|
||||
// we won't have json-decoded the response.
|
||||
body = JSON.parse(body);
|
||||
}
|
||||
err = new module.exports.MatrixError(body);
|
||||
} else if (bodyParser) {
|
||||
body = bodyParser(body);
|
||||
}
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
if (err) {
|
||||
err.httpStatus = response.statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
@@ -451,6 +756,7 @@ var requestCallback = function(defer, userDefinedCallback, onlyData) {
|
||||
* @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";
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/** @module interactive-auth */
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("./utils");
|
||||
|
||||
/**
|
||||
* Abstracts the logic used to drive the interactive auth process.
|
||||
*
|
||||
* <p>Components implementing an interactive auth flow should instantiate one of
|
||||
* these, passing in the necessary callbacks to the constructor. They should
|
||||
* then call attemptAuth, which will return a promise which will resolve or
|
||||
* reject when the interactive-auth process completes.
|
||||
*
|
||||
* <p>Meanwhile, calls will be made to the startAuthStage and doRequest
|
||||
* callbacks, and information gathered from the user can be submitted with
|
||||
* submitAuthDict.
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:interactive-auth
|
||||
*
|
||||
* @param {object} opts options object
|
||||
*
|
||||
* @param {object?} opts.authData error response from the last request. If
|
||||
* null, a request will be made with no auth before starting.
|
||||
*
|
||||
* @param {function(object?): module:client.Promise} opts.doRequest
|
||||
* called with the new auth dict to submit the request. Should return a
|
||||
* promise which resolves to the successful response or rejects with a
|
||||
* MatrixError.
|
||||
*
|
||||
* @param {function(string, object?)} opts.startAuthStage
|
||||
* called to ask the UI to start a particular auth stage. The arguments
|
||||
* are: the login type (eg m.login.password); and (if the last request
|
||||
* returned an error), an error object, with fields 'errcode' and 'error'.
|
||||
*
|
||||
*/
|
||||
function InteractiveAuth(opts) {
|
||||
this._data = opts.authData;
|
||||
this._requestCallback = opts.doRequest;
|
||||
this._startAuthStageCallback = opts.startAuthStage;
|
||||
this._completionDeferred = null;
|
||||
}
|
||||
|
||||
InteractiveAuth.prototype = {
|
||||
/**
|
||||
* begin the authentication process.
|
||||
*
|
||||
* @return {module:client.Promise} which resolves to the response on success,
|
||||
* or rejects with the error on failure.
|
||||
*/
|
||||
attemptAuth: function() {
|
||||
this._completionDeferred = q.defer();
|
||||
|
||||
if (!this._data) {
|
||||
this._doRequest(null);
|
||||
} else {
|
||||
this._startNextAuthStage();
|
||||
}
|
||||
|
||||
return this._completionDeferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* get the auth session ID
|
||||
*
|
||||
* @return {string} session id
|
||||
*/
|
||||
getSessionId: function() {
|
||||
return this._data ? this._data.session : undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* get the server params for a given stage
|
||||
*
|
||||
* @param {string} login type for the stage
|
||||
* @return {object?} any parameters from the server for this stage
|
||||
*/
|
||||
getStageParams: function(loginType) {
|
||||
var params = {};
|
||||
if (this._data && this._data.params) {
|
||||
params = this._data.params;
|
||||
}
|
||||
return params[loginType];
|
||||
},
|
||||
|
||||
/**
|
||||
* submit a new auth dict and fire off the request. This will either
|
||||
* make attemptAuth resolve/reject, or cause the startAuthStage callback
|
||||
* to be called for a new stage.
|
||||
*
|
||||
* @param {object} authData new auth dict to send to the server. Should
|
||||
* include a `type` propterty denoting the login type, as well as any
|
||||
* other params for that stage.
|
||||
*/
|
||||
submitAuthDict: function(authData) {
|
||||
if (!this._completionDeferred) {
|
||||
throw new Error("submitAuthDict() called before attemptAuth()");
|
||||
}
|
||||
|
||||
// use the sessionid from the last request.
|
||||
var auth = {
|
||||
session: this._data.session,
|
||||
};
|
||||
utils.extend(auth, authData);
|
||||
|
||||
this._doRequest(auth);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fire off a request, and either resolve the promise, or call
|
||||
* startAuthStage.
|
||||
*
|
||||
* @private
|
||||
* @param {object?} auth new auth dict, including session id
|
||||
*/
|
||||
_doRequest: function(auth) {
|
||||
var self = this;
|
||||
|
||||
// hackery to make sure that synchronous exceptions end up in the catch
|
||||
// handler (without the additional event loop entailed by q.fcall or an
|
||||
// extra q().then)
|
||||
var prom;
|
||||
try {
|
||||
prom = this._requestCallback(auth);
|
||||
} catch (e) {
|
||||
prom = q.reject(e);
|
||||
}
|
||||
|
||||
prom.then(
|
||||
function(result) {
|
||||
console.log("result from request: ", result);
|
||||
self._completionDeferred.resolve(result);
|
||||
}, function(error) {
|
||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||
// doesn't look like an interactive-auth failure. fail the whole lot.
|
||||
throw error;
|
||||
}
|
||||
self._data = error.data;
|
||||
self._startNextAuthStage();
|
||||
}
|
||||
).catch(this._completionDeferred.reject).done();
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick the next stage and call the callback
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_startNextAuthStage: function() {
|
||||
var nextStage = this._chooseStage();
|
||||
if (!nextStage) {
|
||||
throw new Error("No incomplete flows from the server");
|
||||
}
|
||||
|
||||
var stageError = null;
|
||||
if (this._data.errcode || this._data.error) {
|
||||
stageError = {
|
||||
errcode: this._data.errcode || "",
|
||||
error: this._data.error || "",
|
||||
};
|
||||
}
|
||||
this._startAuthStageCallback(nextStage, stageError);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick the next auth stage
|
||||
*
|
||||
* @private
|
||||
* @return {string?} login type
|
||||
*/
|
||||
_chooseStage: function() {
|
||||
var flow = this._chooseFlow();
|
||||
console.log("Active flow => %s", JSON.stringify(flow));
|
||||
var nextStage = this._firstUncompletedStage(flow);
|
||||
console.log("Next stage: %s", nextStage);
|
||||
return nextStage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick one of the flows from the returned list
|
||||
*
|
||||
* @private
|
||||
* @return {object} flow
|
||||
*/
|
||||
_chooseFlow: function() {
|
||||
var flows = this._data.flows || [];
|
||||
// always use the first flow for now
|
||||
return flows[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the first uncompleted stage in the given flow
|
||||
*
|
||||
* @private
|
||||
* @param {object} flow
|
||||
* @return {string} login type
|
||||
*/
|
||||
_firstUncompletedStage: function(flow) {
|
||||
var completed = (this._data || {}).completed || [];
|
||||
for (var i = 0; i < flow.stages.length; ++i) {
|
||||
var stageType = flow.stages[i];
|
||||
if (completed.indexOf(stageType) === -1) {
|
||||
return stageType;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/** */
|
||||
module.exports = InteractiveAuth;
|
||||
+64
-3
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
|
||||
@@ -15,9 +30,13 @@ module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
|
||||
module.exports.MatrixError = require("./http-api").MatrixError;
|
||||
/** The {@link module:client.MatrixClient|MatrixClient} class. */
|
||||
module.exports.MatrixClient = require("./client").MatrixClient;
|
||||
/** The {@link module:models/room~Room|Room} class. */
|
||||
/** The {@link module:models/room|Room} class. */
|
||||
module.exports.Room = require("./models/room");
|
||||
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
|
||||
/** The {@link module:models/event-timeline~EventTimeline} class. */
|
||||
module.exports.EventTimeline = require("./models/event-timeline");
|
||||
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
|
||||
module.exports.EventTimelineSet = require("./models/event-timeline-set");
|
||||
/** The {@link module:models/room-member|RoomMember} class. */
|
||||
module.exports.RoomMember = require("./models/room-member");
|
||||
/** The {@link module:models/room-state~RoomState|RoomState} class. */
|
||||
module.exports.RoomState = require("./models/room-state");
|
||||
@@ -30,6 +49,15 @@ module.exports.MatrixScheduler = require("./scheduler");
|
||||
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
|
||||
/** True if crypto libraries are being used on this client. */
|
||||
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
|
||||
/** {@link module:content-repo|ContentRepo} utility functions. */
|
||||
module.exports.ContentRepo = require("./content-repo");
|
||||
/** The {@link module:filter~Filter|Filter} class. */
|
||||
module.exports.Filter = require("./filter");
|
||||
/** The {@link module:timeline-window~TimelineWindow} class. */
|
||||
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
|
||||
/** The {@link module:interactive-auth} class. */
|
||||
module.exports.InteractiveAuth = require("./interactive-auth");
|
||||
|
||||
|
||||
/**
|
||||
* Create a new Matrix Call.
|
||||
@@ -54,6 +82,27 @@ module.exports.request = function(r) {
|
||||
request = r;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the currently-set request function.
|
||||
* @return {requestFunction} The current request function.
|
||||
*/
|
||||
module.exports.getRequest = function() {
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply wrapping code around the request function. The wrapper function is
|
||||
* installed as the new request handler, and when invoked it is passed the
|
||||
* previous value, along with the options and callback arguments.
|
||||
* @param {requestWrapperFunction} wrapper The wrapping function.
|
||||
*/
|
||||
module.exports.wrapRequest = function(wrapper) {
|
||||
var origRequest = request;
|
||||
request = function(options, callback) {
|
||||
return wrapper(origRequest, options, callback);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
@@ -77,7 +126,9 @@ module.exports.createClient = function(opts) {
|
||||
};
|
||||
}
|
||||
opts.request = opts.request || request;
|
||||
opts.store = opts.store || new module.exports.MatrixInMemoryStore();
|
||||
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
|
||||
localStorage: global.localStorage
|
||||
});
|
||||
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
|
||||
return new module.exports.MatrixClient(opts);
|
||||
};
|
||||
@@ -99,6 +150,16 @@ module.exports.createClient = function(opts) {
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A wrapper for the request function interface.
|
||||
* @callback requestWrapperFunction
|
||||
* @param {requestFunction} origRequest The underlying request function being
|
||||
* wrapped
|
||||
* @param {Object} opts The options for this HTTP request, given in the same
|
||||
* form as {@link requestFunction}.
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The request callback interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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/event-context
|
||||
*/
|
||||
|
||||
/**
|
||||
* Construct a new EventContext
|
||||
*
|
||||
* An eventcontext is used for circumstances such as search results, when we
|
||||
* have a particular event of interest, and a bunch of events before and after
|
||||
* it.
|
||||
*
|
||||
* It also stores pagination tokens for going backwards and forwards in the
|
||||
* timeline.
|
||||
*
|
||||
* @param {MatrixEvent} ourEvent the event at the centre of this context
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function EventContext(ourEvent) {
|
||||
this._timeline = [ourEvent];
|
||||
this._ourEventIndex = 0;
|
||||
this._paginateTokens = {b: null, f: null};
|
||||
|
||||
// this is used by MatrixClient to keep track of active requests
|
||||
this._paginateRequests = {b: null, f: null};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main event of interest
|
||||
*
|
||||
* This is a convenience function for getTimeline()[getOurEventIndex()].
|
||||
*
|
||||
* @return {MatrixEvent} The event at the centre of this context.
|
||||
*/
|
||||
EventContext.prototype.getEvent = function() {
|
||||
return this._timeline[this._ourEventIndex];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of events in this context
|
||||
*
|
||||
* @return {Array} An array of MatrixEvents
|
||||
*/
|
||||
EventContext.prototype.getTimeline = function() {
|
||||
return this._timeline;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the index in the timeline of our event
|
||||
*
|
||||
* @return {Number}
|
||||
*/
|
||||
EventContext.prototype.getOurEventIndex = function() {
|
||||
return this._ourEventIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a pagination token.
|
||||
*
|
||||
* @param {boolean} backwards true to get the pagination token for going
|
||||
* backwards in time
|
||||
* @return {string}
|
||||
*/
|
||||
EventContext.prototype.getPaginateToken = function(backwards) {
|
||||
return this._paginateTokens[backwards ? 'b' : 'f'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a pagination token.
|
||||
*
|
||||
* Generally this will be used only by the matrix js sdk.
|
||||
*
|
||||
* @param {string} token pagination token
|
||||
* @param {boolean} backwards true to set the pagination token for going
|
||||
* backwards in time
|
||||
*/
|
||||
EventContext.prototype.setPaginateToken = function(token, backwards) {
|
||||
this._paginateTokens[backwards ? 'b' : 'f'] = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add more events to the timeline
|
||||
*
|
||||
* @param {Array} events new events, in timeline order
|
||||
* @param {boolean} atStart true to insert new events at the start
|
||||
*/
|
||||
EventContext.prototype.addEvents = function(events, atStart) {
|
||||
// TODO: should we share logic with Room.addEventsToTimeline?
|
||||
// Should Room even use EventContext?
|
||||
|
||||
if (atStart) {
|
||||
this._timeline = events.concat(this._timeline);
|
||||
this._ourEventIndex += events.length;
|
||||
} else {
|
||||
this._timeline = this._timeline.concat(events);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The EventContext class
|
||||
*/
|
||||
module.exports = EventContext;
|
||||
@@ -0,0 +1,654 @@
|
||||
/*
|
||||
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";
|
||||
/**
|
||||
* @module models/event-timeline-set
|
||||
*/
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
var utils = require("../utils");
|
||||
var EventTimeline = require("./event-timeline");
|
||||
|
||||
// var DEBUG = false;
|
||||
var DEBUG = true;
|
||||
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
var debuglog = console.log.bind(console);
|
||||
} else {
|
||||
var debuglog = function() {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a set of EventTimeline objects, typically on behalf of a given
|
||||
* room. A room may have multiple EventTimelineSets for different levels
|
||||
* of filtering. The global notification list is also an EventTimelineSet, but
|
||||
* lacks a room.
|
||||
*
|
||||
* <p>This is an ordered sequence of timelines, which may or may not
|
||||
* be continuous. Each timeline lists a series of events, as well as tracking
|
||||
* the room state at the start and the end of the timeline (if appropriate).
|
||||
* It also tracks forward and backward pagination tokens, as well as containing
|
||||
* links to the next timeline in the sequence.
|
||||
*
|
||||
* <p>There is one special timeline - the 'live' timeline, which represents the
|
||||
* timeline to which events are being added in real-time as they are received
|
||||
* from the /sync API. Note that you should not retain references to this
|
||||
* timeline - even if it is the current timeline right now, it may not remain
|
||||
* so if the server gives us a timeline gap in /sync.
|
||||
*
|
||||
* <p>In order that we can find events from their ids later, we also maintain a
|
||||
* map from event_id to timeline and index.
|
||||
*
|
||||
* @constructor
|
||||
* @param {?Room} room the optional room for this timelineSet
|
||||
* @param {Object} opts hash of options inherited from Room.
|
||||
* opts.timelineSupport gives whether timeline support is enabled
|
||||
* opts.filter is the filter object, if any, for this timelineSet.
|
||||
*/
|
||||
function EventTimelineSet(room, opts) {
|
||||
this.room = room;
|
||||
|
||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||
this._liveTimeline = new EventTimeline(this);
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this._timelines = [this._liveTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
|
||||
this._filter = opts.filter || null;
|
||||
}
|
||||
utils.inherits(EventTimelineSet, EventEmitter);
|
||||
|
||||
/**
|
||||
* Get the filter object this timeline set is filtered on, if any
|
||||
* @return {?Filter} the optional filter for this timelineSet
|
||||
*/
|
||||
EventTimelineSet.prototype.getFilter = function() {
|
||||
return this._filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the filter object this timeline set is filtered on
|
||||
* (passed to the server when paginating via /messages).
|
||||
* @param {Filter} filter the filter for this timelineSet
|
||||
*/
|
||||
EventTimelineSet.prototype.setFilter = function(filter) {
|
||||
this._filter = filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of pending sent events for this timelineSet's room, filtered
|
||||
* by the timelineSet's filter if appropriate.
|
||||
*
|
||||
* @return {module:models/event.MatrixEvent[]} A list of the sent events
|
||||
* waiting for remote echo.
|
||||
*
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
*/
|
||||
EventTimelineSet.prototype.getPendingEvents = function() {
|
||||
if (!this.room) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this._filter) {
|
||||
return this._filter.filterRoomTimeline(this.room.getPendingEvents());
|
||||
}
|
||||
else {
|
||||
return this.room.getPendingEvents();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the live timeline for this room.
|
||||
*
|
||||
* @return {module:models/event-timeline~EventTimeline} live timeline
|
||||
*/
|
||||
EventTimelineSet.prototype.getLiveTimeline = function() {
|
||||
return this._liveTimeline;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the timeline (if any) this event is in.
|
||||
* @param {String} eventId the eventId being sought
|
||||
* @return {module:models/event-timeline~EventTimeline} timeline
|
||||
*/
|
||||
EventTimelineSet.prototype.eventIdToTimeline = function(eventId) {
|
||||
return this._eventIdToTimeline[eventId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Track a new event as if it were in the same timeline as an old event,
|
||||
* replacing it.
|
||||
* @param {String} oldEventId event ID of the original event
|
||||
* @param {String} newEventId event ID of the replacement event
|
||||
*/
|
||||
EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
|
||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the live timeline, and start a new one.
|
||||
*
|
||||
* <p>This is used when /sync returns a 'limited' timeline.
|
||||
*
|
||||
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
||||
* @param {?bool} flush Whether to flush the non-live timelines too.
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
||||
*/
|
||||
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
|
||||
var newTimeline;
|
||||
|
||||
if (!this._timelineSupport || flush) {
|
||||
// if timeline support is disabled, forget about the old timelines
|
||||
newTimeline = new EventTimeline(this);
|
||||
this._timelines = [newTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
} else {
|
||||
newTimeline = this.addTimeline();
|
||||
}
|
||||
|
||||
// initialise the state in the new timeline from our last known state
|
||||
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
|
||||
var events = [];
|
||||
for (var evtype in evMap) {
|
||||
if (!evMap.hasOwnProperty(evtype)) { continue; }
|
||||
for (var stateKey in evMap[evtype]) {
|
||||
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
|
||||
events.push(evMap[evtype][stateKey]);
|
||||
}
|
||||
}
|
||||
newTimeline.initialiseState(events);
|
||||
|
||||
// make sure we set the pagination token before firing timelineReset,
|
||||
// otherwise clients which start back-paginating will fail, and then get
|
||||
// stuck without realising that they *can* back-paginate.
|
||||
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
|
||||
|
||||
this._liveTimeline = newTimeline;
|
||||
this.emit("Room.timelineReset", this.room, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the timeline which contains the given event, if any
|
||||
*
|
||||
* @param {string} eventId event ID to look for
|
||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
|
||||
var res = this._eventIdToTimeline[eventId];
|
||||
return (res === undefined) ? null : res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an event which is stored in our timelines
|
||||
*
|
||||
* @param {string} eventId event ID to look for
|
||||
* @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
|
||||
*/
|
||||
EventTimelineSet.prototype.findEventById = function(eventId) {
|
||||
var tl = this.getTimelineForEvent(eventId);
|
||||
if (!tl) {
|
||||
return undefined;
|
||||
}
|
||||
return utils.findElement(tl.getEvents(),
|
||||
function(ev) { return ev.getId() == eventId; });
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new timeline to this timeline list
|
||||
*
|
||||
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
||||
*/
|
||||
EventTimelineSet.prototype.addTimeline = function() {
|
||||
if (!this._timelineSupport) {
|
||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
" parameter to true when creating MatrixClient to enable" +
|
||||
" it.");
|
||||
}
|
||||
|
||||
var timeline = new EventTimeline(this);
|
||||
this._timelines.push(timeline);
|
||||
return timeline;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add events to a timeline
|
||||
*
|
||||
* <p>Will fire "Room.timeline" for each event added.
|
||||
*
|
||||
* @param {MatrixEvent[]} events A list of events to add.
|
||||
*
|
||||
* @param {boolean} toStartOfTimeline True to add these events to the start
|
||||
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
|
||||
* event will be the <b>last</b> element of 'events'.
|
||||
*
|
||||
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
|
||||
* add events to.
|
||||
*
|
||||
* @param {string=} paginationToken token for the next batch of events
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
*
|
||||
*/
|
||||
EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
timeline, paginationToken) {
|
||||
if (!timeline) {
|
||||
throw new Error(
|
||||
"'timeline' not specified for EventTimelineSet.addEventsToTimeline"
|
||||
);
|
||||
}
|
||||
|
||||
if (!toStartOfTimeline && timeline == this._liveTimeline) {
|
||||
throw new Error(
|
||||
"EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
|
||||
"the live timeline - use Room.addLiveEvents instead"
|
||||
);
|
||||
}
|
||||
|
||||
if (this._filter) {
|
||||
events = this._filter.filterRoomTimeline(events);
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
|
||||
EventTimeline.FORWARDS;
|
||||
var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
|
||||
EventTimeline.BACKWARDS;
|
||||
|
||||
// Adding events to timelines can be quite complicated. The following
|
||||
// illustrates some of the corner-cases.
|
||||
//
|
||||
// Let's say we start by knowing about four timelines. timeline3 and
|
||||
// timeline4 are neighbours:
|
||||
//
|
||||
// timeline1 timeline2 timeline3 timeline4
|
||||
// [M] [P] [S] <------> [T]
|
||||
//
|
||||
// Now we paginate timeline1, and get the following events from the server:
|
||||
// [M, N, P, R, S, T, U].
|
||||
//
|
||||
// 1. First, we ignore event M, since we already know about it.
|
||||
//
|
||||
// 2. Next, we append N to timeline 1.
|
||||
//
|
||||
// 3. Next, we don't add event P, since we already know about it,
|
||||
// but we do link together the timelines. We now have:
|
||||
//
|
||||
// timeline1 timeline2 timeline3 timeline4
|
||||
// [M, N] <---> [P] [S] <------> [T]
|
||||
//
|
||||
// 4. Now we add event R to timeline2:
|
||||
//
|
||||
// timeline1 timeline2 timeline3 timeline4
|
||||
// [M, N] <---> [P, R] [S] <------> [T]
|
||||
//
|
||||
// Note that we have switched the timeline we are working on from
|
||||
// timeline1 to timeline2.
|
||||
//
|
||||
// 5. We ignore event S, but again join the timelines:
|
||||
//
|
||||
// timeline1 timeline2 timeline3 timeline4
|
||||
// [M, N] <---> [P, R] <---> [S] <------> [T]
|
||||
//
|
||||
// 6. We ignore event T, and the timelines are already joined, so there
|
||||
// is nothing to do.
|
||||
//
|
||||
// 7. Finally, we add event U to timeline4:
|
||||
//
|
||||
// timeline1 timeline2 timeline3 timeline4
|
||||
// [M, N] <---> [P, R] <---> [S] <------> [T, U]
|
||||
//
|
||||
// The important thing to note in the above is what happened when we
|
||||
// already knew about a given event:
|
||||
//
|
||||
// - if it was appropriate, we joined up the timelines (steps 3, 5).
|
||||
// - in any case, we started adding further events to the timeline which
|
||||
// contained the event we knew about (steps 3, 5, 6).
|
||||
//
|
||||
//
|
||||
// So much for adding events to the timeline. But what do we want to do
|
||||
// with the pagination token?
|
||||
//
|
||||
// In the case above, we will be given a pagination token which tells us how to
|
||||
// get events beyond 'U' - in this case, it makes sense to store this
|
||||
// against timeline4. But what if timeline4 already had 'U' and beyond? in
|
||||
// that case, our best bet is to throw away the pagination token we were
|
||||
// given and stick with whatever token timeline4 had previously. In short,
|
||||
// we want to only store the pagination token if the last event we receive
|
||||
// is one we didn't previously know about.
|
||||
//
|
||||
// We make an exception for this if it turns out that we already knew about
|
||||
// *all* of the events, and we weren't able to join up any timelines. When
|
||||
// that happens, it means our existing pagination token is faulty, since it
|
||||
// is only telling us what we already know. Rather than repeatedly
|
||||
// paginating with the same token, we might as well use the new pagination
|
||||
// token in the hope that we eventually work our way out of the mess.
|
||||
|
||||
var didUpdate = false;
|
||||
var lastEventWasNew = false;
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var event = events[i];
|
||||
var eventId = event.getId();
|
||||
|
||||
var existingTimeline = this._eventIdToTimeline[eventId];
|
||||
|
||||
if (!existingTimeline) {
|
||||
// we don't know about this event yet. Just add it to the timeline.
|
||||
this.addEventToTimeline(event, timeline, toStartOfTimeline);
|
||||
lastEventWasNew = true;
|
||||
didUpdate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
lastEventWasNew = false;
|
||||
|
||||
if (existingTimeline == timeline) {
|
||||
debuglog("Event " + eventId + " already in timeline " + timeline);
|
||||
continue;
|
||||
}
|
||||
|
||||
var neighbour = timeline.getNeighbouringTimeline(direction);
|
||||
if (neighbour) {
|
||||
// this timeline already has a neighbour in the relevant direction;
|
||||
// let's assume the timelines are already correctly linked up, and
|
||||
// skip over to it.
|
||||
//
|
||||
// there's probably some edge-case here where we end up with an
|
||||
// event which is in a timeline a way down the chain, and there is
|
||||
// a break in the chain somewhere. But I can't really imagine how
|
||||
// that would happen, so I'm going to ignore it for now.
|
||||
//
|
||||
if (existingTimeline == neighbour) {
|
||||
debuglog("Event " + eventId + " in neighbouring timeline - " +
|
||||
"switching to " + existingTimeline);
|
||||
} else {
|
||||
debuglog("Event " + eventId + " already in a different " +
|
||||
"timeline " + existingTimeline);
|
||||
}
|
||||
timeline = existingTimeline;
|
||||
continue;
|
||||
}
|
||||
|
||||
// time to join the timelines.
|
||||
console.info("Already have timeline for " + eventId +
|
||||
" - joining timeline " + timeline + " to " +
|
||||
existingTimeline);
|
||||
timeline.setNeighbouringTimeline(existingTimeline, direction);
|
||||
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
|
||||
timeline = existingTimeline;
|
||||
didUpdate = true;
|
||||
}
|
||||
|
||||
// see above - if the last event was new to us, or if we didn't find any
|
||||
// new information, we update the pagination token for whatever
|
||||
// timeline we ended up on.
|
||||
if (lastEventWasNew || !didUpdate) {
|
||||
timeline.setPaginationToken(paginationToken, direction);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an event to the end of this live timeline.
|
||||
*
|
||||
* @param {MatrixEvent} event Event to be added
|
||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||
*/
|
||||
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
|
||||
if (this._filter) {
|
||||
var events = this._filter.filterRoomTimeline([event]);
|
||||
if (!events.length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var timeline = this._eventIdToTimeline[event.getId()];
|
||||
if (timeline) {
|
||||
if (duplicateStrategy === "replace") {
|
||||
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
|
||||
event.getId());
|
||||
var tlEvents = timeline.getEvents();
|
||||
for (var j = 0; j < tlEvents.length; j++) {
|
||||
if (tlEvents[j].getId() === event.getId()) {
|
||||
// still need to set the right metadata on this event
|
||||
EventTimeline.setEventMetadata(
|
||||
event,
|
||||
timeline.getState(EventTimeline.FORWARDS),
|
||||
false
|
||||
);
|
||||
|
||||
if (!tlEvents[j].encryptedType) {
|
||||
tlEvents[j] = event;
|
||||
}
|
||||
|
||||
// XXX: we need to fire an event when this happens.
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
|
||||
event.getId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.addEventToTimeline(event, this._liveTimeline, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add event to the given timeline, and emit Room.timeline. Assumes
|
||||
* we have already checked we don't know about this event.
|
||||
*
|
||||
* Will fire "Room.timeline" for each event added.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* @param {EventTimeline} timeline
|
||||
* @param {boolean} toStartOfTimeline
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
*/
|
||||
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
|
||||
toStartOfTimeline) {
|
||||
var eventId = event.getId();
|
||||
timeline.addEvent(event, toStartOfTimeline);
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
|
||||
var data = {
|
||||
timeline: timeline,
|
||||
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
|
||||
};
|
||||
this.emit("Room.timeline", event, this.room,
|
||||
Boolean(toStartOfTimeline), false, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces event with ID oldEventId with one with newEventId, if oldEventId is
|
||||
* recognised. Otherwise, add to the live timeline. Used to handle remote echos.
|
||||
*
|
||||
* @param {MatrixEvent} localEvent the new event to be added to the timeline
|
||||
* @param {String} oldEventId the ID of the original event
|
||||
* @param {boolean} newEventId the ID of the replacement event
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
*/
|
||||
EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
|
||||
newEventId) {
|
||||
// XXX: why don't we infer newEventId from localEvent?
|
||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
} else {
|
||||
if (this._filter) {
|
||||
if (this._filter.filterRoomTimeline([localEvent]).length) {
|
||||
this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.addEventToTimeline(localEvent, this._liveTimeline, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a single event from this room.
|
||||
*
|
||||
* @param {String} eventId The id of the event to remove
|
||||
*
|
||||
* @return {?MatrixEvent} the removed event, or null if the event was not found
|
||||
* in this room.
|
||||
*/
|
||||
EventTimelineSet.prototype.removeEvent = function(eventId) {
|
||||
var timeline = this._eventIdToTimeline[eventId];
|
||||
if (!timeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var removed = timeline.removeEvent(eventId);
|
||||
if (removed) {
|
||||
delete this._eventIdToTimeline[eventId];
|
||||
var data = {
|
||||
timeline: timeline,
|
||||
};
|
||||
this.emit("Room.timeline", removed, this.room, undefined, true, data);
|
||||
}
|
||||
return removed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine where two events appear in the timeline relative to one another
|
||||
*
|
||||
* @param {string} eventId1 The id of the first event
|
||||
* @param {string} eventId2 The id of the second event
|
||||
|
||||
* @return {?number} a number less than zero if eventId1 precedes eventId2, and
|
||||
* greater than zero if eventId1 succeeds eventId2. zero if they are the
|
||||
* same event; null if we can't tell (either because we don't know about one
|
||||
* of the events, or because they are in separate timelines which don't join
|
||||
* up).
|
||||
*/
|
||||
EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
||||
if (eventId1 == eventId2) {
|
||||
// optimise this case
|
||||
return 0;
|
||||
}
|
||||
|
||||
var timeline1 = this._eventIdToTimeline[eventId1];
|
||||
var timeline2 = this._eventIdToTimeline[eventId2];
|
||||
|
||||
if (timeline1 === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (timeline2 === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (timeline1 === timeline2) {
|
||||
// both events are in the same timeline - figure out their
|
||||
// relative indices
|
||||
var idx1, idx2;
|
||||
var events = timeline1.getEvents();
|
||||
for (var idx = 0; idx < events.length &&
|
||||
(idx1 === undefined || idx2 === undefined); idx++) {
|
||||
var evId = events[idx].getId();
|
||||
if (evId == eventId1) {
|
||||
idx1 = idx;
|
||||
}
|
||||
if (evId == eventId2) {
|
||||
idx2 = idx;
|
||||
}
|
||||
}
|
||||
return idx1 - idx2;
|
||||
}
|
||||
|
||||
// the events are in different timelines. Iterate through the
|
||||
// linkedlist to see which comes first.
|
||||
|
||||
// first work forwards from timeline1
|
||||
var tl = timeline1;
|
||||
while (tl) {
|
||||
if (tl === timeline2) {
|
||||
// timeline1 is before timeline2
|
||||
return -1;
|
||||
}
|
||||
tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
||||
}
|
||||
|
||||
// now try backwards from timeline1
|
||||
tl = timeline1;
|
||||
while (tl) {
|
||||
if (tl === timeline2) {
|
||||
// timeline2 is before timeline1
|
||||
return 1;
|
||||
}
|
||||
tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
|
||||
}
|
||||
|
||||
// the timelines are not contiguous.
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The EventTimelineSet class.
|
||||
*/
|
||||
module.exports = EventTimelineSet;
|
||||
|
||||
/**
|
||||
* Fires whenever the timeline in a room is updated.
|
||||
* @event module:client~MatrixClient#"Room.timeline"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {?Room} room The room, if any, whose timeline was updated.
|
||||
* @param {boolean} toStartOfTimeline True if this event was added to the start
|
||||
* @param {boolean} removed True if this event has just been removed from the timeline
|
||||
* (beginning; oldest) of the timeline e.g. due to pagination.
|
||||
*
|
||||
* @param {object} data more data about the event
|
||||
*
|
||||
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
|
||||
* event was added to/removed from
|
||||
*
|
||||
* @param {boolean} data.liveEvent true if the event was a real-time event
|
||||
* added to the end of the live timeline
|
||||
*
|
||||
* @example
|
||||
* matrixClient.on("Room.timeline",
|
||||
* function(event, room, toStartOfTimeline, removed, data) {
|
||||
* if (!toStartOfTimeline && data.liveEvent) {
|
||||
* var messageToAppend = room.timeline.[room.timeline.length - 1];
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the live timeline in a room is reset.
|
||||
*
|
||||
* When we get a 'limited' sync (for example, after a network outage), we reset
|
||||
* the live timeline to be empty before adding the recent events to the new
|
||||
* timeline. This event is fired after the timeline is reset, and before the
|
||||
* new events are added.
|
||||
*
|
||||
* @event module:client~MatrixClient#"Room.timelineReset"
|
||||
* @param {Room} room The room whose live timeline was reset, if any
|
||||
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
|
||||
*/
|
||||
@@ -0,0 +1,338 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module models/event-timeline
|
||||
*/
|
||||
|
||||
var RoomState = require("./room-state");
|
||||
var utils = require("../utils");
|
||||
var MatrixEvent = require("./event").MatrixEvent;
|
||||
|
||||
/**
|
||||
* Construct a new EventTimeline
|
||||
*
|
||||
* <p>An EventTimeline represents a contiguous sequence of events in a room.
|
||||
*
|
||||
* <p>As well as keeping track of the events themselves, it stores the state of
|
||||
* the room at the beginning and end of the timeline, and pagination tokens for
|
||||
* going backwards and forwards in the timeline.
|
||||
*
|
||||
* <p>In order that clients can meaningfully maintain an index into a timeline,
|
||||
* the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
|
||||
* incremented when events are prepended to the timeline. The index of an event
|
||||
* relative to baseIndex therefore remains constant.
|
||||
*
|
||||
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
||||
* doubly-linked list.
|
||||
*
|
||||
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
|
||||
* @constructor
|
||||
*/
|
||||
function EventTimeline(eventTimelineSet) {
|
||||
this._eventTimelineSet = eventTimelineSet;
|
||||
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
|
||||
this._events = [];
|
||||
this._baseIndex = 0;
|
||||
this._startState = new RoomState(this._roomId);
|
||||
this._startState.paginationToken = null;
|
||||
this._endState = new RoomState(this._roomId);
|
||||
this._endState.paginationToken = null;
|
||||
|
||||
this._prevTimeline = null;
|
||||
this._nextTimeline = null;
|
||||
|
||||
// this is used by client.js
|
||||
this._paginationRequests = {'b': null, 'f': null};
|
||||
|
||||
this._name = this._roomId + ":" + new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Symbolic constant for methods which take a 'direction' argument:
|
||||
* refers to the start of the timeline, or backwards in time.
|
||||
*/
|
||||
EventTimeline.BACKWARDS = "b";
|
||||
|
||||
/**
|
||||
* Symbolic constant for methods which take a 'direction' argument:
|
||||
* refers to the end of the timeline, or forwards in time.
|
||||
*/
|
||||
EventTimeline.FORWARDS = "f";
|
||||
|
||||
/**
|
||||
* Initialise the start and end state with the given events
|
||||
*
|
||||
* <p>This can only be called before any events are added.
|
||||
*
|
||||
* @param {MatrixEvent[]} stateEvents list of state events to initialise the
|
||||
* state with.
|
||||
* @throws {Error} if an attempt is made to call this after addEvent is called.
|
||||
*/
|
||||
EventTimeline.prototype.initialiseState = function(stateEvents) {
|
||||
if (this._events.length > 0) {
|
||||
throw new Error("Cannot initialise state after events are added");
|
||||
}
|
||||
|
||||
// we deep-copy the events here, in case they get changed later - we don't
|
||||
// want changes to the start state leaking through to the end state.
|
||||
var oldStateEvents = utils.map(
|
||||
utils.deepCopy(
|
||||
stateEvents.map(function(mxEvent) { return mxEvent.event; })
|
||||
), function(ev) { return new MatrixEvent(ev); });
|
||||
|
||||
this._startState.setStateEvents(oldStateEvents);
|
||||
this._endState.setStateEvents(stateEvents);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the ID of the room for this timeline
|
||||
* @return {string} room ID
|
||||
*/
|
||||
EventTimeline.prototype.getRoomId = function() {
|
||||
return this._roomId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the filter for this timeline's timelineSet (if any)
|
||||
* @return {Filter} filter
|
||||
*/
|
||||
EventTimeline.prototype.getFilter = function() {
|
||||
return this._eventTimelineSet.getFilter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the timelineSet for this timeline
|
||||
* @return {EventTimelineSet} timelineSet
|
||||
*/
|
||||
EventTimeline.prototype.getTimelineSet = function() {
|
||||
return this._eventTimelineSet;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the base index.
|
||||
*
|
||||
* <p>This is an index which is incremented when events are prepended to the
|
||||
* timeline. An individual event therefore stays at the same index in the array
|
||||
* relative to the base index (although note that a given event's index may
|
||||
* well be less than the base index, thus giving that event a negative relative
|
||||
* index).
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
EventTimeline.prototype.getBaseIndex = function() {
|
||||
return this._baseIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of events in this context
|
||||
*
|
||||
* @return {MatrixEvent[]} An array of MatrixEvents
|
||||
*/
|
||||
EventTimeline.prototype.getEvents = function() {
|
||||
return this._events;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the room state at the start/end of the timeline
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
||||
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
||||
* of the timeline.
|
||||
*
|
||||
* @return {RoomState} state at the start/end of the timeline
|
||||
*/
|
||||
EventTimeline.prototype.getState = function(direction) {
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
return this._startState;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
return this._endState;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a pagination token
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to get the pagination
|
||||
* token for going backwards in time; EventTimeline.FORWARDS to get the
|
||||
* pagination token for going forwards in time.
|
||||
*
|
||||
* @return {?string} pagination token
|
||||
*/
|
||||
EventTimeline.prototype.getPaginationToken = function(direction) {
|
||||
return this.getState(direction).paginationToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a pagination token
|
||||
*
|
||||
* @param {?string} token pagination token
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
|
||||
* token for going backwards in time; EventTimeline.FORWARDS to set the
|
||||
* pagination token for going forwards in time.
|
||||
*/
|
||||
EventTimeline.prototype.setPaginationToken = function(token, direction) {
|
||||
this.getState(direction).paginationToken = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the next timeline in the series
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to get the previous
|
||||
* timeline; EventTimeline.FORWARDS to get the next timeline.
|
||||
*
|
||||
* @return {?EventTimeline} previous or following timeline, if they have been
|
||||
* joined up.
|
||||
*/
|
||||
EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
return this._prevTimeline;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
return this._nextTimeline;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the next timeline in the series
|
||||
*
|
||||
* @param {EventTimeline} neighbour previous/following timeline
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to set the previous
|
||||
* timeline; EventTimeline.FORWARDS to set the next timeline.
|
||||
*
|
||||
* @throws {Error} if an attempt is made to set the neighbouring timeline when
|
||||
* it is already set.
|
||||
*/
|
||||
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
|
||||
if (this.getNeighbouringTimeline(direction)) {
|
||||
throw new Error("timeline already has a neighbouring timeline - " +
|
||||
"cannot reset neighbour");
|
||||
}
|
||||
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
this._prevTimeline = neighbour;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
this._nextTimeline = neighbour;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
|
||||
// make sure we don't try to paginate this timeline
|
||||
this.setPaginationToken(null, direction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new event to the timeline, and update the state
|
||||
*
|
||||
* @param {MatrixEvent} event new event
|
||||
* @param {boolean} atStart true to insert new event at the start
|
||||
*/
|
||||
EventTimeline.prototype.addEvent = function(event, atStart) {
|
||||
var stateContext = atStart ? this._startState : this._endState;
|
||||
|
||||
// only call setEventMetadata on the unfiltered timelineSets
|
||||
var timelineSet = this.getTimelineSet();
|
||||
if (timelineSet.room &&
|
||||
timelineSet.room.getUnfilteredTimelineSet() === timelineSet)
|
||||
{
|
||||
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||
|
||||
// modify state
|
||||
if (event.isState()) {
|
||||
stateContext.setStateEvents([event]);
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
// it again if the prop wasn't previously set. It may also mean that
|
||||
// the sender/target is updated (if the event set was a room member event)
|
||||
// so we want to use the *updated* member (new avatar/name) instead.
|
||||
//
|
||||
// However, we do NOT want to do this on member events if we're going
|
||||
// back in time, else we'll set the .sender value for BEFORE the given
|
||||
// member event, whereas we want to set the .sender value for the ACTUAL
|
||||
// member event itself.
|
||||
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
||||
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var insertIndex;
|
||||
|
||||
if (atStart) {
|
||||
insertIndex = 0;
|
||||
} else {
|
||||
insertIndex = this._events.length;
|
||||
}
|
||||
|
||||
this._events.splice(insertIndex, 0, event); // insert element
|
||||
if (atStart) {
|
||||
this._baseIndex++;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Static helper method to set sender and target properties
|
||||
*
|
||||
* @param {MatrixEvent} event the event whose metadata is to be set
|
||||
* @param {RoomState} stateContext the room state to be queried
|
||||
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
|
||||
*/
|
||||
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
|
||||
// set sender and target properties
|
||||
event.sender = stateContext.getSentinelMember(
|
||||
event.getSender()
|
||||
);
|
||||
if (event.getType() === "m.room.member") {
|
||||
event.target = stateContext.getSentinelMember(
|
||||
event.getStateKey()
|
||||
);
|
||||
}
|
||||
if (event.isState()) {
|
||||
// room state has no concept of 'old' or 'current', but we want the
|
||||
// room state to regress back to previous values if toStartOfTimeline
|
||||
// is set, which means inspecting prev_content if it exists. This
|
||||
// is done by toggling the forwardLooking flag.
|
||||
if (toStartOfTimeline) {
|
||||
event.forwardLooking = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an event from the timeline
|
||||
*
|
||||
* @param {string} eventId ID of event to be removed
|
||||
* @return {?MatrixEvent} removed event, or null if not found
|
||||
*/
|
||||
EventTimeline.prototype.removeEvent = function(eventId) {
|
||||
for (var i = this._events.length - 1; i >= 0; i--) {
|
||||
var ev = this._events[i];
|
||||
if (ev.getId() == eventId) {
|
||||
this._events.splice(i, 1);
|
||||
if (i < this._baseIndex) {
|
||||
this._baseIndex--;
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a string to identify this timeline, for debugging
|
||||
*
|
||||
* @return {string} name for this timeline
|
||||
*/
|
||||
EventTimeline.prototype.toString = function() {
|
||||
return this._name;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The EventTimeline class
|
||||
*/
|
||||
module.exports = EventTimeline;
|
||||
+278
-22
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/**
|
||||
@@ -6,6 +21,9 @@
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var utils = require('../utils.js');
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
@@ -15,21 +33,33 @@
|
||||
module.exports.EventStatus = {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT: "not_sent",
|
||||
|
||||
/** The message is being encrypted */
|
||||
ENCRYPTING: "encrypting",
|
||||
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING: "sending",
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED: "queued"
|
||||
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 {boolean} encrypted Was the event encrypted
|
||||
* @prop {Object} event The raw 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 {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.
|
||||
@@ -39,15 +69,24 @@ module.exports.EventStatus = {
|
||||
* 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, encrypted) {
|
||||
module.exports.MatrixEvent = function MatrixEvent(
|
||||
event
|
||||
) {
|
||||
this.event = event || {};
|
||||
this.sender = null;
|
||||
this.target = null;
|
||||
this.status = null;
|
||||
this.forwardLooking = true;
|
||||
this.encrypted = Boolean(encrypted);
|
||||
this._pushActions = null;
|
||||
|
||||
this._clearEvent = {};
|
||||
this._keysProved = {};
|
||||
this._keysClaimed = {};
|
||||
};
|
||||
module.exports.MatrixEvent.prototype = {
|
||||
utils.inherits(module.exports.MatrixEvent, EventEmitter);
|
||||
|
||||
|
||||
utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
@@ -63,23 +102,26 @@ module.exports.MatrixEvent.prototype = {
|
||||
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
|
||||
*/
|
||||
getSender: function() {
|
||||
return this.event.user_id;
|
||||
return this.event.sender || this.event.user_id; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the type of event.
|
||||
* Get the (decrypted, if necessary) type of event.
|
||||
*
|
||||
* @return {string} The event type, e.g. <code>m.room.message</code>
|
||||
*/
|
||||
getType: function() {
|
||||
return this.event.type;
|
||||
return this._clearEvent.type || this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the type of the event that will be sent to the homeserver.
|
||||
* Get the (possibly encrypted) type of the event that will be sent to the
|
||||
* homeserver.
|
||||
*
|
||||
* @return {string} The event type.
|
||||
*/
|
||||
getWireType: function() {
|
||||
return this.encryptedType || this.event.type;
|
||||
return this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -101,19 +143,22 @@ module.exports.MatrixEvent.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event content JSON.
|
||||
* Get the (decrypted, if necessary) event content JSON.
|
||||
*
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getContent: function() {
|
||||
return this.event.content || {};
|
||||
return this._clearEvent.content || this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event content JSON that will be sent to the homeserver.
|
||||
* 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.encryptedContent || this.event.content || {};
|
||||
return this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -122,12 +167,15 @@ module.exports.MatrixEvent.prototype = {
|
||||
* @return {Object} The previous event content JSON, or an empty object.
|
||||
*/
|
||||
getPrevContent: function() {
|
||||
return this.event.prev_content || {};
|
||||
// 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.
|
||||
@@ -143,7 +191,7 @@ module.exports.MatrixEvent.prototype = {
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
*/
|
||||
getAge: function() {
|
||||
return this.event.age;
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -163,11 +211,219 @@ module.exports.MatrixEvent.prototype = {
|
||||
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.
|
||||
* @param {object} keys The local keys claimed and proved by this event.
|
||||
*/
|
||||
makeEncrypted: function(crypto_type, crypto_content, keys) {
|
||||
// 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;
|
||||
this._keysProved = keys;
|
||||
this._keysClaimed = keys;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the cleartext data on this event.
|
||||
*
|
||||
* (This is used after decrypting an event; it should not be used by applications).
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.decrypted"
|
||||
*
|
||||
* @param {Object} clearEvent The plaintext payload for the event
|
||||
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*
|
||||
* @param {Object=} keysProved Keys owned by the sender of this event.
|
||||
* See {@link module:models/event.MatrixEvent#getKeysProved}.
|
||||
*
|
||||
* @param {Object=} keysClaimed Keys the sender of this event claims.
|
||||
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
|
||||
*/
|
||||
setClearData: function(clearEvent, keysProved, keysClaimed) {
|
||||
this._clearEvent = clearEvent;
|
||||
this._keysProved = keysProved || {};
|
||||
this._keysClaimed = keysClaimed || {};
|
||||
this.emit("Event.decrypted", this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the event is encrypted.
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
isEncrypted: function() {
|
||||
return this.encrypted;
|
||||
}
|
||||
return this.event.type === "m.room.encrypted";
|
||||
},
|
||||
|
||||
/**
|
||||
* The curve25519 key that sent this event
|
||||
* @return {string}
|
||||
*/
|
||||
getSenderKey: function() {
|
||||
return this.getKeysProved().curve25519 || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* The keys that must have been owned by the sender of this encrypted event.
|
||||
* <p>
|
||||
* These don't necessarily have to come from this event itself, but may be
|
||||
* implied by the cryptographic session.
|
||||
*
|
||||
* @return {Object<string, string>}
|
||||
*/
|
||||
getKeysProved: function() {
|
||||
return this._keysProved;
|
||||
},
|
||||
|
||||
/**
|
||||
* The additional keys the sender of this encrypted event claims to possess.
|
||||
* <p>
|
||||
* These don't necessarily have to come from this event itself, but may be
|
||||
* implied by the cryptographic session.
|
||||
* For example megolm messages don't claim keys directly, but instead
|
||||
* inherit a claim from the olm message that established the session.
|
||||
*
|
||||
* @return {Object<string, string>}
|
||||
*/
|
||||
getKeysClaimed: function() {
|
||||
return this._keysClaimed;
|
||||
},
|
||||
|
||||
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 {module:models/event.MatrixEvent} redaction_event
|
||||
* event causing the redaction
|
||||
*/
|
||||
makeRedacted: function(redaction_event) {
|
||||
// quick sanity-check
|
||||
if (!redaction_event.event) {
|
||||
throw new Error("invalid redaction_event in makeRedacted");
|
||||
}
|
||||
|
||||
// we attempt to replicate what we would see from the server if
|
||||
// the event had been redacted before we saw it.
|
||||
//
|
||||
// The server removes (most of) the content of the event, and adds a
|
||||
// "redacted_because" key to the unsigned section containing the
|
||||
// redacted event.
|
||||
if (!this.event.unsigned) {
|
||||
this.event.unsigned = {};
|
||||
}
|
||||
this.event.unsigned.redacted_because = redaction_event.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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the push actions, if known, for this event
|
||||
*
|
||||
* @return {?Object} push actions
|
||||
*/
|
||||
getPushActions: function() {
|
||||
return this._pushActions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the push actions for this event.
|
||||
*
|
||||
* @param {Object} pushActions push actions
|
||||
*/
|
||||
setPushActions: function(pushActions) {
|
||||
this._pushActions = pushActions;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/* 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},
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Fires when an event is decrypted
|
||||
*
|
||||
* @event module:models/event.MatrixEvent#"Event.decrypted"
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event
|
||||
* The matrix event which has been decrypted
|
||||
*/
|
||||
|
||||
+78
-33
@@ -1,14 +1,33 @@
|
||||
/*
|
||||
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-member
|
||||
*/
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
var ContentRepo = require("../content-repo");
|
||||
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Construct a new room member.
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:models/room-member
|
||||
*
|
||||
* @param {string} roomId The room ID of the member.
|
||||
* @param {string} userId The user ID of the member.
|
||||
* @prop {string} roomId The room ID for this member.
|
||||
@@ -61,11 +80,11 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
|
||||
this.name = calculateDisplayName(this, event, roomState);
|
||||
if (oldMembership !== this.membership) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.membership", event, this);
|
||||
this.emit("RoomMember.membership", event, this, oldMembership);
|
||||
}
|
||||
if (oldName !== this.name) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.name", event, this);
|
||||
this.emit("RoomMember.name", event, this, oldName);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,11 +105,14 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
|
||||
});
|
||||
var oldPowerLevel = this.powerLevel;
|
||||
var oldPowerLevelNorm = this.powerLevelNorm;
|
||||
this.powerLevel = (
|
||||
powerLevelEvent.getContent().users[this.userId] ||
|
||||
powerLevelEvent.getContent().users_default ||
|
||||
0
|
||||
);
|
||||
|
||||
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
|
||||
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
|
||||
} else if (powerLevelEvent.getContent().users_default !== undefined) {
|
||||
this.powerLevel = powerLevelEvent.getContent().users_default;
|
||||
} else {
|
||||
this.powerLevel = 0;
|
||||
}
|
||||
this.powerLevelNorm = 0;
|
||||
if (maxLevel > 0) {
|
||||
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
||||
@@ -147,25 +169,49 @@ RoomMember.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the avatar URL for a room member.
|
||||
* @param {string} baseUrl The base homeserver URL See
|
||||
* {@link module:client~MatrixClient#getHomeserverUrl}.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDefault (optional) Passing false causes this method to
|
||||
* return null if the user has no avatar image. Otherwise, a default image URL
|
||||
* will be returned. Default: true.
|
||||
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
|
||||
* returned even if it is a direct hyperlink rather than a matrix content URL.
|
||||
* If false, any non-matrix content URLs will be ignored. Setting this option to
|
||||
* true will expose URLs that, if fetched, will leak information about the user
|
||||
* to anyone who they share a room with.
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
RoomMember.prototype.getAvatarUrl =
|
||||
function(baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
|
||||
if (allowDefault === undefined) { allowDefault = true; }
|
||||
if (!this.events.member && !allowDefault) {
|
||||
return null;
|
||||
}
|
||||
var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
|
||||
var httpUrl = ContentRepo.getHttpUriForMxc(
|
||||
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks
|
||||
);
|
||||
if (httpUrl) {
|
||||
return httpUrl;
|
||||
}
|
||||
else if (allowDefault) {
|
||||
return ContentRepo.getIdenticonUri(
|
||||
baseUrl, this.userId, width, height
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function calculateDisplayName(member, event, roomState) {
|
||||
var displayName = event.getDirectionalContent().displayname;
|
||||
var selfUserId = member.userId;
|
||||
|
||||
/*
|
||||
// FIXME: this would be great but still needs to use the
|
||||
// full userId to disambiguate if needed...
|
||||
|
||||
if (!displayName) {
|
||||
var matches = selfUserId.match(/^@(.*?):/);
|
||||
if (matches) {
|
||||
return matches[1];
|
||||
}
|
||||
else {
|
||||
return selfUserId;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (!displayName) {
|
||||
return selfUserId;
|
||||
}
|
||||
@@ -174,18 +220,13 @@ function calculateDisplayName(member, event, roomState) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
var stateEvents = utils.filter(
|
||||
roomState.getStateEvents("m.room.member"),
|
||||
function(e) {
|
||||
return e.getContent().displayname === displayName &&
|
||||
e.getSender() !== selfUserId;
|
||||
}
|
||||
);
|
||||
if (stateEvents.length > 0) {
|
||||
// need to disambiguate
|
||||
var userIds = roomState.getUserIdsWithDisplayName(displayName);
|
||||
var otherUsers = userIds.filter(function(u) {
|
||||
return u !== selfUserId;
|
||||
});
|
||||
if (otherUsers.length > 0) {
|
||||
return displayName + " (" + selfUserId + ")";
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@@ -199,6 +240,8 @@ module.exports = RoomMember;
|
||||
* @event module:client~MatrixClient#"RoomMember.name"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.name changed.
|
||||
* @param {string?} oldName The previous name. Null if the member didn't have a
|
||||
* name previously.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.name", function(event, member){
|
||||
* var newName = member.name;
|
||||
@@ -210,8 +253,10 @@ module.exports = RoomMember;
|
||||
* @event module:client~MatrixClient#"RoomMember.membership"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.membership changed.
|
||||
* @param {string?} oldMembership The previous membership state. Null if it's a
|
||||
* new member.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.membership", function(event, member){
|
||||
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
|
||||
* var newState = member.membership;
|
||||
* });
|
||||
*/
|
||||
|
||||
+208
-1
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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
|
||||
@@ -10,7 +25,8 @@ var RoomMember = require("./room-member");
|
||||
/**
|
||||
* Construct room state.
|
||||
* @constructor
|
||||
* @param {string} roomId Required. The ID of the room which has this state.
|
||||
* @param {?string} roomId Optional. The ID of the room which has this state.
|
||||
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
|
||||
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
|
||||
* on the user's ID.
|
||||
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
|
||||
@@ -31,6 +47,9 @@ function RoomState(roomId) {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this._updateModifiedTime();
|
||||
this._displayNameToUserIds = {};
|
||||
this._userIdsToDisplayNames = {};
|
||||
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
|
||||
}
|
||||
utils.inherits(RoomState, EventEmitter);
|
||||
|
||||
@@ -108,6 +127,12 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -121,6 +146,21 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
|
||||
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);
|
||||
@@ -149,6 +189,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
var members = utils.values(self.members);
|
||||
utils.forEach(members, function(member) {
|
||||
member.setPowerLevelEvent(event);
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -164,6 +205,16 @@ RoomState.prototype.setTypingEvent = function(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.
|
||||
*/
|
||||
@@ -180,11 +231,167 @@ 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"
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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-summary
|
||||
|
||||
+1188
-175
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
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/search-result
|
||||
*/
|
||||
|
||||
var EventContext = require("./event-context");
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Construct a new SearchResult
|
||||
*
|
||||
* @param {number} rank where this SearchResult ranks in the results
|
||||
* @param {event-context.EventContext} eventContext the matching event and its
|
||||
* context
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function SearchResult(rank, eventContext) {
|
||||
this.rank = rank;
|
||||
this.context = eventContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SearchResponse from the response to /search
|
||||
* @static
|
||||
* @param {Object} jsonObj
|
||||
* @param {function} eventMapper
|
||||
* @return {SearchResult}
|
||||
*/
|
||||
|
||||
SearchResult.fromJson = function(jsonObj, eventMapper) {
|
||||
var jsonContext = jsonObj.context || {};
|
||||
var events_before = jsonContext.events_before || [];
|
||||
var events_after = jsonContext.events_after || [];
|
||||
|
||||
var context = new EventContext(eventMapper(jsonObj.result));
|
||||
|
||||
context.setPaginateToken(jsonContext.start, true);
|
||||
context.addEvents(utils.map(events_before, eventMapper), true);
|
||||
context.addEvents(utils.map(events_after, eventMapper), false);
|
||||
context.setPaginateToken(jsonContext.end, false);
|
||||
|
||||
return new SearchResult(jsonObj.rank, context);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The SearchResult class
|
||||
*/
|
||||
module.exports = SearchResult;
|
||||
+122
-8
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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/user
|
||||
@@ -15,16 +30,28 @@
|
||||
* @prop {string} displayName The 'displayname' of the user if known.
|
||||
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
|
||||
* @prop {string} presence The presence enum if known.
|
||||
* @prop {Number} lastActiveAgo The last time the user performed some action in ms.
|
||||
* @prop {string} presenceStatusMsg The presence status message if known.
|
||||
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
|
||||
* proactively with the server, or we saw a message from the user
|
||||
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
|
||||
* received presence data for this user. We can subtract
|
||||
* lastActiveAgo from this to approximate an absolute value for
|
||||
* when a user was last active.
|
||||
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
|
||||
* an approximation and that the user should be seen as active 'now'
|
||||
* @prop {Object} events The events describing this user.
|
||||
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
||||
*/
|
||||
function User(userId) {
|
||||
this.userId = userId;
|
||||
this.presence = "offline";
|
||||
this.presenceStatusMsg = null;
|
||||
this.displayName = userId;
|
||||
this.rawDisplayName = userId;
|
||||
this.avatarUrl = null;
|
||||
this.lastActiveAgo = 0;
|
||||
this.lastPresenceTs = 0;
|
||||
this.currentlyActive = false;
|
||||
this.events = {
|
||||
presence: null,
|
||||
profile: null
|
||||
@@ -53,27 +80,82 @@ User.prototype.setPresenceEvent = function(event) {
|
||||
if (event.getContent().presence !== this.presence || firstFire) {
|
||||
eventsToFire.push("User.presence");
|
||||
}
|
||||
if (event.getContent().avatar_url !== this.avatarUrl) {
|
||||
if (event.getContent().avatar_url &&
|
||||
event.getContent().avatar_url !== this.avatarUrl)
|
||||
{
|
||||
eventsToFire.push("User.avatarUrl");
|
||||
}
|
||||
if (event.getContent().displayname !== this.displayName) {
|
||||
if (event.getContent().displayname &&
|
||||
event.getContent().displayname !== this.displayName)
|
||||
{
|
||||
eventsToFire.push("User.displayName");
|
||||
}
|
||||
if (event.getContent().currently_active !== undefined &&
|
||||
event.getContent().currently_active !== this.currentlyActive)
|
||||
{
|
||||
eventsToFire.push("User.currentlyActive");
|
||||
}
|
||||
|
||||
this.presence = event.getContent().presence;
|
||||
this.displayName = event.getContent().displayname;
|
||||
this.avatarUrl = event.getContent().avatar_url;
|
||||
this.lastActiveAgo = event.getContent().last_active_ago;
|
||||
eventsToFire.push("User.lastPresenceTs");
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
this._updateModifiedTime();
|
||||
if (event.getContent().status_msg) {
|
||||
this.presenceStatusMsg = event.getContent().status_msg;
|
||||
}
|
||||
if (event.getContent().displayname) {
|
||||
this.displayName = event.getContent().displayname;
|
||||
}
|
||||
if (event.getContent().avatar_url) {
|
||||
this.avatarUrl = event.getContent().avatar_url;
|
||||
}
|
||||
this.lastActiveAgo = event.getContent().last_active_ago;
|
||||
this.lastPresenceTs = Date.now();
|
||||
this.currentlyActive = event.getContent().currently_active;
|
||||
|
||||
this._updateModifiedTime();
|
||||
|
||||
for (var i = 0; i < eventsToFire.length; i++) {
|
||||
this.emit(eventsToFire[i], event, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manually set this user's display name. No event is emitted in response to this
|
||||
* as there is no underlying MatrixEvent to emit with.
|
||||
* @param {string} name The new display name.
|
||||
*/
|
||||
User.prototype.setDisplayName = function(name) {
|
||||
var oldName = this.displayName;
|
||||
this.displayName = name;
|
||||
if (name !== oldName) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Manually set this user's non-disambiguated display name. No event is emitted
|
||||
* in response to this as there is no underlying MatrixEvent to emit with.
|
||||
* @param {string} name The new display name.
|
||||
*/
|
||||
User.prototype.setRawDisplayName = function(name) {
|
||||
this.rawDisplayName = name;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Manually set this user's avatar URL. No event is emitted in response to this
|
||||
* as there is no underlying MatrixEvent to emit with.
|
||||
* @param {string} url The new avatar URL.
|
||||
*/
|
||||
User.prototype.setAvatarUrl = function(url) {
|
||||
var oldUrl = this.avatarUrl;
|
||||
this.avatarUrl = url;
|
||||
if (url !== oldUrl) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the last modified time to the current time.
|
||||
*/
|
||||
@@ -91,11 +173,32 @@ User.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the absolute timestamp when this User was last known active on the server.
|
||||
* It is *NOT* accurate if this.currentlyActive is true.
|
||||
* @return {number} The timestamp
|
||||
*/
|
||||
User.prototype.getLastActiveTs = function() {
|
||||
return this.lastPresenceTs - this.lastActiveAgo;
|
||||
};
|
||||
|
||||
/**
|
||||
* The User class.
|
||||
*/
|
||||
module.exports = User;
|
||||
|
||||
/**
|
||||
* Fires whenever any user's lastPresenceTs changes,
|
||||
* ie. whenever any presence event is received for a user.
|
||||
* @event module:client~MatrixClient#"User.lastPresenceTs"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {User} user The user whose User.lastPresenceTs changed.
|
||||
* @example
|
||||
* matrixClient.on("User.lastPresenceTs", function(event, user){
|
||||
* var newlastPresenceTs = user.lastPresenceTs;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's presence changes.
|
||||
* @event module:client~MatrixClient#"User.presence"
|
||||
@@ -107,6 +210,17 @@ module.exports = User;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's currentlyActive changes.
|
||||
* @event module:client~MatrixClient#"User.currentlyActive"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {User} user The user whose User.currentlyActive changed.
|
||||
* @example
|
||||
* matrixClient.on("User.currentlyActive", function(event, user){
|
||||
* var newCurrentlyActive = user.currentlyActive;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's display name changes.
|
||||
* @event module:client~MatrixClient#"User.displayName"
|
||||
|
||||
+75
-25
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
@@ -107,10 +122,12 @@ function PushProcessor(client) {
|
||||
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
|
||||
if (!cond.is) { return false; }
|
||||
|
||||
var room = client.getRoom(ev.room_id);
|
||||
var room = client.getRoom(ev.getRoomId());
|
||||
if (!room || !room.currentState || !room.currentState.members) { return false; }
|
||||
|
||||
var memberCount = Object.keys(room.currentState.members).length;
|
||||
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; }
|
||||
@@ -135,18 +152,21 @@ function PushProcessor(client) {
|
||||
};
|
||||
|
||||
var eventFulfillsDisplayNameCondition = function(cond, ev) {
|
||||
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
|
||||
var content = ev.getContent();
|
||||
if (!content || !content.body || typeof content.body != 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var room = client.getRoom(ev.room_id);
|
||||
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;
|
||||
|
||||
var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i');
|
||||
return ev.content.body.search(pat) > -1;
|
||||
// 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) {
|
||||
@@ -159,7 +179,7 @@ function PushProcessor(client) {
|
||||
|
||||
var pat;
|
||||
if (cond.key == 'content.body') {
|
||||
pat = '\\b' + globToRegexp(cond.pattern) + '\\b';
|
||||
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
|
||||
} else {
|
||||
pat = '^' + globToRegexp(cond.pattern) + '$';
|
||||
}
|
||||
@@ -185,7 +205,21 @@ function PushProcessor(client) {
|
||||
|
||||
var valueForDottedKey = function(key, ev) {
|
||||
var parts = key.split('.');
|
||||
var val = ev;
|
||||
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; }
|
||||
@@ -195,8 +229,8 @@ function PushProcessor(client) {
|
||||
};
|
||||
|
||||
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets) { return null; }
|
||||
if (ev.user_id == client.credentials.userId) { return null; }
|
||||
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) {
|
||||
@@ -209,25 +243,11 @@ function PushProcessor(client) {
|
||||
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);
|
||||
var actionObj = PushProcessor.actionListToActionsObject(rule.actions);
|
||||
|
||||
// Some actions are implicit in some situations: we add those here
|
||||
if (actionObj.tweaks.highlight === undefined) {
|
||||
@@ -239,11 +259,40 @@ function PushProcessor(client) {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a list of actions into a object with the actions as keys and their values
|
||||
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
|
||||
* becomes { notify: true, tweaks: { sound: 'default' } }
|
||||
* @param {array} actionlist The actions list
|
||||
*
|
||||
* @return {object} A object with key 'notify' (true or false) and an object of actions
|
||||
*/
|
||||
PushProcessor.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;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} PushAction
|
||||
* @type {Object}
|
||||
@@ -257,3 +306,4 @@ function PushProcessor(client) {
|
||||
|
||||
/** The PushProcessor class. */
|
||||
module.exports = PushProcessor;
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/* A re-implementation of the javascript callback functions (setTimeout,
|
||||
* clearTimeout; setInterval and clearInterval are not yet implemented) which
|
||||
* try to improve handling of large clock jumps (as seen when
|
||||
* suspending/resuming the system).
|
||||
*
|
||||
* In particular, if a timeout would have fired while the system was suspended,
|
||||
* it will instead fire as soon as possible after resume.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// we schedule a callback at least this often, to check if we've missed out on
|
||||
// some wall-clock time due to being suspended.
|
||||
var TIMER_CHECK_PERIOD_MS = 1000;
|
||||
|
||||
// counter, for making up ids to return from setTimeout
|
||||
var _count = 0;
|
||||
|
||||
// the key for our callback with the real global.setTimeout
|
||||
var _realCallbackKey;
|
||||
|
||||
// a sorted list of the callbacks to be run.
|
||||
// each is an object with keys [runAt, func, params, key].
|
||||
var _callbackList = [];
|
||||
|
||||
// var debuglog = console.log.bind(console);
|
||||
var debuglog = function() {};
|
||||
|
||||
/**
|
||||
* Replace the function used by this module to get the current time.
|
||||
*
|
||||
* Intended for use by the unit tests.
|
||||
*
|
||||
* @param {function} f function which should return a millisecond counter
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
module.exports.setNow = function(f) {
|
||||
_now = f || Date.now;
|
||||
};
|
||||
var _now = Date.now;
|
||||
|
||||
/**
|
||||
* reimplementation of window.setTimeout, which will call the callback if
|
||||
* the wallclock time goes past the deadline.
|
||||
*
|
||||
* @param {function} func callback to be called after a delay
|
||||
* @param {Number} delayMs number of milliseconds to delay by
|
||||
*
|
||||
* @return {Number} an identifier for this callback, which may be passed into
|
||||
* clearTimeout later.
|
||||
*/
|
||||
module.exports.setTimeout = function(func, delayMs) {
|
||||
delayMs = delayMs || 0;
|
||||
if (delayMs < 0) {
|
||||
delayMs = 0;
|
||||
}
|
||||
|
||||
var params = Array.prototype.slice.call(arguments, 2);
|
||||
var runAt = _now() + delayMs;
|
||||
var key = _count++;
|
||||
debuglog("setTimeout: scheduling cb", key, "at", runAt,
|
||||
"(delay", delayMs, ")");
|
||||
var data = {
|
||||
runAt: runAt,
|
||||
func: func,
|
||||
params: params,
|
||||
key: key,
|
||||
};
|
||||
|
||||
// figure out where it goes in the list
|
||||
var idx = binarySearch(
|
||||
_callbackList, function(el) {
|
||||
return el.runAt - runAt;
|
||||
}
|
||||
);
|
||||
|
||||
_callbackList.splice(idx, 0, data);
|
||||
_scheduleRealCallback();
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
/**
|
||||
* reimplementation of window.clearTimeout, which mirrors setTimeout
|
||||
*
|
||||
* @param {Number} key result from an earlier setTimeout call
|
||||
*/
|
||||
module.exports.clearTimeout = function(key) {
|
||||
if (_callbackList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the element from the list
|
||||
var i;
|
||||
for (i = 0; i < _callbackList.length; i++) {
|
||||
var cb = _callbackList[i];
|
||||
if (cb.key == key) {
|
||||
_callbackList.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// iff it was the first one in the list, reschedule our callback.
|
||||
if (i === 0) {
|
||||
_scheduleRealCallback();
|
||||
}
|
||||
};
|
||||
|
||||
// use the real global.setTimeout to schedule a callback to _runCallbacks.
|
||||
function _scheduleRealCallback() {
|
||||
if (_realCallbackKey) {
|
||||
global.clearTimeout(_realCallbackKey);
|
||||
}
|
||||
|
||||
var first = _callbackList[0];
|
||||
|
||||
if (!first) {
|
||||
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
|
||||
return;
|
||||
}
|
||||
|
||||
var now = _now();
|
||||
var delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
|
||||
|
||||
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
|
||||
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
|
||||
}
|
||||
|
||||
function _runCallbacks() {
|
||||
var cb;
|
||||
var now = _now();
|
||||
debuglog("_runCallbacks: now:", now);
|
||||
|
||||
// get the list of things to call
|
||||
var callbacksToRun = [];
|
||||
while (true) {
|
||||
var first = _callbackList[0];
|
||||
if (!first || first.runAt > now) {
|
||||
break;
|
||||
}
|
||||
cb = _callbackList.shift();
|
||||
debuglog("_runCallbacks: popping", cb.key);
|
||||
callbacksToRun.push(cb);
|
||||
}
|
||||
|
||||
// reschedule the real callback before running our functions, to
|
||||
// keep the codepaths the same whether or not our functions
|
||||
// register their own setTimeouts.
|
||||
_scheduleRealCallback();
|
||||
|
||||
for (var i = 0; i < callbacksToRun.length; i++) {
|
||||
cb = callbacksToRun[i];
|
||||
try {
|
||||
cb.func.apply(null, cb.params);
|
||||
} catch (e) {
|
||||
console.error("Uncaught exception in callback function",
|
||||
e.stack || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* search in a sorted array.
|
||||
*
|
||||
* returns the index of the last element for which func returns
|
||||
* greater than zero, or array.length if no such element exists.
|
||||
*/
|
||||
function binarySearch(array, func) {
|
||||
// min is inclusive, max exclusive.
|
||||
var min = 0,
|
||||
max = array.length;
|
||||
|
||||
while (min < max) {
|
||||
var mid = (min + max) >> 1;
|
||||
var res = func(array[mid]);
|
||||
if (res > 0) {
|
||||
// the element at 'mid' is too big; set it as the new max.
|
||||
max = mid;
|
||||
} else {
|
||||
// the element at 'mid' is too small. 'min' is inclusive, so +1.
|
||||
min = mid + 1;
|
||||
}
|
||||
}
|
||||
// presumably, min==max now.
|
||||
return min;
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 which manages queuing, scheduling and retrying
|
||||
@@ -133,6 +148,12 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
// client error; no amount of retrying with save you now.
|
||||
return -1;
|
||||
}
|
||||
// we ship with browser-request which returns { cors: rejected } when trying
|
||||
// with no connection, so if we match that, give up since they have no conn.
|
||||
if (err.cors === "rejected") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
var waitTime = err.data.retry_after_ms;
|
||||
if (waitTime) {
|
||||
|
||||
+167
-2
@@ -1,15 +1,36 @@
|
||||
/*
|
||||
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 MatrixInMemoryStore} for the public class.
|
||||
* @module store/memory
|
||||
*/
|
||||
var utils = require("../utils");
|
||||
var User = require("../models/user");
|
||||
|
||||
/**
|
||||
* Construct a new in-memory data store for the Matrix Client.
|
||||
* @constructor
|
||||
* @param {Object=} opts Config options
|
||||
* @param {LocalStorage} opts.localStorage The local storage instance to persist
|
||||
* some forms of data such as tokens. Rooms will NOT be stored. See
|
||||
* {@link WebStorageStore} to persist rooms.
|
||||
*/
|
||||
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
|
||||
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
opts = opts || {};
|
||||
this.rooms = {
|
||||
// roomId: Room
|
||||
};
|
||||
@@ -17,6 +38,15 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
|
||||
// userId: User
|
||||
};
|
||||
this.syncToken = null;
|
||||
this.filters = {
|
||||
// userId: {
|
||||
// filterId: Filter
|
||||
// }
|
||||
};
|
||||
this.accountData = {
|
||||
// type : content
|
||||
};
|
||||
this.localStorage = opts.localStorage;
|
||||
};
|
||||
|
||||
module.exports.MatrixInMemoryStore.prototype = {
|
||||
@@ -29,6 +59,7 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
return this.syncToken;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Set the token to stream from.
|
||||
* @param {string} token The token to stream from.
|
||||
@@ -43,6 +74,43 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
*/
|
||||
storeRoom: function(room) {
|
||||
this.rooms[room.roomId] = room;
|
||||
// add listeners for room member changes so we can keep the room member
|
||||
// map up-to-date.
|
||||
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
|
||||
// add existing members
|
||||
var self = this;
|
||||
room.currentState.getMembers().forEach(function(m) {
|
||||
self._onRoomMember(null, room.currentState, m);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a room member in a room being tracked by this store has been
|
||||
* updated.
|
||||
* @param {MatrixEvent} event
|
||||
* @param {RoomState} state
|
||||
* @param {RoomMember} member
|
||||
*/
|
||||
_onRoomMember: function(event, state, member) {
|
||||
if (member.membership === "invite") {
|
||||
// We do NOT add invited members because people love to typo user IDs
|
||||
// which would then show up in these lists (!)
|
||||
return;
|
||||
}
|
||||
|
||||
var user = this.users[member.userId] || new User(member.userId);
|
||||
if (member.name) {
|
||||
user.setDisplayName(member.name);
|
||||
if (member.events.member) {
|
||||
user.setRawDisplayName(
|
||||
member.events.member.getDirectionalContent().displayname
|
||||
);
|
||||
}
|
||||
}
|
||||
if (member.events.member && member.events.member.getContent().avatar_url) {
|
||||
user.setAvatarUrl(member.events.member.getContent().avatar_url);
|
||||
}
|
||||
this.users[user.userId] = user;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -62,6 +130,17 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
return utils.values(this.rooms);
|
||||
},
|
||||
|
||||
/**
|
||||
* Permanently delete a room.
|
||||
* @param {string} roomId
|
||||
*/
|
||||
removeRoom: function(roomId) {
|
||||
if (this.rooms[roomId]) {
|
||||
this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember);
|
||||
}
|
||||
delete this.rooms[roomId];
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a summary of all the rooms.
|
||||
* @return {RoomSummary[]} A summary of each room.
|
||||
@@ -89,6 +168,14 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
return this.users[userId] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve all known users.
|
||||
* @return {User[]} A list of users, which may be empty.
|
||||
*/
|
||||
getUsers: function() {
|
||||
return utils.values(this.users);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve scrollback for this room.
|
||||
* @param {Room} room The matrix room
|
||||
@@ -109,7 +196,85 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
*/
|
||||
storeEvents: function(room, events, token, toStart) {
|
||||
// no-op because they've already been added to the room instance.
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a filter.
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
storeFilter: function(filter) {
|
||||
if (!filter) { return; }
|
||||
if (!this.filters[filter.userId]) {
|
||||
this.filters[filter.userId] = {};
|
||||
}
|
||||
this.filters[filter.userId][filter.filterId] = filter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter.
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @return {?Filter} A filter or null.
|
||||
*/
|
||||
getFilter: function(userId, filterId) {
|
||||
if (!this.filters[userId] || !this.filters[userId][filterId]) {
|
||||
return null;
|
||||
}
|
||||
return this.filters[userId][filterId];
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter ID with the given name.
|
||||
* @param {string} filterName The filter name.
|
||||
* @return {?string} The filter ID or null.
|
||||
*/
|
||||
getFilterIdByName: function(filterName) {
|
||||
if (!this.localStorage) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
|
||||
}
|
||||
catch (e) {}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a filter name to ID mapping.
|
||||
* @param {string} filterName
|
||||
* @param {string} filterId
|
||||
*/
|
||||
setFilterIdByName: function(filterName, filterId) {
|
||||
if (!this.localStorage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
|
||||
}
|
||||
catch (e) {}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store user-scoped account data events.
|
||||
* N.B. that account data only allows a single event per type, so multiple
|
||||
* events with the same type will replace each other.
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
*/
|
||||
storeAccountDataEvents: function(events) {
|
||||
var self = this;
|
||||
events.forEach(function(event) {
|
||||
self.accountData[event.getType()] = event;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get account data event by event type
|
||||
* @param {string} eventType The event type being queried
|
||||
* @return {?MatrixEvent} the user account_data event of given type, if any
|
||||
*/
|
||||
getAccountData: function(eventType) {
|
||||
return this.accountData[eventType];
|
||||
},
|
||||
|
||||
// TODO
|
||||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/**
|
||||
@@ -47,6 +62,22 @@ WebStorageSessionStore.prototype = {
|
||||
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a flag indicating that we have announced the new device.
|
||||
*/
|
||||
setDeviceAnnounced: function() {
|
||||
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the "device announced" flag is set
|
||||
*
|
||||
* @return {boolean} true if the "device announced" flag has been set.
|
||||
*/
|
||||
getDeviceAnnounced: function() {
|
||||
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the known devices for a user.
|
||||
* @param {string} userId The user's ID.
|
||||
@@ -89,6 +120,16 @@ WebStorageSessionStore.prototype = {
|
||||
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||||
},
|
||||
|
||||
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
|
||||
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
|
||||
return this.store.getItem(key);
|
||||
},
|
||||
|
||||
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
|
||||
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
|
||||
return this.store.setItem(key, pickledSession);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the end-to-end state for a room.
|
||||
* @param {string} roomId The room's ID.
|
||||
@@ -109,6 +150,7 @@ WebStorageSessionStore.prototype = {
|
||||
};
|
||||
|
||||
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
var KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
@@ -118,6 +160,10 @@ function keyEndToEndSessions(deviceKey) {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
|
||||
return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId;
|
||||
}
|
||||
|
||||
function keyEndToEndRoom(roomId) {
|
||||
return E2E_PREFIX + "rooms/" + roomId;
|
||||
}
|
||||
|
||||
+83
-1
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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.
|
||||
@@ -54,6 +69,14 @@ StubStore.prototype = {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Permanently delete a room.
|
||||
* @param {string} roomId
|
||||
*/
|
||||
removeRoom: function(roomId) {
|
||||
return;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @return {Array} An empty array.
|
||||
@@ -78,6 +101,14 @@ StubStore.prototype = {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @return {User[]}
|
||||
*/
|
||||
getUsers: function() {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Room} room
|
||||
@@ -96,7 +127,58 @@ StubStore.prototype = {
|
||||
* @param {boolean} toStart True if these are paginated results.
|
||||
*/
|
||||
storeEvents: function(room, events, token, toStart) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a filter.
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
storeFilter: function(filter) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter.
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @return {?Filter} A filter or null.
|
||||
*/
|
||||
getFilter: function(userId, filterId) {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter ID with the given name.
|
||||
* @param {string} filterName The filter name.
|
||||
* @return {?string} The filter ID or null.
|
||||
*/
|
||||
getFilterIdByName: function(filterName) {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a filter name to ID mapping.
|
||||
* @param {string} filterName
|
||||
* @param {string} filterId
|
||||
*/
|
||||
setFilterIdByName: function(filterName, filterId) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Store user-scoped account data events
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
*/
|
||||
storeAccountDataEvents: function(events) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Get account data event by event type
|
||||
* @param {string} eventType The event type being queried
|
||||
*/
|
||||
getAccountData: function(eventType) {
|
||||
|
||||
},
|
||||
|
||||
// TODO
|
||||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||||
|
||||
+38
-3
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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:
|
||||
@@ -358,7 +373,7 @@ WebStorageStore.prototype.scrollback = function(room, limit) {
|
||||
);
|
||||
room.addEventsToTimeline(utils.map(scrollback, function(e) {
|
||||
return new MatrixEvent(e);
|
||||
}), true);
|
||||
}), true, room.getLiveTimeline());
|
||||
|
||||
this._tokens[room.storageToken] = {
|
||||
earliestIndex: earliestIndex
|
||||
@@ -458,6 +473,24 @@ WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
|
||||
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: {}
|
||||
@@ -514,7 +547,9 @@ SerialisedRoom.fromRoom = function(room, batchSize) {
|
||||
};
|
||||
|
||||
function loadRoom(store, roomId, numEvents, tokenArray) {
|
||||
var room = new Room(roomId, tokenArray.length);
|
||||
var room = new Room(roomId, {
|
||||
storageToken: tokenArray.length
|
||||
});
|
||||
|
||||
// populate state (flatten nested struct to event array)
|
||||
var currentStateMap = getItem(store, keyName(roomId, "state"));
|
||||
@@ -559,7 +594,7 @@ function loadRoom(store, roomId, numEvents, tokenArray) {
|
||||
index--;
|
||||
}
|
||||
// add events backwards to diverge old state correctly.
|
||||
room.addEventsToTimeline(recentEvents.reverse(), true);
|
||||
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.
|
||||
|
||||
+1138
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,483 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/** @module timeline-window */
|
||||
|
||||
var q = require("q");
|
||||
var EventTimeline = require("./models/event-timeline");
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
var DEBUG = false;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
var debuglog = DEBUG ? console.log.bind(console) : function() {};
|
||||
|
||||
/**
|
||||
* the number of times we ask the server for more events before giving up
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* Construct a TimelineWindow.
|
||||
*
|
||||
* <p>This abstracts the separate timelines in a Matrix {@link
|
||||
* module:models/room|Room} into a single iterable thing. It keeps track of
|
||||
* the start and endpoints of the window, which can be advanced with the help
|
||||
* of pagination requests.
|
||||
*
|
||||
* <p>Before the window is useful, it must be initialised by calling {@link
|
||||
* module:timeline-window~TimelineWindow#load|load}.
|
||||
*
|
||||
* <p>Note that the window will not automatically extend itself when new events
|
||||
* are received from /sync; you should arrange to call {@link
|
||||
* module:timeline-window~TimelineWindow#paginate|paginate} on {@link
|
||||
* module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events.
|
||||
*
|
||||
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
||||
* requests.
|
||||
*
|
||||
* @param {EventTimelineSet} timelineSet The timelineSet to track
|
||||
*
|
||||
* @param {Object} [opts] Configuration options for this window
|
||||
*
|
||||
* @param {number} [opts.windowLimit = 1000] maximum number of events to keep
|
||||
* in the window. If more events are retrieved via pagination requests,
|
||||
* excess events will be dropped from the other end of the window.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function TimelineWindow(client, timelineSet, opts) {
|
||||
opts = opts || {};
|
||||
this._client = client;
|
||||
this._timelineSet = timelineSet;
|
||||
|
||||
// these will be TimelineIndex objects; they delineate the 'start' and
|
||||
// 'end' of the window.
|
||||
//
|
||||
// _start.index is inclusive; _end.index is exclusive.
|
||||
this._start = null;
|
||||
this._end = null;
|
||||
|
||||
this._eventCount = 0;
|
||||
this._windowLimit = opts.windowLimit || 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the window to point at a given event, or the live timeline
|
||||
*
|
||||
* @param {string} [initialEventId] If given, the window will contain the
|
||||
* given event
|
||||
* @param {number} [initialWindowSize = 20] Size of the initial window
|
||||
*
|
||||
* @return {module:client.Promise}
|
||||
*/
|
||||
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
var self = this;
|
||||
initialWindowSize = initialWindowSize || 20;
|
||||
|
||||
// given an EventTimeline, and an event index within it, initialise our
|
||||
// fields so that the event in question is in the middle of the window.
|
||||
var initFields = function(timeline, eventIndex) {
|
||||
var endIndex = Math.min(timeline.getEvents().length,
|
||||
eventIndex + Math.ceil(initialWindowSize / 2));
|
||||
var startIndex = Math.max(0, endIndex - initialWindowSize);
|
||||
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
|
||||
self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
|
||||
self._eventCount = endIndex - startIndex;
|
||||
};
|
||||
|
||||
// We avoid delaying the resolution of the promise by a reactor tick if
|
||||
// we already have the data we need, which is important to keep room-switching
|
||||
// feeling snappy.
|
||||
//
|
||||
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
|
||||
// skip straight to the find-event loop.
|
||||
if (initialEventId) {
|
||||
return this._client.getEventTimeline(this._timelineSet, initialEventId)
|
||||
.then(function(tl) {
|
||||
// make sure that our window includes the event
|
||||
for (var i = 0; i < tl.getEvents().length; i++) {
|
||||
if (tl.getEvents()[i].getId() == initialEventId) {
|
||||
initFields(tl, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("getEventTimeline result didn't include requested event");
|
||||
});
|
||||
} else {
|
||||
// start with the most recent events
|
||||
var tl = this._timelineSet.getLiveTimeline();
|
||||
initFields(tl, tl.getEvents().length);
|
||||
return q();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this window can be extended
|
||||
*
|
||||
* <p>This returns true if we either have more events, or if we have a
|
||||
* pagination token which means we can paginate in that direction. It does not
|
||||
* necessarily mean that there are more events available in that direction at
|
||||
* this time.
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to check if we can
|
||||
* paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
|
||||
*
|
||||
* @return {boolean} true if we can paginate in the given direction
|
||||
*/
|
||||
TimelineWindow.prototype.canPaginate = function(direction) {
|
||||
var tl;
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
tl = this._start;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
tl = this._end;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
if (tl.index > tl.minIndex()) { return true; }
|
||||
} else {
|
||||
if (tl.index < tl.maxIndex()) { return true; }
|
||||
}
|
||||
|
||||
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
|
||||
tl.timeline.getPaginationToken(direction));
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to extend the window
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to extend the window
|
||||
* backwards (towards older events); EventTimeline.FORWARDS to go forwards.
|
||||
*
|
||||
* @param {number} size number of events to try to extend by. If fewer than this
|
||||
* number are immediately available, then we return immediately rather than
|
||||
* making an API call.
|
||||
*
|
||||
* @param {boolean} [makeRequest = true] whether we should make API calls to
|
||||
* fetch further events if we don't have any at all. (This has no effect if
|
||||
* the room already knows about additional events in the relevant direction,
|
||||
* even if there are fewer than 'size' of them, as we will just return those
|
||||
* we already know about.)
|
||||
*
|
||||
* @param {number} [requestLimit = 5] limit for the number of API requests we
|
||||
* should make.
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to a boolean which is true if more events
|
||||
* were successfully retrieved.
|
||||
*/
|
||||
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
requestLimit) {
|
||||
// Either wind back the message cap (if there are enough events in the
|
||||
// timeline to do so), or fire off a pagination request.
|
||||
|
||||
if (makeRequest === undefined) {
|
||||
makeRequest = true;
|
||||
}
|
||||
|
||||
if (requestLimit === undefined) {
|
||||
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
|
||||
}
|
||||
|
||||
var tl;
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
tl = this._start;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
tl = this._end;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
return q(false);
|
||||
}
|
||||
|
||||
if (tl.pendingPaginate) {
|
||||
return tl.pendingPaginate;
|
||||
}
|
||||
|
||||
// try moving the cap
|
||||
var count = (direction == EventTimeline.BACKWARDS) ?
|
||||
tl.retreat(size) : tl.advance(size);
|
||||
|
||||
if (count) {
|
||||
this._eventCount += count;
|
||||
debuglog("TimelineWindow: increased cap by " + count +
|
||||
" (now " + this._eventCount + ")");
|
||||
// remove some events from the other end, if necessary
|
||||
var excess = this._eventCount - this._windowLimit;
|
||||
if (excess > 0) {
|
||||
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
}
|
||||
return q(true);
|
||||
}
|
||||
|
||||
if (!makeRequest || requestLimit === 0) {
|
||||
// todo: should we return something different to indicate that there
|
||||
// might be more events out there, but we haven't found them yet?
|
||||
return q(false);
|
||||
}
|
||||
|
||||
// try making a pagination request
|
||||
var token = tl.timeline.getPaginationToken(direction);
|
||||
if (!token) {
|
||||
debuglog("TimelineWindow: no token");
|
||||
return q(false);
|
||||
}
|
||||
|
||||
debuglog("TimelineWindow: starting request");
|
||||
var self = this;
|
||||
|
||||
var prom = this._client.paginateEventTimeline(tl.timeline, {
|
||||
backwards: direction == EventTimeline.BACKWARDS,
|
||||
limit: size
|
||||
}).finally(function() {
|
||||
tl.pendingPaginate = null;
|
||||
}).then(function(r) {
|
||||
debuglog("TimelineWindow: request completed with result " + r);
|
||||
if (!r) {
|
||||
// end of timeline
|
||||
return false;
|
||||
}
|
||||
|
||||
// recurse to advance the index into the results.
|
||||
//
|
||||
// If we don't get any new events, we want to make sure we keep asking
|
||||
// the server for events for as long as we have a valid pagination
|
||||
// token. In particular, we want to know if we've actually hit the
|
||||
// start of the timeline, or if we just happened to know about all of
|
||||
// the events thanks to https://matrix.org/jira/browse/SYN-645.
|
||||
//
|
||||
// On the other hand, we necessarily want to wait forever for the
|
||||
// server to make its mind up about whether there are other events,
|
||||
// because it gives a bad user experience
|
||||
// (https://github.com/vector-im/vector-web/issues/1204).
|
||||
return self.paginate(direction, size, true, requestLimit - 1);
|
||||
});
|
||||
tl.pendingPaginate = prom;
|
||||
return prom;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove `delta` events from the start or end of the timeline.
|
||||
*
|
||||
* @param {number} delta number of events to remove from the timeline
|
||||
* @param {boolean} startOfTimeline if events should be removed from the start
|
||||
* of the timeline.
|
||||
*/
|
||||
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
|
||||
var tl = startOfTimeline ? this._start : this._end;
|
||||
|
||||
// sanity-check the delta
|
||||
if (delta > this._eventCount || delta < 0) {
|
||||
throw new Error("Attemting to unpaginate " + delta + " events, but " +
|
||||
"only have " + this._eventCount + " in the timeline");
|
||||
}
|
||||
|
||||
while (delta > 0) {
|
||||
var count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
|
||||
if (count <= 0) {
|
||||
// sadness. This shouldn't be possible.
|
||||
throw new Error(
|
||||
"Unable to unpaginate any further, but still have " +
|
||||
this._eventCount + " events");
|
||||
}
|
||||
|
||||
delta -= count;
|
||||
this._eventCount -= count;
|
||||
debuglog("TimelineWindow.unpaginate: dropped " + count +
|
||||
" (now " + this._eventCount + ")");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of the events currently in the window
|
||||
*
|
||||
* @return {MatrixEvent[]} the events in the window
|
||||
*/
|
||||
TimelineWindow.prototype.getEvents = function() {
|
||||
if (!this._start) {
|
||||
// not yet loaded
|
||||
return [];
|
||||
}
|
||||
|
||||
var result = [];
|
||||
|
||||
// iterate through each timeline between this._start and this._end
|
||||
// (inclusive).
|
||||
var timeline = this._start.timeline;
|
||||
while (true) {
|
||||
var events = timeline.getEvents();
|
||||
|
||||
// For the first timeline in the chain, we want to start at
|
||||
// this._start.index. For the last timeline in the chain, we want to
|
||||
// stop before this._end.index. Otherwise, we want to copy all of the
|
||||
// events in the timeline.
|
||||
//
|
||||
// (Note that both this._start.index and this._end.index are relative
|
||||
// to their respective timelines' BaseIndex).
|
||||
//
|
||||
var startIndex = 0, endIndex = events.length;
|
||||
if (timeline === this._start.timeline) {
|
||||
startIndex = this._start.index + timeline.getBaseIndex();
|
||||
}
|
||||
if (timeline === this._end.timeline) {
|
||||
endIndex = this._end.index + timeline.getBaseIndex();
|
||||
}
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++) {
|
||||
result.push(events[i]);
|
||||
}
|
||||
|
||||
// if we're not done, iterate to the next timeline.
|
||||
if (timeline === this._end.timeline) {
|
||||
break;
|
||||
} else {
|
||||
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* a thing which contains a timeline reference, and an index into it.
|
||||
*
|
||||
* @constructor
|
||||
* @param {EventTimeline} timeline
|
||||
* @param {number} index
|
||||
* @private
|
||||
*/
|
||||
function TimelineIndex(timeline, index) {
|
||||
this.timeline = timeline;
|
||||
|
||||
// the indexes are relative to BaseIndex, so could well be negative.
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number} the minimum possible value for the index in the current
|
||||
* timeline
|
||||
*/
|
||||
TimelineIndex.prototype.minIndex = function() {
|
||||
return this.timeline.getBaseIndex() * -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {number} the maximum possible value for the index in the current
|
||||
* timeline (exclusive - ie, it actually returns one more than the index
|
||||
* of the last element).
|
||||
*/
|
||||
TimelineIndex.prototype.maxIndex = function() {
|
||||
return this.timeline.getEvents().length - this.timeline.getBaseIndex();
|
||||
};
|
||||
|
||||
/**
|
||||
* Try move the index forward, or into the neighbouring timeline
|
||||
*
|
||||
* @param {number} delta number of events to advance by
|
||||
* @return {number} number of events successfully advanced by
|
||||
*/
|
||||
TimelineIndex.prototype.advance = function(delta) {
|
||||
if (!delta) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// first try moving the index in the current timeline. See if there is room
|
||||
// to do so.
|
||||
var cappedDelta;
|
||||
if (delta < 0) {
|
||||
// we want to wind the index backwards.
|
||||
//
|
||||
// (this.minIndex() - this.index) is a negative number whose magnitude
|
||||
// is the amount of room we have to wind back the index in the current
|
||||
// timeline. We cap delta to this quantity.
|
||||
cappedDelta = Math.max(delta, this.minIndex() - this.index);
|
||||
if (cappedDelta < 0) {
|
||||
this.index += cappedDelta;
|
||||
return cappedDelta;
|
||||
}
|
||||
} else {
|
||||
// we want to wind the index forwards.
|
||||
//
|
||||
// (this.maxIndex() - this.index) is a (positive) number whose magnitude
|
||||
// is the amount of room we have to wind forward the index in the current
|
||||
// timeline. We cap delta to this quantity.
|
||||
cappedDelta = Math.min(delta, this.maxIndex() - this.index);
|
||||
if (cappedDelta > 0) {
|
||||
this.index += cappedDelta;
|
||||
return cappedDelta;
|
||||
}
|
||||
}
|
||||
|
||||
// the index is already at the start/end of the current timeline.
|
||||
//
|
||||
// next see if there is a neighbouring timeline to switch to.
|
||||
var neighbour = this.timeline.getNeighbouringTimeline(
|
||||
delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
|
||||
if (neighbour) {
|
||||
this.timeline = neighbour;
|
||||
if (delta < 0) {
|
||||
this.index = this.maxIndex();
|
||||
} else {
|
||||
this.index = this.minIndex();
|
||||
}
|
||||
|
||||
debuglog("paginate: switched to new neighbour");
|
||||
|
||||
// recurse, using the next timeline
|
||||
return this.advance(delta);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Try move the index backwards, or into the neighbouring timeline
|
||||
*
|
||||
* @param {number} delta number of events to retreat by
|
||||
* @return {number} number of events successfully retreated by
|
||||
*/
|
||||
TimelineIndex.prototype.retreat = function(delta) {
|
||||
return this.advance(delta * -1) * -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* The TimelineWindow class.
|
||||
*/
|
||||
module.exports.TimelineWindow = TimelineWindow;
|
||||
|
||||
/**
|
||||
* The TimelineIndex class. exported here for unit testing.
|
||||
*/
|
||||
module.exports.TimelineIndex = TimelineIndex;
|
||||
+341
-3
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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.
|
||||
@@ -152,19 +167,22 @@ module.exports.findElement = function(array, fn, reverse) {
|
||||
*/
|
||||
module.exports.removeElement = function(array, fn, reverse) {
|
||||
var i;
|
||||
var removed;
|
||||
if (reverse) {
|
||||
for (i = array.length - 1; i >= 0; i--) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return true;
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return true;
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +204,8 @@ module.exports.isFunction = function(value) {
|
||||
* @return {boolean} True if it is an array.
|
||||
*/
|
||||
module.exports.isArray = function(value) {
|
||||
return Boolean(value && value.constructor === Array);
|
||||
return Array.isArray ? Array.isArray(value) :
|
||||
Boolean(value && value.constructor === Array);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -228,6 +247,325 @@ module.exports.deepCopy = function(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two objects for equality. The objects MUST NOT have circular references.
|
||||
*
|
||||
* @param {Object} x The first object to compare.
|
||||
* @param {Object} y The second object to compare.
|
||||
*
|
||||
* @return {boolean} true if the two objects are equal
|
||||
*/
|
||||
var deepCompare = module.exports.deepCompare = function(x, y) {
|
||||
// Inspired by
|
||||
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
|
||||
|
||||
// Compare primitives and functions.
|
||||
// Also check if both arguments link to the same object.
|
||||
if (x === y) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof x !== typeof y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// special-case NaN (since NaN !== NaN)
|
||||
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// special-case null (since typeof null == 'object', but null.constructor
|
||||
// throws)
|
||||
if (x === null || y === null) {
|
||||
return x === y;
|
||||
}
|
||||
|
||||
// everything else is either an unequal primitive, or an object
|
||||
if (!(x instanceof Object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check they are the same type of object
|
||||
if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// special-casing for some special types of object
|
||||
if (x instanceof RegExp || x instanceof Date) {
|
||||
return x.toString() === y.toString();
|
||||
}
|
||||
|
||||
// the object algorithm works for Array, but it's sub-optimal.
|
||||
if (x instanceof Array) {
|
||||
if (x.length !== y.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
if (!deepCompare(x[i], y[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// disable jshint "The body of a for in should be wrapped in an if
|
||||
// statement"
|
||||
/* jshint -W089 */
|
||||
|
||||
// check that all of y's direct keys are in x
|
||||
var p;
|
||||
for (p in y) {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// finally, compare each of x's keys with y
|
||||
for (p in y) {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepCompare(x[p], y[p])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* jshint +W089 */
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy properties from one object to another.
|
||||
*
|
||||
* All enumerable properties, included inherited ones, are copied.
|
||||
*
|
||||
* This is approximately equivalent to ES6's Object.assign, except
|
||||
* that the latter doesn't copy inherited properties.
|
||||
*
|
||||
* @param {Object} target The object that will receive new properties
|
||||
* @param {...Object} source Objects from which to copy properties
|
||||
*
|
||||
* @return {Object} target
|
||||
*/
|
||||
module.exports.extend = function() {
|
||||
var target = arguments[0] || {};
|
||||
// disable jshint "The body of a for in should be wrapped in an if
|
||||
// statement"
|
||||
/* jshint -W089 */
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var source = arguments[i];
|
||||
for (var propName in source) {
|
||||
target[propName] = source[propName];
|
||||
}
|
||||
}
|
||||
/* jshint +W089 */
|
||||
return target;
|
||||
};
|
||||
|
||||
/**
|
||||
* Run polyfills to add Array.map and Array.filter if they are missing.
|
||||
*/
|
||||
module.exports.runPolyfills = function() {
|
||||
// Array.prototype.filter
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
|
||||
if (!Array.prototype.filter) {
|
||||
Array.prototype.filter = function(fun/*, thisArg*/) {
|
||||
|
||||
if (this === void 0 || this === null) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var t = Object(this);
|
||||
var len = t.length >>> 0;
|
||||
if (typeof fun !== 'function') {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var res = [];
|
||||
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in t) {
|
||||
var val = t[i];
|
||||
|
||||
// NOTE: Technically this should Object.defineProperty at
|
||||
// the next index, as push can be affected by
|
||||
// properties on Object.prototype and Array.prototype.
|
||||
// But that method's new, and collisions should be
|
||||
// rare, so use the more-compatible alternative.
|
||||
if (fun.call(thisArg, val, i, t)) {
|
||||
res.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
// Array.prototype.map
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.19
|
||||
// Reference: http://es5.github.io/#x15.4.4.19
|
||||
if (!Array.prototype.map) {
|
||||
|
||||
Array.prototype.map = function(callback, thisArg) {
|
||||
|
||||
var T, A, k;
|
||||
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the |this|
|
||||
// value as the argument.
|
||||
var O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal
|
||||
// method of O with the argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
var len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let A be a new array created as if by the expression new Array(len)
|
||||
// where Array is the standard built-in constructor with that name and
|
||||
// len is the value of len.
|
||||
A = new Array(len);
|
||||
|
||||
// 7. Let k be 0
|
||||
k = 0;
|
||||
|
||||
// 8. Repeat, while k < len
|
||||
while (k < len) {
|
||||
|
||||
var kValue, mappedValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
|
||||
// i. Let kValue be the result of calling the Get internal
|
||||
// method of O with argument Pk.
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Let mappedValue be the result of calling the Call internal
|
||||
// method of callback with T as the this value and argument
|
||||
// list containing kValue, k, and O.
|
||||
mappedValue = callback.call(T, kValue, k, O);
|
||||
|
||||
// iii. Call the DefineOwnProperty internal method of A with arguments
|
||||
// Pk, Property Descriptor
|
||||
// { Value: mappedValue,
|
||||
// Writable: true,
|
||||
// Enumerable: true,
|
||||
// Configurable: true },
|
||||
// and false.
|
||||
|
||||
// In browsers that support Object.defineProperty, use the following:
|
||||
// Object.defineProperty(A, k, {
|
||||
// value: mappedValue,
|
||||
// writable: true,
|
||||
// enumerable: true,
|
||||
// configurable: true
|
||||
// });
|
||||
|
||||
// For best browser support, use the following:
|
||||
A[k] = mappedValue;
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
|
||||
// 9. return A
|
||||
return A;
|
||||
};
|
||||
}
|
||||
|
||||
// Array.prototype.forEach
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.18
|
||||
// Reference: http://es5.github.io/#x15.4.4.18
|
||||
if (!Array.prototype.forEach) {
|
||||
|
||||
Array.prototype.forEach = function(callback, thisArg) {
|
||||
|
||||
var T, k;
|
||||
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the |this| value as the
|
||||
// argument.
|
||||
var O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal method of O with the
|
||||
// argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
var len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let k be 0
|
||||
k = 0;
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
|
||||
var kValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with
|
||||
// argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
|
||||
// i. Let kValue be the result of calling the Get internal method of O with
|
||||
// argument Pk
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Call the Call internal method of callback with T as the this value and
|
||||
// argument list containing kValue, k, and O.
|
||||
callback.call(T, kValue, k, O);
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
// 8. return undefined
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherit the prototype methods from one constructor into another. This is a
|
||||
* port of the Node.js implementation with an Object.create polyfill.
|
||||
|
||||
+380
-34
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
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 createNewMatrixCall} for the public API.
|
||||
@@ -44,6 +59,14 @@ function MatrixCall(opts) {
|
||||
// possible
|
||||
this.candidateSendQueue = [];
|
||||
this.candidateSendTries = 0;
|
||||
|
||||
// Lookup from opaque queue ID to a promise for media element operations that
|
||||
// need to be serialised into a given queue. Store this per-MatrixCall on the
|
||||
// assumption that multiple matrix calls will never compete for control of the
|
||||
// same DOM elements.
|
||||
this.mediaPromises = Object.create(null);
|
||||
|
||||
this.screenSharingStream = null;
|
||||
}
|
||||
/** The length of time a call can be ringing for. */
|
||||
MatrixCall.CALL_TIMEOUT_MS = 60000;
|
||||
@@ -64,6 +87,7 @@ utils.inherits(MatrixCall, EventEmitter);
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
MatrixCall.prototype.placeVoiceCall = function() {
|
||||
debuglog("placeVoiceCall");
|
||||
checkForErrorListener(this);
|
||||
_placeCallWithConstraints(this, _getUserMediaVideoContraints('voice'));
|
||||
this.type = 'voice';
|
||||
@@ -78,6 +102,7 @@ MatrixCall.prototype.placeVoiceCall = function() {
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoElement) {
|
||||
debuglog("placeVideoCall");
|
||||
checkForErrorListener(this);
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
@@ -86,6 +111,125 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Place a screen-sharing call to this room. This includes audio.
|
||||
* <b>This method is EXPERIMENTAL and subject to change without warning. It
|
||||
* only works in Google Chrome.</b>
|
||||
* @param {Element} remoteVideoElement a <code><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></code> DOM element
|
||||
* to render the local camera preview.
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
MatrixCall.prototype.placeScreenSharingCall =
|
||||
function(remoteVideoElement, localVideoElement)
|
||||
{
|
||||
debuglog("placeScreenSharingCall");
|
||||
checkForErrorListener(this);
|
||||
var screenConstraints = _getChromeScreenSharingConstraints(this);
|
||||
if (!screenConstraints) {
|
||||
return;
|
||||
}
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
var self = this;
|
||||
this.webRtc.getUserMedia(screenConstraints, function(stream) {
|
||||
self.screenSharingStream = stream;
|
||||
debuglog("Got screen stream, requesting audio stream...");
|
||||
var audioConstraints = _getUserMediaVideoContraints('voice');
|
||||
_placeCallWithConstraints(self, audioConstraints);
|
||||
}, function(err) {
|
||||
self.emit("error",
|
||||
callError(
|
||||
MatrixCall.ERR_NO_USER_MEDIA,
|
||||
"Failed to get screen-sharing stream: " + err
|
||||
)
|
||||
);
|
||||
});
|
||||
this.type = 'video';
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Play the given HTMLMediaElement, serialising the operation into a chain
|
||||
* of promises to avoid racing access to the element
|
||||
* @param {Element} HTMLMediaElement element to play
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.playElement = function(element, queueId) {
|
||||
console.log("queuing play on " + queueId + " and element " + element);
|
||||
// XXX: FIXME: Does this leak elements, given the old promises
|
||||
// may hang around and retain a reference to them?
|
||||
if (this.mediaPromises[queueId]) {
|
||||
// XXX: these promises can fail (e.g. by <video/> being unmounted whilst
|
||||
// pending receiving media to play - e.g. whilst switching between
|
||||
// rooms before answering an inbound call), and throw unhandled exceptions.
|
||||
// However, we should soldier on as best we can even if they fail, given
|
||||
// these failures may be non-fatal (as in the case of unmounts)
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
return element.play();
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
return element.play();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.mediaPromises[queueId] = element.play();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Pause the given HTMLMediaElement, serialising the operation into a chain
|
||||
* of promises to avoid racing access to the element
|
||||
* @param {Element} HTMLMediaElement element to pause
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.pauseElement = function(element, queueId) {
|
||||
console.log("queuing pause on " + queueId + " and element " + element);
|
||||
if (this.mediaPromises[queueId]) {
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
return element.pause();
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
return element.pause();
|
||||
});
|
||||
}
|
||||
else {
|
||||
// pause doesn't actually return a promise, but do this for symmetry
|
||||
// and just in case it does in future.
|
||||
this.mediaPromises[queueId] = element.pause();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign the given HTMLMediaElement by setting the .src attribute on it,
|
||||
* serialising the operation into a chain of promises to avoid racing access
|
||||
* to the element
|
||||
* @param {Element} HTMLMediaElement element to pause
|
||||
* @param {string} src the src attribute value to assign to the element
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.assignElement = function(element, src, queueId) {
|
||||
console.log("queuing assign on " + queueId + " element " + element + " for " + src);
|
||||
if (this.mediaPromises[queueId]) {
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
element.src = src;
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
element.src = src;
|
||||
});
|
||||
}
|
||||
else {
|
||||
element.src = src;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the local <code><video></code> DOM element.
|
||||
* @return {Element} The dom element
|
||||
@@ -95,13 +239,23 @@ MatrixCall.prototype.getLocalVideoElement = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><video></code> DOM element.
|
||||
* Retrieve the remote <code><video></code> DOM element
|
||||
* used for playing back video capable streams.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
MatrixCall.prototype.getRemoteVideoElement = function() {
|
||||
return this.remoteVideoElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><audio></code> DOM element
|
||||
* used for playing back audio only streams.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
MatrixCall.prototype.getRemoteAudioElement = function() {
|
||||
return this.remoteAudioElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the local <code><video></code> DOM element. If this call is active,
|
||||
* video will be rendered to it immediately.
|
||||
@@ -112,13 +266,15 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
|
||||
|
||||
if (element && this.localAVStream && this.type === 'video') {
|
||||
element.autoplay = true;
|
||||
element.src = this.URL.createObjectURL(this.localAVStream);
|
||||
this.assignElement(element,
|
||||
this.URL.createObjectURL(this.localAVStream),
|
||||
"localVideo");
|
||||
element.muted = true;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
var vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -126,7 +282,7 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
|
||||
|
||||
/**
|
||||
* Set the remote <code><video></code> DOM element. If this call is active,
|
||||
* video will be rendered to it immediately.
|
||||
* the first received video-capable stream will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
@@ -134,6 +290,18 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the remote <code><audio></code> DOM element. If this call is active,
|
||||
* the first received audio-only stream will be rendered to it immediately.
|
||||
* The audio will *not* be rendered from the remoteVideoElement.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
MatrixCall.prototype.setRemoteAudioElement = function(element) {
|
||||
this.remoteVideoElement.muted = true;
|
||||
this.remoteAudioElement = element;
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure this call from an invite event. Used by MatrixClient.
|
||||
* @protected
|
||||
@@ -170,6 +338,7 @@ MatrixCall.prototype._initWithInvite = function(event) {
|
||||
if (event.getAge()) {
|
||||
setTimeout(function() {
|
||||
if (self.state == 'ringing') {
|
||||
debuglog("Call invite has expired. Hanging up.");
|
||||
self.hangupParty = 'remote'; // effectively
|
||||
setState(self, 'ended');
|
||||
stopAllMedia(self);
|
||||
@@ -238,6 +407,7 @@ MatrixCall.prototype._replacedBy = function(newCall) {
|
||||
}
|
||||
newCall.localVideoElement = this.localVideoElement;
|
||||
newCall.remoteVideoElement = this.remoteVideoElement;
|
||||
newCall.remoteAudioElement = this.remoteAudioElement;
|
||||
this.successor = newCall;
|
||||
this.emit("replaced", newCall);
|
||||
this.hangup(true);
|
||||
@@ -259,6 +429,60 @@ MatrixCall.prototype.hangup = function(reason, suppressEvent) {
|
||||
sendEvent(this, 'm.call.hangup', content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the local video preview should be muted or not.
|
||||
* @param {boolean} muted True to mute the local video.
|
||||
*/
|
||||
MatrixCall.prototype.setLocalVideoMuted = function(muted) {
|
||||
if (!this.localAVStream) {
|
||||
return;
|
||||
}
|
||||
setTracksEnabled(this.localAVStream.getVideoTracks(), !muted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if local video is muted.
|
||||
*
|
||||
* If there are multiple video tracks, <i>all</i> of the tracks need to be muted
|
||||
* for this to return true. This means if there are no video tracks, this will
|
||||
* return true.
|
||||
* @return {Boolean} True if the local preview video is muted, else false
|
||||
* (including if the call is not set up yet).
|
||||
*/
|
||||
MatrixCall.prototype.isLocalVideoMuted = function() {
|
||||
if (!this.localAVStream) {
|
||||
return false;
|
||||
}
|
||||
return !isTracksEnabled(this.localAVStream.getVideoTracks());
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the microphone should be muted or not.
|
||||
* @param {boolean} muted True to mute the mic.
|
||||
*/
|
||||
MatrixCall.prototype.setMicrophoneMuted = function(muted) {
|
||||
if (!this.localAVStream) {
|
||||
return;
|
||||
}
|
||||
setTracksEnabled(this.localAVStream.getAudioTracks(), !muted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the microphone is muted.
|
||||
*
|
||||
* If there are multiple audio tracks, <i>all</i> of the tracks need to be muted
|
||||
* for this to return true. This means if there are no audio tracks, this will
|
||||
* return true.
|
||||
* @return {Boolean} True if the mic is muted, else false (including if the call
|
||||
* is not set up yet).
|
||||
*/
|
||||
MatrixCall.prototype.isMicrophoneMuted = function() {
|
||||
if (!this.localAVStream) {
|
||||
return false;
|
||||
}
|
||||
return !isTracksEnabled(this.localAVStream.getAudioTracks());
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal
|
||||
* @private
|
||||
@@ -272,28 +496,43 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
if (this.state == 'ended') {
|
||||
return;
|
||||
}
|
||||
debuglog("_gotUserMediaForInvite -> " + this.type);
|
||||
var self = this;
|
||||
var videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type == 'video') {
|
||||
videoEl.autoplay = true;
|
||||
videoEl.src = this.URL.createObjectURL(stream);
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video element");
|
||||
this.assignElement(videoEl,
|
||||
this.URL.createObjectURL(this.screenSharingStream),
|
||||
"localVideo");
|
||||
}
|
||||
else {
|
||||
this.assignElement(videoEl,
|
||||
this.URL.createObjectURL(stream),
|
||||
"localVideo");
|
||||
}
|
||||
videoEl.muted = true;
|
||||
setTimeout(function() {
|
||||
var vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.localAVStream = stream;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
this.peerConn.addStream(stream);
|
||||
if (this.screenSharingStream) {
|
||||
console.log("Adding screen-sharing stream to peer connection");
|
||||
this.peerConn.addStream(this.screenSharingStream);
|
||||
// let's use this for the local preview...
|
||||
this.localAVStream = this.screenSharingStream;
|
||||
}
|
||||
this.peerConn.createOffer(
|
||||
hookCallback(self, self._gotLocalOffer),
|
||||
hookCallback(self, self._getLocalOfferFailed)
|
||||
@@ -315,21 +554,20 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
|
||||
|
||||
if (localVidEl && self.type == 'video') {
|
||||
localVidEl.autoplay = true;
|
||||
localVidEl.src = self.URL.createObjectURL(stream);
|
||||
this.assignElement(localVidEl,
|
||||
this.URL.createObjectURL(stream),
|
||||
"localVideo");
|
||||
localVidEl.muted = true;
|
||||
setTimeout(function() {
|
||||
var vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
self.localAVStream = stream;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
self.peerConn.addStream(stream);
|
||||
|
||||
var constraints = {
|
||||
@@ -481,8 +719,9 @@ MatrixCall.prototype._getLocalOfferFailed = function(error) {
|
||||
/**
|
||||
* Internal
|
||||
* @private
|
||||
* @param {Object} error
|
||||
*/
|
||||
MatrixCall.prototype._getUserMediaFailed = function() {
|
||||
MatrixCall.prototype._getUserMediaFailed = function(error) {
|
||||
this.emit(
|
||||
"error",
|
||||
callError(
|
||||
@@ -550,31 +789,44 @@ MatrixCall.prototype._onSetRemoteDescriptionError = function(e) {
|
||||
* @param {Object} event
|
||||
*/
|
||||
MatrixCall.prototype._onAddStream = function(event) {
|
||||
debuglog("Stream added" + event);
|
||||
debuglog("Stream id " + event.stream.id + " added");
|
||||
|
||||
var s = event.stream;
|
||||
|
||||
this.remoteAVStream = s;
|
||||
|
||||
if (this.direction == 'inbound') {
|
||||
if (s.getVideoTracks().length > 0) {
|
||||
this.type = 'video';
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
}
|
||||
if (s.getVideoTracks().length > 0) {
|
||||
this.type = 'video';
|
||||
this.remoteAVStream = s;
|
||||
this.remoteAStream = s;
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
this.remoteAStream = s;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
forAllTracksOnStream(s, function(t) {
|
||||
debuglog("Track id " + t.id + " added");
|
||||
// not currently implemented in chrome
|
||||
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
|
||||
});
|
||||
|
||||
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
|
||||
if (event.stream.oninactive !== undefined) {
|
||||
event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded);
|
||||
}
|
||||
else {
|
||||
// onended is deprecated from Chrome 54
|
||||
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
|
||||
}
|
||||
|
||||
// not currently implemented in chrome
|
||||
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
|
||||
|
||||
_tryPlayRemoteStream(this);
|
||||
if (this.type === 'video') {
|
||||
_tryPlayRemoteStream(this);
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
}
|
||||
else {
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -631,6 +883,21 @@ MatrixCall.prototype._onAnsweredElsewhere = function(msg) {
|
||||
terminate(this, "remote", "answered_elsewhere", true);
|
||||
};
|
||||
|
||||
var setTracksEnabled = function(tracks, enabled) {
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
tracks[i].enabled = enabled;
|
||||
}
|
||||
};
|
||||
|
||||
var isTracksEnabled = function(tracks) {
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i].enabled) {
|
||||
return true; // at least one track is enabled
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var setState = function(self, state) {
|
||||
var oldState = self.state;
|
||||
self.state = state;
|
||||
@@ -662,15 +929,21 @@ var sendCandidate = function(self, content) {
|
||||
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
|
||||
if (self.getRemoteVideoElement()) {
|
||||
if (self.getRemoteVideoElement().pause) {
|
||||
self.getRemoteVideoElement().pause();
|
||||
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
|
||||
}
|
||||
self.getRemoteVideoElement().src = "";
|
||||
self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo");
|
||||
}
|
||||
if (self.getRemoteAudioElement()) {
|
||||
if (self.getRemoteAudioElement().pause) {
|
||||
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
|
||||
}
|
||||
self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio");
|
||||
}
|
||||
if (self.getLocalVideoElement()) {
|
||||
if (self.getLocalVideoElement().pause) {
|
||||
self.getLocalVideoElement().pause();
|
||||
self.pauseElement(self.getLocalVideoElement(), "localVideo");
|
||||
}
|
||||
self.getLocalVideoElement().src = "";
|
||||
self.assignElement(self.getLocalVideoElement(), "", "localVideo");
|
||||
}
|
||||
self.hangupParty = hangupParty;
|
||||
self.hangupReason = hangupReason;
|
||||
@@ -685,6 +958,7 @@ var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
|
||||
};
|
||||
|
||||
var stopAllMedia = function(self) {
|
||||
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
|
||||
if (self.localAVStream) {
|
||||
forAllTracksOnStream(self.localAVStream, function(t) {
|
||||
if (t.stop) {
|
||||
@@ -697,6 +971,16 @@ var stopAllMedia = function(self) {
|
||||
self.localAVStream.stop();
|
||||
}
|
||||
}
|
||||
if (self.screenSharingStream) {
|
||||
forAllTracksOnStream(self.screenSharingStream, function(t) {
|
||||
if (t.stop) {
|
||||
t.stop();
|
||||
}
|
||||
});
|
||||
if (self.screenSharingStream.stop) {
|
||||
self.screenSharingStream.stop();
|
||||
}
|
||||
}
|
||||
if (self.remoteAVStream) {
|
||||
forAllTracksOnStream(self.remoteAVStream, function(t) {
|
||||
if (t.stop) {
|
||||
@@ -704,17 +988,46 @@ var stopAllMedia = function(self) {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (self.remoteAStream) {
|
||||
forAllTracksOnStream(self.remoteAStream, function(t) {
|
||||
if (t.stop) {
|
||||
t.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var _tryPlayRemoteStream = function(self) {
|
||||
if (self.getRemoteVideoElement() && self.remoteAVStream) {
|
||||
var player = self.getRemoteVideoElement();
|
||||
player.autoplay = true;
|
||||
player.src = self.URL.createObjectURL(self.remoteAVStream);
|
||||
self.assignElement(player,
|
||||
self.URL.createObjectURL(self.remoteAVStream),
|
||||
"remoteVideo");
|
||||
setTimeout(function() {
|
||||
var vel = self.getRemoteVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "remoteVideo");
|
||||
}
|
||||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||||
if (self.webRtc.isOpenWebRTC()) {
|
||||
setState(self, 'connected');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
var _tryPlayRemoteAudioStream = function(self) {
|
||||
if (self.getRemoteAudioElement() && self.remoteAStream) {
|
||||
var player = self.getRemoteAudioElement();
|
||||
player.autoplay = true;
|
||||
self.assignElement(player,
|
||||
self.URL.createObjectURL(self.remoteAStream),
|
||||
"remoteAudio");
|
||||
setTimeout(function() {
|
||||
var ael = self.getRemoteAudioElement();
|
||||
if (ael.play) {
|
||||
self.playElement(ael, "remoteAudio");
|
||||
}
|
||||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||||
if (self.webRtc.isOpenWebRTC()) {
|
||||
@@ -822,6 +1135,38 @@ var _createPeerConnection = function(self) {
|
||||
return pc;
|
||||
};
|
||||
|
||||
var _getChromeScreenSharingConstraints = function(call) {
|
||||
var screen = global.screen;
|
||||
if (!screen) {
|
||||
call.emit("error", callError(
|
||||
MatrixCall.ERR_NO_USER_MEDIA,
|
||||
"Couldn't determine screen sharing constaints."
|
||||
));
|
||||
return;
|
||||
}
|
||||
// it won't work at all if you're not on HTTPS so whine whine whine
|
||||
if (!global.window || global.window.location.protocol !== "https:") {
|
||||
call.emit("error", callError(
|
||||
MatrixCall.ERR_NO_USER_MEDIA,
|
||||
"You need to be using HTTPS to place a screen-sharing call."
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
chromeMediaSourceId: "" + Date.now(),
|
||||
maxWidth: screen.width,
|
||||
maxHeight: screen.height,
|
||||
minFrameRate: 1,
|
||||
maxFrameRate: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var _getUserMediaVideoContraints = function(callType) {
|
||||
switch (callType) {
|
||||
case 'voice':
|
||||
@@ -866,6 +1211,7 @@ var forAllTracksOnStream = function(s, f) {
|
||||
/** The MatrixCall class. */
|
||||
module.exports.MatrixCall = MatrixCall;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new Matrix call for the browser.
|
||||
* @param {MatrixClient} client The client instance to use.
|
||||
|
||||
+35
-10
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.2.2",
|
||||
"version": "0.7.2",
|
||||
"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 --forceexit --captureExceptions",
|
||||
"check": "jasmine-node spec --verbose --junitreport --forceexit --captureExceptions",
|
||||
"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 --max_line_length 90 -r spec/ -r lib/",
|
||||
"release": "npm run build && mkdir 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"
|
||||
"build": "jshint -c .jshint lib/ && rimraf dist && mkdir dist && browserify --exclude olm browser-index.js -o dist/browser-matrix.js --ignore-missing && uglifyjs -c -m -o dist/browser-matrix.min.js dist/browser-matrix.js",
|
||||
"dist": "npm run build",
|
||||
"watch": "watchify --exclude olm 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/",
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
@@ -20,17 +21,41 @@
|
||||
],
|
||||
"browser": "browser-index.js",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache 2.0",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"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"
|
||||
],
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"browserify": "^10.2.3",
|
||||
"q": "^1.4.1",
|
||||
"request": "^2.53.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"watchify": "^3.2.1",
|
||||
"istanbul": "^0.3.13",
|
||||
"jasmine-node": "^1.14.5",
|
||||
"jshint": "^2.8.0"
|
||||
"jsdoc": "^3.4.0",
|
||||
"jshint": "^2.8.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"uglifyjs": "^2.4.10",
|
||||
"watchify": "^3.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk. Performs the steps documented
|
||||
# in RELEASING.md
|
||||
#
|
||||
# Requires:
|
||||
# github-changelog-generator; to install, do
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# jq; install from your distibution's package manager (https://stedolan.github.io/jq/)
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
$USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
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
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
changelog_file="CHANGELOG.md"
|
||||
while getopts hc:xz f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
c)
|
||||
changelog_file="$OPTARG"
|
||||
;;
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $USAGE" >&2
|
||||
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
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
|
||||
# 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
|
||||
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
# 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
|
||||
echo "Generating changelog"
|
||||
update_changelog -f "$changelog_file" "$release"
|
||||
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
||||
|
||||
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
|
||||
echo "Committing updated changelog"
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json and build the dist
|
||||
echo "npm version"
|
||||
# npm 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.
|
||||
npm version --no-git-tag-version "$release"
|
||||
git commit package.json -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 npm 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"
|
||||
npm install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" npm run 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
|
||||
|
||||
# push the release branch (github can't release from
|
||||
# a branch it doesn't have)
|
||||
git push origin "$rel_branch"
|
||||
|
||||
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
|
||||
git push origin "$tag"
|
||||
|
||||
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}"
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
npm run gendoc
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -a ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
fi
|
||||
|
||||
# merge release branch to master
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
|
||||
# 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
|
||||
git merge master
|
||||
git push origin develop
|
||||
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,54 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A mock implementation of the webstorage api
|
||||
* @constructor
|
||||
*/
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
var keys = [];
|
||||
for (var k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) { continue; }
|
||||
keys.push(k);
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
}
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = MockStorageApi;
|
||||
@@ -1,244 +1,787 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var q = require("q");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
var utils = require("../../lib/utils");
|
||||
var test_utils = require("../test-utils");
|
||||
|
||||
var aliHttpBackend;
|
||||
var bobHttpBackend;
|
||||
var aliClient;
|
||||
var roomId = "!room:localhost";
|
||||
var aliUserId = "@ali:localhost";
|
||||
var aliDeviceId = "zxcvb";
|
||||
var aliAccessToken = "aseukfgwef";
|
||||
var bobClient;
|
||||
var bobUserId = "@bob:localhost";
|
||||
var bobDeviceId = "bvcxz";
|
||||
var bobAccessToken = "fewgfkuesa";
|
||||
var bobOneTimeKeys;
|
||||
var aliDeviceKeys;
|
||||
var bobDeviceKeys;
|
||||
var bobDeviceCurve25519Key;
|
||||
var bobDeviceEd25519Key;
|
||||
var aliStorage;
|
||||
var bobStorage;
|
||||
var aliMessages;
|
||||
var bobMessages;
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that the client will upload device keys and a number of
|
||||
* one-time keys; then flush the http requests.
|
||||
*
|
||||
* @param {string} deviceId expected device id in upload request
|
||||
* @param {object} httpBackend
|
||||
*
|
||||
* @return {promise} completes once the http requests have completed, returning combined
|
||||
* {one_time_keys: {}, device_keys: {}}
|
||||
*/
|
||||
function expectKeyUpload(deviceId, httpBackend) {
|
||||
var uploadPath = "/keys/upload/" + deviceId;
|
||||
var keys = {};
|
||||
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toBeDefined();
|
||||
expect(content.device_keys).toBeDefined();
|
||||
keys.device_keys = content.device_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
});
|
||||
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.device_keys).not.toBeDefined();
|
||||
expect(content.one_time_keys).toBeDefined();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
var count = 0;
|
||||
for (var key in content.one_time_keys) {
|
||||
if (content.one_time_keys.hasOwnProperty(key)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
expect(count).toEqual(5);
|
||||
keys.one_time_keys = content.one_time_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: count}};
|
||||
});
|
||||
|
||||
return httpBackend.flush(uploadPath, 2).then(function() {
|
||||
return keys;
|
||||
});
|
||||
}
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will upload device keys and a number of one-time keys;
|
||||
* then flush the http requests.
|
||||
*
|
||||
* <p>Updates <tt>aliDeviceKeys</tt>
|
||||
*
|
||||
* @return {promise} completes once the http requests have completed.
|
||||
*/
|
||||
function expectAliKeyUpload() {
|
||||
return expectKeyUpload(aliDeviceId, aliHttpBackend).then(function(content) {
|
||||
aliDeviceKeys = content.device_keys;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that bob will upload device keys and a number of one-time keys;
|
||||
* then flush the http requests.
|
||||
*
|
||||
* <p>Updates <tt>bobDeviceKeys</tt>, <tt>bobOneTimeKeys</tt>,
|
||||
* <tt>bobDeviceCurve25519Key</tt>, <tt>bobDeviceEd25519Key</tt>
|
||||
*
|
||||
* @return {promise} completes once the http requests have completed.
|
||||
*/
|
||||
function expectBobKeyUpload() {
|
||||
return expectKeyUpload(bobDeviceId, bobHttpBackend).then(function(content) {
|
||||
bobDeviceKeys = content.device_keys;
|
||||
bobOneTimeKeys = content.one_time_keys;
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
|
||||
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
|
||||
});
|
||||
}
|
||||
|
||||
function bobUploadsKeys() {
|
||||
bobClient.uploadKeys(5).catch(test_utils.failTest);
|
||||
return expectBobKeyUpload();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will query bobs keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliQueryKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
return aliHttpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that bob will query alis keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} which resolves once the http request has completed.
|
||||
*/
|
||||
function expectBobQueryKeys() {
|
||||
// can't query keys before ali has uploaded them
|
||||
expect(aliDeviceKeys).toBeDefined();
|
||||
|
||||
var aliKeys = {};
|
||||
aliKeys[aliDeviceId] = aliDeviceKeys;
|
||||
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
return bobHttpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliClaimKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
|
||||
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
var claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
|
||||
return {one_time_keys: result};
|
||||
});
|
||||
|
||||
return aliHttpBackend.flush("/keys/claim", 1);
|
||||
}
|
||||
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobDeviceEd25519Key).toBeDefined();
|
||||
|
||||
var p1 = aliClient.downloadKeys([bobUserId]).then(function() {
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
|
||||
id: "bvcxz",
|
||||
key: bobDeviceEd25519Key,
|
||||
verified: false,
|
||||
blocked: false,
|
||||
display_name: null,
|
||||
}]);
|
||||
});
|
||||
var p2 = expectAliQueryKeys();
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
return q.all([p1, p2]).then(function() {
|
||||
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
});
|
||||
}
|
||||
|
||||
function aliEnablesEncryption() {
|
||||
return aliClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
function bobEnablesEncryption() {
|
||||
return bobClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function() {
|
||||
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsFirstMessage() {
|
||||
return q.all([
|
||||
sendMessage(aliClient),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest)
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsMessage() {
|
||||
return q.all([
|
||||
sendMessage(aliClient),
|
||||
expectAliSendMessageRequest()
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
function bobSendsReplyMessage() {
|
||||
return q.all([
|
||||
sendMessage(bobClient),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest)
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectAliSendMessageRequest() {
|
||||
return expectSendMessageRequest(aliHttpBackend).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
|
||||
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeDefined();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectBobSendMessageRequest() {
|
||||
return expectSendMessageRequest(bobHttpBackend).then(function(content) {
|
||||
bobMessages.push(content);
|
||||
var aliKeyId = "curve25519:" + aliDeviceId;
|
||||
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
|
||||
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
var ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeDefined();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(client) {
|
||||
return client.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"}
|
||||
);
|
||||
}
|
||||
|
||||
function expectSendMessageRequest(httpBackend) {
|
||||
var path = "/send/m.room.encrypted/";
|
||||
var sent;
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
sent = content;
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
return httpBackend.flush(path, 1).then(function() {
|
||||
return sent;
|
||||
});
|
||||
}
|
||||
|
||||
function aliRecvMessage() {
|
||||
var message = bobMessages.shift();
|
||||
return recvMessage(aliHttpBackend, aliClient, bobUserId, message);
|
||||
}
|
||||
|
||||
function bobRecvMessage() {
|
||||
var message = aliMessages.shift();
|
||||
return recvMessage(bobHttpBackend, bobClient, aliUserId, message);
|
||||
}
|
||||
|
||||
function recvMessage(httpBackend, client, sender, message) {
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
var deferred = q.defer();
|
||||
var onEvent = function(event) {
|
||||
console.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World"
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
client.on("event", onEvent);
|
||||
|
||||
httpBackend.flush();
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
||||
function aliStartClient() {
|
||||
expectAliKeyUpload().catch(test_utils.failTest);
|
||||
|
||||
// ali will try to query her own keys on start
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[aliUserId] = {};
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
startClient(aliHttpBackend, aliClient);
|
||||
return aliHttpBackend.flush().then(function() {
|
||||
console.log("Ali client started");
|
||||
});
|
||||
}
|
||||
|
||||
function bobStartClient() {
|
||||
expectBobKeyUpload().catch(test_utils.failTest);
|
||||
|
||||
// bob will try to query his own keys on start
|
||||
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
startClient(bobHttpBackend, bobClient);
|
||||
return bobHttpBackend.flush().then(function() {
|
||||
console.log("Bob client started");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set http responses for the requests which are made when a client starts, and
|
||||
* start the client.
|
||||
*
|
||||
* @param {object} httpBackend
|
||||
* @param {MatrixClient} client
|
||||
*/
|
||||
function startClient(httpBackend, client) {
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
// send a sync response including our test room.
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: { }
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
test_utils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
test_utils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
]
|
||||
},
|
||||
timeline: {
|
||||
events: []
|
||||
}
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
}
|
||||
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var httpBackend;
|
||||
var aliClient;
|
||||
var roomId = "!room:localhost";
|
||||
var aliUserId = "@ali:localhost";
|
||||
var aliDeviceId = "zxcvb";
|
||||
var aliAccessToken = "aseukfgwef";
|
||||
var bobClient;
|
||||
var bobUserId = "@bob:localhost";
|
||||
var bobDeviceId = "bvcxz";
|
||||
var bobAccessToken = "fewgfkuesa";
|
||||
var bobOneTimeKeys;
|
||||
var bobDeviceKeys;
|
||||
var bobDeviceCurve25519Key;
|
||||
var bobDeviceEd25519Key;
|
||||
var aliLocalStore;
|
||||
var aliStorage;
|
||||
var bobStorage;
|
||||
var aliMessage;
|
||||
|
||||
beforeEach(function() {
|
||||
aliLocalStore = new MockStorageApi();
|
||||
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
|
||||
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
test_utils.beforeEach(this);
|
||||
|
||||
aliStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||
aliHttpBackend = new HttpBackend();
|
||||
aliClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
baseUrl: "http://alis.server",
|
||||
userId: aliUserId,
|
||||
accessToken: aliAccessToken,
|
||||
deviceId: aliDeviceId,
|
||||
sessionStore: aliStorage
|
||||
sessionStore: aliStorage,
|
||||
request: aliHttpBackend.requestFn,
|
||||
});
|
||||
|
||||
bobStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||
bobHttpBackend = new HttpBackend();
|
||||
bobClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
baseUrl: "http://bobs.server",
|
||||
userId: bobUserId,
|
||||
accessToken: bobAccessToken,
|
||||
deviceId: bobDeviceId,
|
||||
sessionStore: bobStorage
|
||||
sessionStore: bobStorage,
|
||||
request: bobHttpBackend.requestFn,
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
bobOneTimeKeys = undefined;
|
||||
aliDeviceKeys = undefined;
|
||||
bobDeviceKeys = undefined;
|
||||
bobDeviceCurve25519Key = undefined;
|
||||
bobDeviceEd25519Key = undefined;
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
});
|
||||
|
||||
describe("Ali account setup", function() {
|
||||
it("should have device keys", function(done) {
|
||||
expect(aliClient.deviceKeys).toBeDefined();
|
||||
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
|
||||
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
|
||||
done();
|
||||
});
|
||||
it("should have a curve25519 key", function(done) {
|
||||
expect(aliClient.deviceCurve25519Key).toBeDefined();
|
||||
done();
|
||||
});
|
||||
afterEach(function() {
|
||||
aliClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
function bobUploadsKeys(done) {
|
||||
var uploadPath = "/keys/upload/bvcxz";
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).toEqual({});
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
bobDeviceKeys = content.device_keys;
|
||||
bobOneTimeKeys = content.one_time_keys;
|
||||
var count = 0;
|
||||
for (var key in content.one_time_keys) {
|
||||
if (content.one_time_keys.hasOwnProperty(key)) {
|
||||
count++;
|
||||
}
|
||||
it('Ali knows the difference between a new user and one with no devices',
|
||||
function(done) {
|
||||
aliHttpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:id': {},
|
||||
}
|
||||
expect(count).toEqual(5);
|
||||
return {one_time_key_counts: {curve25519: count}};
|
||||
});
|
||||
return {one_time_key_counts: {}};
|
||||
});
|
||||
bobClient.uploadKeys(5);
|
||||
httpBackend.flush().done(function() {
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
|
||||
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
|
||||
var p1 = aliClient.downloadKeys(['@bob:id']);
|
||||
var p2 = aliHttpBackend.flush('/keys/query', 1);
|
||||
|
||||
q.all([p1, p2]).then(function() {
|
||||
var devices = aliStorage.getEndToEndDevicesForUser('@bob:id');
|
||||
expect(utils.keys(devices).length).toEqual(0);
|
||||
|
||||
// request again: should be no more requests
|
||||
return aliClient.downloadKeys(['@bob:id']);
|
||||
}).nodeify(done);
|
||||
}
|
||||
);
|
||||
|
||||
it("Bob uploads without one-time keys and with one-time keys", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs keys", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliDownloadsKeys)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(function() {
|
||||
// tamper bob's keys!
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeDefined();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
|
||||
return q.all(aliClient.downloadKeys([bobUserId]),
|
||||
expectAliQueryKeys());
|
||||
})
|
||||
.then(function() {
|
||||
// should get an empty list
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
|
||||
})
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", function(done) {
|
||||
var eveUserId = "@eve:localhost";
|
||||
|
||||
var bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bvcxz',
|
||||
keys: {
|
||||
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
||||
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function aliDownloadsKeys(done) {
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
var result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
aliClient.downloadKeys([bobUserId]).then(function() {
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
|
||||
id: "bvcxz",
|
||||
key: bobDeviceEd25519Key
|
||||
}]);
|
||||
});
|
||||
httpBackend.flush().done(function() {
|
||||
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
|
||||
expect(devices).toEqual(bobKeys);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
it("Ali downloads Bobs keys", function(done) {
|
||||
bobUploadsKeys(function() {aliDownloadsKeys(done);});
|
||||
q.all(
|
||||
aliClient.downloadKeys([bobUserId, eveUserId]),
|
||||
aliHttpBackend.flush("/keys/query", 1)
|
||||
).then(function() {
|
||||
// should get an empty list
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
|
||||
expect(aliClient.listDeviceKeys(eveUserId)).toEqual([]);
|
||||
}).catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
function aliEnablesEncryption(done) {
|
||||
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
it("Ali gets keys with an incorrect deviceId", function(done) {
|
||||
var bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
keys: {
|
||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
|
||||
return {one_time_keys: result};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
aliClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
members: [aliUserId, bobUserId]
|
||||
}).then(function(res) {
|
||||
expect(res.missingUsers).toEqual([]);
|
||||
expect(res.missingDevices).toEqual({});
|
||||
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
httpBackend.flush();
|
||||
}
|
||||
|
||||
q.all(
|
||||
aliClient.downloadKeys([bobUserId]),
|
||||
aliHttpBackend.flush("/keys/query", 1)
|
||||
).then(function() {
|
||||
// should get an empty list
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
|
||||
}).catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali enables encryption", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(done);
|
||||
});
|
||||
});
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
function aliSendsMessage(done) {
|
||||
var txnId = "a.transaction.id";
|
||||
var path = "/send/m.room.encrypted/" + txnId;
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
aliMessage = content;
|
||||
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
|
||||
return {};
|
||||
});
|
||||
aliClient.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
|
||||
);
|
||||
httpBackend.flush().done(function() {done();});
|
||||
}
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(function() {
|
||||
aliSendsMessage(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.catch(test_utils.failTest).nodeify(done);
|
||||
});
|
||||
|
||||
function bobRecvMessage(done) {
|
||||
var initialSync = {
|
||||
end: "alpha",
|
||||
presence: [],
|
||||
rooms: []
|
||||
};
|
||||
var events = {
|
||||
start: "alpha",
|
||||
end: "beta",
|
||||
chunk: [utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: aliMessage
|
||||
})]
|
||||
};
|
||||
httpBackend.when("GET", "initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "events").respond(200, events);
|
||||
bobClient.on("event", function(event) {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World"
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
bobClient.startClient();
|
||||
httpBackend.flush();
|
||||
}
|
||||
|
||||
it("Bob receives a message", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(function() {
|
||||
aliSendsMessage(function() {
|
||||
bobRecvMessage(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 30000); //timeout after 30s
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(function() {
|
||||
var message = aliMessages.shift();
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
bobHttpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
var deferred = q.defer();
|
||||
var onEvent = function(event) {
|
||||
console.log(bobClient.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
bobClient.removeListener("event", onEvent);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
bobClient.on("event", onEvent);
|
||||
|
||||
bobHttpBackend.flush();
|
||||
return deferred.promise;
|
||||
})
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliDownloadsKeys)
|
||||
.then(function() {
|
||||
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||
var p1 = sendMessage(aliClient);
|
||||
var p2 = expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(function() {
|
||||
return expectSendMessageRequest(aliHttpBackend);
|
||||
}).then(function(sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
return q.all([p1, p2]);
|
||||
}).catch(test_utils.failTest).nodeify(done);
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobRecvMessage)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.then(bobEnablesEncryption)
|
||||
.then(bobSendsReplyMessage).then(function(ciphertext) {
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
}).then(aliRecvMessage)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
|
||||
it("Ali does a key query when she gets a new_device event", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(function() {
|
||||
var syncData = {
|
||||
next_batch: '2',
|
||||
to_device: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
content: {
|
||||
device_id: 'TEST_DEVICE',
|
||||
rooms: [],
|
||||
},
|
||||
sender: bobUserId,
|
||||
type: 'm.new_device',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliHttpBackend.when('GET', '/sync').respond(200, syncData);
|
||||
return aliHttpBackend.flush('/sync', 1);
|
||||
}).then(expectAliQueryKeys)
|
||||
.nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,115 +19,123 @@ describe("MatrixClient events", function() {
|
||||
accessToken: selfAccessToken
|
||||
});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
var initialSync = {
|
||||
end: "s_5_3",
|
||||
presence: [{
|
||||
event_id: "$wefiuewh:bar",
|
||||
type: "m.presence",
|
||||
content: {
|
||||
user_id: "@foo:bar",
|
||||
displayname: "Foo Bar",
|
||||
presence: "online"
|
||||
}
|
||||
}],
|
||||
rooms: [{
|
||||
room_id: "!erufh:bar",
|
||||
membership: "join",
|
||||
messages: {
|
||||
start: "s",
|
||||
end: "t",
|
||||
chunk: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar"
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: "!erufh:bar", user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar"
|
||||
}
|
||||
var SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online"
|
||||
})
|
||||
]
|
||||
}]
|
||||
};
|
||||
var eventData = {
|
||||
start: "s_5_3",
|
||||
end: "e_6_7",
|
||||
chunk: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: "!erufh:bar", content: {
|
||||
user_ids: ["@foo:bar"]
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
"!erufh:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
|
||||
})
|
||||
],
|
||||
prev_batch: "s"
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar"
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar"
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
var NEXT_SYNC_DATA = {
|
||||
next_batch: "e_6_7",
|
||||
rooms: {
|
||||
join: {
|
||||
"!erufh:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
|
||||
}),
|
||||
]
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: "!erufh:bar", content: {
|
||||
user_ids: ["@foo:bar"]
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it("should emit events from both /initialSync and /events", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var 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
|
||||
);
|
||||
|
||||
// initial sync events are unordered, so make an array of the types
|
||||
// that should be emitted and we'll just pick them off one by one,
|
||||
// so long as this is emptied we're good.
|
||||
var initialSyncEventTypes = [
|
||||
"m.presence", "m.room.member", "m.room.message", "m.room.create"
|
||||
];
|
||||
var chunkIndex = 0;
|
||||
client.on("event", function(event) {
|
||||
if (initialSyncEventTypes.length === 0) {
|
||||
if (chunkIndex + 1 >= eventData.chunk.length) {
|
||||
return;
|
||||
var found = false;
|
||||
for (var i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
// this should be /events now
|
||||
expect(eventData.chunk[chunkIndex].event_id).toEqual(
|
||||
event.getId()
|
||||
);
|
||||
chunkIndex++;
|
||||
return;
|
||||
}
|
||||
var index = initialSyncEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected event type: " + event.getType()
|
||||
expect(found).toBe(
|
||||
true, "Unexpected 'event' emitted: " + event.getType()
|
||||
);
|
||||
if (index >= 0) {
|
||||
initialSyncEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
expect(initialSyncEventTypes.length).toEqual(
|
||||
0, "Failed to see all events from /initialSync"
|
||||
);
|
||||
expect(chunkIndex + 1).toEqual(
|
||||
eventData.chunk.length, "Failed to see all events from /events"
|
||||
expect(expectedEvents.length).toEqual(
|
||||
0, "Failed to see all events from /sync calls"
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
var fired = false;
|
||||
client.on("User.presence", function(event, user) {
|
||||
fired = true;
|
||||
@@ -135,9 +143,9 @@ describe("MatrixClient events", function() {
|
||||
expect(event).toBeDefined();
|
||||
if (!user || !event) { return; }
|
||||
|
||||
expect(event.event).toEqual(initialSync.presence[0]);
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
initialSync.presence[0].content.presence
|
||||
SYNC_DATA.presence.events[0].content.presence
|
||||
);
|
||||
});
|
||||
client.startClient();
|
||||
@@ -149,8 +157,8 @@ describe("MatrixClient events", function() {
|
||||
});
|
||||
|
||||
it("should emit Room events", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
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;
|
||||
@@ -183,8 +191,8 @@ describe("MatrixClient events", function() {
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
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"
|
||||
@@ -232,8 +240,8 @@ describe("MatrixClient events", function() {
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var typingInvokeCount = 0;
|
||||
var powerLevelInvokeCount = 0;
|
||||
@@ -272,6 +280,24 @@ describe("MatrixClient events", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
|
||||
|
||||
var sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(event, member) {
|
||||
sessionLoggedOutCount++;
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times"
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,735 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
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
|
||||
});
|
||||
|
||||
var ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
});
|
||||
|
||||
var INITIAL_SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello"
|
||||
})
|
||||
],
|
||||
prev_batch: "f_1_1"
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob"
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var EVENTS = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "we",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "could",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "be",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "heroes",
|
||||
}),
|
||||
];
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
function startClient(httpBackend, client) {
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
|
||||
|
||||
client.startClient();
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
var deferred = q.defer();
|
||||
client.on("sync", function(state) {
|
||||
console.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
||||
describe("getEventTimeline support", function() {
|
||||
var httpBackend;
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
if (client) {
|
||||
client.stopClient();
|
||||
}
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function(done) {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||
.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function(done) {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||
.not.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
});
|
||||
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function(done) {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
var room;
|
||||
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_5",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[1],
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
var client, httpBackend;
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client)
|
||||
.catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1], EVENTS[0]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
state: [
|
||||
ROOM_NAME_EVENT,
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
],
|
||||
end: "end_token",
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(timelineSet, "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);
|
||||
});
|
||||
|
||||
it("should return existing timeline for known events", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[0],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.flush("/sync").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);
|
||||
});
|
||||
|
||||
it("should update timelines where they overlap a previous /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
EVENTS[3],
|
||||
],
|
||||
prev_batch: "f_1_2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [EVENTS[1]],
|
||||
event: EVENTS[2],
|
||||
events_after: [EVENTS[3]],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
});
|
||||
|
||||
it("should join timelines where they overlap a previous /context",
|
||||
function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var 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/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[2].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token2",
|
||||
events_before: [],
|
||||
event: EVENTS[2],
|
||||
events_after: [],
|
||||
end: "end_token2",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[3].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token3",
|
||||
events_before: [],
|
||||
event: EVENTS[3],
|
||||
events_after: [],
|
||||
end: "end_token3",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[1].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token4",
|
||||
events_before: [EVENTS[0]],
|
||||
event: EVENTS[1],
|
||||
events_after: [EVENTS[2], EVENTS[3]],
|
||||
end: "end_token4",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
var tl0, tl2, tl3;
|
||||
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);
|
||||
tl2 = tl;
|
||||
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");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
});
|
||||
|
||||
it("should fail gracefully if there is no event field", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var 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")
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token",
|
||||
events_before: [],
|
||||
events_after: [],
|
||||
end: "end_token",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(timelineSet, "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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline", function() {
|
||||
it("should allow you to paginate backwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
var params = req.queryParams;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual(30);
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "start_token1",
|
||||
};
|
||||
});
|
||||
|
||||
var tl;
|
||||
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");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
});
|
||||
|
||||
|
||||
it("should allow you to paginate forwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
.respond(200, function() {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function(req) {
|
||||
var params = req.queryParams;
|
||||
expect(params.dir).toEqual("f");
|
||||
expect(params.from).toEqual("end_token0");
|
||||
expect(params.limit).toEqual(20);
|
||||
}).respond(200, function() {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2]],
|
||||
end: "end_token1",
|
||||
};
|
||||
});
|
||||
|
||||
var tl;
|
||||
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");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush().catch(utils.failTest);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event timeline for sent events", function() {
|
||||
var TXN_ID = "txn1";
|
||||
var event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body",
|
||||
});
|
||||
event.unsigned = {transaction_id: TXN_ID};
|
||||
|
||||
beforeEach(function() {
|
||||
// set up handlers for both the message send, and the
|
||||
// /sync
|
||||
httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
|
||||
.respond(200, {
|
||||
event_id: event.event_id,
|
||||
});
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: {
|
||||
events: [
|
||||
event
|
||||
],
|
||||
prev_batch: "f_1_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should work when /send returns before /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var 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(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(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest);
|
||||
});
|
||||
|
||||
it("should work when /send returns after /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var 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(timelineSet, 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);
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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
|
||||
var event = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: otherUserId
|
||||
});
|
||||
var redaction = utils.mkEvent({
|
||||
type: "m.room.redaction",
|
||||
room_id: roomId,
|
||||
sender: otherUserId,
|
||||
content: {}
|
||||
});
|
||||
redaction.redacts = event.event_id;
|
||||
|
||||
var syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
redaction,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[1].isRedacted()).toBe(true);
|
||||
|
||||
var 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 httpBackend.flush();
|
||||
}).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
@@ -4,11 +4,13 @@ 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");
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend, store;
|
||||
var client, httpBackend, store, sessionStore;
|
||||
var userId = "@alice:localhost";
|
||||
var accessToken = "aseukfgwef";
|
||||
|
||||
@@ -16,12 +18,18 @@ describe("MatrixClient", function() {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MatrixInMemoryStore();
|
||||
|
||||
var mockStorage = new MockStorageApi();
|
||||
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
|
||||
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
deviceId: "aliceDevice",
|
||||
accessToken: accessToken,
|
||||
store: store
|
||||
store: store,
|
||||
sessionStore: sessionStore,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,11 +37,122 @@ describe("MatrixClient", function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
describe("uploadContent", function() {
|
||||
var buf = new Buffer('hello world');
|
||||
it("should upload the file", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload"
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(buf);
|
||||
expect(req.queryParams.filename).toEqual("hi.txt");
|
||||
expect(req.queryParams.access_token).toEqual(accessToken);
|
||||
expect(req.headers["Content-Type"]).toEqual("text/plain");
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
expect(req.opts.timeout).toBe(undefined);
|
||||
}).respond(200, "content");
|
||||
|
||||
var prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
expect(prom).toBeDefined();
|
||||
|
||||
var 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");
|
||||
|
||||
var uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse the response if rawResponse=false", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload"
|
||||
).check(function(req) {
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
|
||||
|
||||
client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}, {
|
||||
rawResponse: false,
|
||||
}).then(function(response) {
|
||||
expect(response.content_uri).toEqual("uri");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse errors into a MatrixError", function(done) {
|
||||
// opts.json is false, so request returns unparsed json.
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload"
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(buf);
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(400, JSON.stringify({
|
||||
"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");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should return a promise which can be cancelled", function(done) {
|
||||
var prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
var 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");
|
||||
|
||||
var uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
var 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);
|
||||
room.addEvents([
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", event: true
|
||||
})
|
||||
@@ -43,4 +162,239 @@ describe("MatrixClient", function() {
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFilter", function() {
|
||||
var filterId = "f1lt3r1d";
|
||||
|
||||
it("should return a filter from the store if allowCached", function(done) {
|
||||
var filter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client"
|
||||
});
|
||||
store.storeFilter(filter);
|
||||
client.getFilter(userId, filterId, true).done(function(gotFilter) {
|
||||
expect(gotFilter).toEqual(filter);
|
||||
done();
|
||||
});
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if !allowCached even if one exists",
|
||||
function(done) {
|
||||
var httpFilterDefinition = {
|
||||
event_format: "federation"
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
|
||||
).respond(200, httpFilterDefinition);
|
||||
|
||||
var storeFilter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client"
|
||||
});
|
||||
store.storeFilter(storeFilter);
|
||||
client.getFilter(userId, filterId, false).done(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if nothing is in the cache and then store it",
|
||||
function(done) {
|
||||
var httpFilterDefinition = {
|
||||
event_format: "federation"
|
||||
};
|
||||
expect(store.getFilter(userId, filterId)).toBeNull();
|
||||
|
||||
httpBackend.when(
|
||||
"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();
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFilter", function() {
|
||||
var filterId = "f1llllllerid";
|
||||
|
||||
it("should do an HTTP request and then store the filter", function(done) {
|
||||
expect(store.getFilter(userId, filterId)).toBeNull();
|
||||
|
||||
var filterDefinition = {
|
||||
event_format: "client"
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
"POST", "/user/" + encodeURIComponent(userId) + "/filter"
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(filterDefinition);
|
||||
}).respond(200, {
|
||||
filter_id: filterId
|
||||
});
|
||||
|
||||
client.createFilter(filterDefinition).done(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("searching", function() {
|
||||
|
||||
var response = {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
count: 24,
|
||||
results: {
|
||||
"$flibble:localhost": {
|
||||
rank: 0.1,
|
||||
result: {
|
||||
type: "m.room.message",
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
body: "a result",
|
||||
msgtype: "m.text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it("searchMessageText should perform a /search for room_events", function(done) {
|
||||
client.searchMessageText({
|
||||
query: "monkeys"
|
||||
});
|
||||
httpBackend.when("POST", "/search").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
search_categories: {
|
||||
room_events: {
|
||||
search_term: "monkeys"
|
||||
}
|
||||
}
|
||||
});
|
||||
}).respond(200, response);
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
it("should do an HTTP request and then store the keys", function(done) {
|
||||
var ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
var borisKeys = {
|
||||
dev1: {
|
||||
algorithms: ["1"],
|
||||
device_id: "dev1",
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
signatures: {
|
||||
boris: {
|
||||
"ed25519:dev1":
|
||||
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
|
||||
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw"
|
||||
}
|
||||
},
|
||||
unsigned: { "abc": "def" },
|
||||
user_id: "boris",
|
||||
}
|
||||
};
|
||||
var chazKeys = {
|
||||
dev2: {
|
||||
algorithms: ["2"],
|
||||
device_id: "dev2",
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
chaz: {
|
||||
"ed25519:dev2":
|
||||
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
|
||||
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ"
|
||||
}
|
||||
},
|
||||
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));
|
||||
};
|
||||
|
||||
console.log("Ed25519: " + ed25519key);
|
||||
console.log("boris:", sign(borisKeys.dev1));
|
||||
console.log("chaz:", sign(chazKeys.dev2));
|
||||
*/
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}}});
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
chaz: chazKeys,
|
||||
},
|
||||
});
|
||||
|
||||
client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { "abc": "def" },
|
||||
});
|
||||
|
||||
assertObjectContains(res.chaz.dev2, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2" : ed25519key },
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
});
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
var auth = {a: 1};
|
||||
it("should pass through an auth dict", function(done) {
|
||||
httpBackend.when(
|
||||
"DELETE", "/_matrix/client/unstable/devices/my_device"
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({auth: auth});
|
||||
}).respond(200);
|
||||
|
||||
client.deleteDevice(
|
||||
"my_device", auth
|
||||
).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
for (var k in expected) {
|
||||
if (expected.hasOwnProperty(k)) {
|
||||
expect(obj[k]).toEqual(expected[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,47 +11,45 @@ describe("MatrixClient opts", function() {
|
||||
var userB = "@bob:localhost";
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!foo:bar";
|
||||
var eventData = {
|
||||
chunk: [],
|
||||
start: "s",
|
||||
end: "e"
|
||||
};
|
||||
var initialSync = {
|
||||
end: "s_5_3",
|
||||
presence: [],
|
||||
rooms: [{
|
||||
membership: "join",
|
||||
room_id: roomId,
|
||||
messages: {
|
||||
start: "f_1_1",
|
||||
end: "f_2_2",
|
||||
chunk: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "hello"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
var syncData = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {},
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "hello"
|
||||
})
|
||||
],
|
||||
prev_batch: "f_1_1"
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userB,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userB, name: "Bob"
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: "Alice"
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userB, name: "Bob"
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: "Alice"
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -75,6 +73,10 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should be able to send messages", function(done) {
|
||||
var eventId = "$flibble:wibble";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
@@ -101,13 +103,13 @@ describe("MatrixClient opts", function() {
|
||||
);
|
||||
});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
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("/initialSync", 1);
|
||||
return httpBackend.flush("/filter", 1);
|
||||
}).then(function() {
|
||||
return httpBackend.flush("/events", 1);
|
||||
return httpBackend.flush("/sync", 1);
|
||||
}).done(function() {
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var EventStatus = sdk.EventStatus;
|
||||
|
||||
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;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
scheduler = new sdk.MatrixScheduler();
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken
|
||||
accessToken: accessToken,
|
||||
scheduler: scheduler,
|
||||
});
|
||||
room = new sdk.Room(roomId);
|
||||
client.store.storeRoom(room);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -40,6 +48,52 @@ describe("MatrixClient retrying", function() {
|
||||
|
||||
});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// both events should be in the timeline at this point
|
||||
var tl = room.getLiveTimeline().getEvents();
|
||||
expect(tl.length).toEqual(2);
|
||||
ev1 = tl[0];
|
||||
ev2 = tl[1];
|
||||
|
||||
expect(ev1.status).toEqual(EventStatus.SENDING);
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// 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);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() { client.cancelPendingEvent(ev1); })
|
||||
.toThrow();
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// cancel the first message
|
||||
client.cancelPendingEvent(ev1);
|
||||
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(0);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
describe("resending", function() {
|
||||
xit("should be able to resend a NOT_SENT event", function() {
|
||||
|
||||
|
||||
@@ -12,45 +12,94 @@ describe("MatrixClient room timelines", function() {
|
||||
var accessToken = "aseukfgwef";
|
||||
var roomId = "!foo:bar";
|
||||
var otherUserId = "@bob:localhost";
|
||||
var eventData;
|
||||
var initialSync = {
|
||||
end: "s_5_3",
|
||||
presence: [],
|
||||
rooms: [{
|
||||
membership: "join",
|
||||
room_id: roomId,
|
||||
messages: {
|
||||
start: "f_1_1",
|
||||
end: "f_2_2",
|
||||
chunk: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName
|
||||
});
|
||||
var ROOM_NAME_EVENT = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
});
|
||||
var NEXT_SYNC_DATA;
|
||||
var SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
join: {
|
||||
"!foo:bar": { // roomId
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "hello"
|
||||
})
|
||||
],
|
||||
prev_batch: "f_1_1"
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
ROOM_NAME_EVENT,
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join",
|
||||
user: otherUserId, name: "Bob"
|
||||
}),
|
||||
USER_MEMBERSHIP_EVENT,
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: otherUserId, name: "Bob"
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomId, user: userId,
|
||||
content: {
|
||||
creator: userId
|
||||
}
|
||||
})
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setNextSyncData(events) {
|
||||
events = events || [];
|
||||
NEXT_SYNC_DATA = {
|
||||
next_batch: "n",
|
||||
presence: { events: [] },
|
||||
rooms: {
|
||||
invite: {},
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
timeline: { events: [] },
|
||||
state: { events: [] },
|
||||
ephemeral: { events: [] }
|
||||
}
|
||||
},
|
||||
leave: {}
|
||||
}
|
||||
};
|
||||
events.forEach(function(e) {
|
||||
if (e.room_id !== roomId) {
|
||||
throw new Error("setNextSyncData only works with one room id");
|
||||
}
|
||||
if (e.state_key) {
|
||||
if (e.__prev_event === undefined) {
|
||||
throw new Error(
|
||||
"setNextSyncData needs the prev state set to '__prev_event' " +
|
||||
"for " + e.type
|
||||
);
|
||||
}
|
||||
if (e.__prev_event !== null) {
|
||||
// push the previous state for this event type
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
|
||||
}
|
||||
// push the current
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
}
|
||||
else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
|
||||
}
|
||||
else {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
@@ -58,31 +107,34 @@ describe("MatrixClient room timelines", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken
|
||||
accessToken: accessToken,
|
||||
// these tests should work with or without timelineSupport
|
||||
timelineSupport: true,
|
||||
});
|
||||
eventData = {
|
||||
chunk: [],
|
||||
end: "end_",
|
||||
start: "start_"
|
||||
};
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, function() {
|
||||
return eventData;
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
httpBackend.flush("/pushrules").done(done);
|
||||
httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/filter");
|
||||
}).done(done);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
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("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
@@ -96,11 +148,11 @@ describe("MatrixClient room timelines", function() {
|
||||
expect(member.userId).toEqual(userId);
|
||||
expect(member.name).toEqual(userName);
|
||||
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
@@ -109,26 +161,28 @@ describe("MatrixClient room timelines", function() {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId
|
||||
});
|
||||
eventData.chunk = [
|
||||
utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId
|
||||
})
|
||||
];
|
||||
eventData.chunk[0].event_id = eventId;
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
var 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);
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
@@ -137,19 +191,20 @@ describe("MatrixClient room timelines", function() {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId
|
||||
});
|
||||
eventData.chunk = [
|
||||
utils.mkMessage({
|
||||
body: "I am a fish", user: userId, room: roomId
|
||||
})
|
||||
];
|
||||
eventData.chunk[0].event_id = eventId;
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
var 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");
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
// expect 3rd msg, it doesn't know this is the request is just did
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
promise.done(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
@@ -159,7 +214,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,7 +235,8 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
@@ -191,13 +247,29 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/events", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
// make an m.room.member event with prev_content
|
||||
// We're aiming for an eventual timeline of:
|
||||
//
|
||||
// 'Old Alice' joined the room
|
||||
// <Old Alice> I'm old alice
|
||||
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
|
||||
// <Alice> I'm alice
|
||||
// ------^ /messages results above this point, /sync result below
|
||||
// <Bob> hello
|
||||
|
||||
// make an m.room.member event for alice's join
|
||||
var joinMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
||||
url: null
|
||||
});
|
||||
|
||||
// make an m.room.member event with prev_content for alice's nick
|
||||
// change
|
||||
var oldMshipEvent = utils.mkMembership({
|
||||
mship: "join", user: userId, room: roomId, name: userName,
|
||||
url: "mxc://some/url"
|
||||
@@ -208,7 +280,8 @@ describe("MatrixClient room timelines", function() {
|
||||
membership: "join"
|
||||
};
|
||||
|
||||
// set the list of events to return on scrollback
|
||||
// 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"
|
||||
@@ -216,26 +289,31 @@ describe("MatrixClient room timelines", function() {
|
||||
oldMshipEvent,
|
||||
utils.mkMessage({
|
||||
user: userId, room: roomId, msg: "I'm old alice"
|
||||
})
|
||||
}),
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).done(function() {
|
||||
expect(room.timeline.length).toEqual(4);
|
||||
var oldMsg = room.timeline[0];
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
var joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||
var oldMsg = room.timeline[1];
|
||||
expect(oldMsg.sender.name).toEqual("Old Alice");
|
||||
var newMsg = room.timeline[2];
|
||||
var newMsg = room.timeline[3];
|
||||
expect(newMsg.sender.name).toEqual(userName);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/events", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function(done) {
|
||||
@@ -249,7 +327,8 @@ describe("MatrixClient room timelines", function() {
|
||||
})
|
||||
];
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
@@ -261,9 +340,9 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/events", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function(done) {
|
||||
@@ -274,7 +353,8 @@ describe("MatrixClient room timelines", function() {
|
||||
})
|
||||
];
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.oldState.paginationToken).toBeDefined();
|
||||
|
||||
@@ -282,109 +362,117 @@ describe("MatrixClient room timelines", function() {
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend.flush("/messages", 1).done(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("new events", function() {
|
||||
it("should be added to the right place in the timeline", function(done) {
|
||||
eventData.chunk = [
|
||||
var eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({user: userId, room: roomId})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
setNextSyncData(eventData);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
|
||||
var index = 0;
|
||||
client.on("Room.timeline", function(event, rm, toStart) {
|
||||
expect(toStart).toBe(false);
|
||||
expect(rm).toEqual(room);
|
||||
expect(event.event).toEqual(eventData.chunk[index]);
|
||||
expect(event.event).toEqual(eventData[index]);
|
||||
index += 1;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
expect(index).toEqual(2);
|
||||
expect(room.timeline[room.timeline.length - 1].event).toEqual(
|
||||
eventData.chunk[1]
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[2].event).toEqual(
|
||||
eventData[1]
|
||||
);
|
||||
expect(room.timeline[room.timeline.length - 2].event).toEqual(
|
||||
eventData.chunk[0]
|
||||
expect(room.timeline[1].event).toEqual(
|
||||
eventData[0]
|
||||
);
|
||||
done();
|
||||
});
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function(done) {
|
||||
eventData.chunk = [
|
||||
var eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", name: "New Name"
|
||||
}),
|
||||
utils.mkMessage({user: userId, room: roomId})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
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("/events", 1).done(function() {
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
var preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
var postNameEvent = room.timeline[room.timeline.length - 1];
|
||||
expect(preNameEvent.sender.name).toEqual(userName);
|
||||
expect(postNameEvent.sender.name).toEqual("New Name");
|
||||
done();
|
||||
});
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right room.name", function(done) {
|
||||
eventData.chunk = [
|
||||
utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 2"
|
||||
}
|
||||
})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
var secondRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
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;
|
||||
client.on("Room.name", function(rm) {
|
||||
nameEmitCount += 1;
|
||||
});
|
||||
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
expect(nameEmitCount).toEqual(1);
|
||||
expect(room.name).toEqual("Room 2");
|
||||
// do another round
|
||||
eventData.chunk = [
|
||||
utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
name: "Room 3"
|
||||
}
|
||||
})
|
||||
];
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
var thirdRoomNameEvent = utils.mkEvent({
|
||||
user: userId, room: roomId, type: "m.room.name", content: {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should set the right room members", function(done) {
|
||||
var userC = "@cee:bar";
|
||||
var userD = "@dee:bar";
|
||||
eventData.chunk = [
|
||||
var eventData = [
|
||||
utils.mkMembership({
|
||||
user: userC, room: roomId, mship: "join", name: "C"
|
||||
}),
|
||||
@@ -392,9 +480,14 @@ describe("MatrixClient room timelines", function() {
|
||||
user: userC, room: roomId, mship: "invite", skey: userD
|
||||
})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
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("/events", 1).done(function() {
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC).name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC).membership).toEqual(
|
||||
@@ -404,10 +497,67 @@ describe("MatrixClient room timelines", function() {
|
||||
expect(room.currentState.getMember(userD).membership).toEqual(
|
||||
"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 = [
|
||||
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);
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1).done(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"
|
||||
);
|
||||
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId).membership).toEqual(
|
||||
"join"
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/initialSync", 1);
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should emit a 'Room.timelineReset' event", function(done) {
|
||||
var 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);
|
||||
|
||||
var emitCount = 0;
|
||||
client.on("Room.timelineReset", function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend.flush("/sync", 1).done(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var MatrixEvent = sdk.MatrixEvent;
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
@@ -9,6 +11,11 @@ describe("MatrixClient syncing", function() {
|
||||
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";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
@@ -20,27 +27,23 @@ describe("MatrixClient syncing", function() {
|
||||
accessToken: selfAccessToken
|
||||
});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
describe("startClient", function() {
|
||||
var initialSync = {
|
||||
end: "s_5_3",
|
||||
presence: [],
|
||||
rooms: []
|
||||
};
|
||||
var eventData = {
|
||||
start: "s_5_3",
|
||||
end: "e_6_7",
|
||||
chunk: []
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {},
|
||||
presence: {}
|
||||
};
|
||||
|
||||
it("should start with /initialSync then move onto /events.", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
it("should /sync after /pushrules and /filter.", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
@@ -49,12 +52,12 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass the 'end' token from /initialSync to the from= param " +
|
||||
" of /events", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").check(function(req) {
|
||||
expect(req.queryParams.from).toEqual(initialSync.end);
|
||||
}).respond(200, eventData);
|
||||
it("should pass the 'next_batch' token from /sync to the since= param " +
|
||||
" of the next /sync", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.since).toEqual(syncData.next_batch);
|
||||
}).respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
@@ -64,81 +67,32 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("users", function() {
|
||||
var userA = "@alice:bar";
|
||||
var userB = "@bob:bar";
|
||||
var userC = "@claire:bar";
|
||||
var initialSync = {
|
||||
end: "s_5_3",
|
||||
presence: [
|
||||
utils.mkPresence({
|
||||
user: userA, presence: "online"
|
||||
}),
|
||||
utils.mkPresence({
|
||||
user: userB, presence: "unavailable"
|
||||
})
|
||||
],
|
||||
rooms: []
|
||||
};
|
||||
var eventData = {
|
||||
start: "s_5_3",
|
||||
end: "e_6_7",
|
||||
chunk: [
|
||||
// existing user change
|
||||
utils.mkPresence({
|
||||
user: userA, presence: "offline"
|
||||
}),
|
||||
// new user C
|
||||
utils.mkPresence({
|
||||
user: userC, presence: "online"
|
||||
})
|
||||
]
|
||||
describe("resolving invites to profile info", function() {
|
||||
|
||||
var syncData = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: []
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it("should create users for presence events from /initialSync and /events",
|
||||
function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
expect(client.getUser(userA).presence).toEqual("offline");
|
||||
expect(client.getUser(userB).presence).toEqual("unavailable");
|
||||
expect(client.getUser(userC).presence).toEqual("online");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("room state", function() {
|
||||
var roomOne = "!foo:localhost";
|
||||
var roomTwo = "!bar:localhost";
|
||||
var msgText = "some text here";
|
||||
var otherDisplayName = "Bob Smith";
|
||||
var initialSync = {
|
||||
end: "s_5_3",
|
||||
presence: [],
|
||||
rooms: [
|
||||
{
|
||||
membership: "join",
|
||||
room_id: roomOne,
|
||||
messages: {
|
||||
start: "f_1_1",
|
||||
end: "f_2_2",
|
||||
chunk: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
}),
|
||||
beforeEach(function() {
|
||||
syncData.presence.events = [];
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId
|
||||
}),
|
||||
@@ -152,72 +106,271 @@ describe("MatrixClient syncing", function() {
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
membership: "join",
|
||||
room_id: roomTwo,
|
||||
messages: {
|
||||
start: "f_1_1",
|
||||
end: "f_2_2",
|
||||
chunk: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hiii"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: [
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: otherUserId,
|
||||
name: otherDisplayName
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: selfUserId
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomTwo, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
var eventData = {
|
||||
start: "s_5_3",
|
||||
end: "e_6_7",
|
||||
chunk: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: selfUserId,
|
||||
content: { name: "A new room name" }
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: msgText
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: roomTwo,
|
||||
content: { user_ids: [otherUserId] }
|
||||
};
|
||||
});
|
||||
|
||||
it("should resolve incoming invites from /sync", function(done) {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
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"
|
||||
}
|
||||
);
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true
|
||||
});
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var member = client.getRoom(roomOne).getMember(userC);
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false)
|
||||
).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use cached values from m.presence wherever possible", function(done) {
|
||||
syncData.presence.events = [
|
||||
utils.mkPresence({
|
||||
user: userC, presence: "online", name: "The Ghost"
|
||||
}),
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC
|
||||
})
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true
|
||||
});
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var 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) {
|
||||
syncData.presence.events = [
|
||||
utils.mkPresence({
|
||||
user: userC, presence: "online", name: "The Ghost"
|
||||
})
|
||||
];
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "invite", user: userC
|
||||
})
|
||||
);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
var latestFiredName = null;
|
||||
client.on("RoomMember.name", function(event, m) {
|
||||
if (m.userId === userC && m.roomId === roomOne) {
|
||||
latestFiredName = m.name;
|
||||
}
|
||||
});
|
||||
|
||||
client.startClient({
|
||||
resolveInvitesToProfiles: true
|
||||
});
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
expect(latestFiredName).toEqual("The Ghost");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
|
||||
syncData.rooms.join[roomOne].state.events.push(
|
||||
utils.mkMembership({
|
||||
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);
|
||||
expect(member.name).toEqual(userC);
|
||||
expect(
|
||||
member.getAvatarUrl("home.server.url", null, null, null, false)
|
||||
).toBeNull();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("users", function() {
|
||||
var syncData = {
|
||||
next_batch: "nb",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: userA, presence: "online"
|
||||
}),
|
||||
utils.mkPresence({
|
||||
user: userB, presence: "unavailable"
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
it("should create users for presence events from /sync",
|
||||
function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(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";
|
||||
|
||||
var syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hiii"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: otherUserId,
|
||||
name: otherDisplayName
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomTwo, mship: "join", user: selfUserId
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomTwo, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var nextSyncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
nextSyncData.rooms.join[roomOne] = {
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: selfUserId,
|
||||
content: { name: "A new room name" }
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
nextSyncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: msgText
|
||||
})
|
||||
]
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: roomTwo,
|
||||
content: { user_ids: [otherUserId] }
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
it("should continually recalculate the right room name.", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
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);
|
||||
// should have clobbered the name to the one from /events
|
||||
expect(room.name).toEqual(eventData.chunk[0].content.name);
|
||||
expect(room.name).toEqual(
|
||||
nextSyncData.rooms.join[roomOne].state.events[0].content.name
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should store the right events in the timeline.", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
@@ -231,8 +384,8 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
|
||||
it("should set the right room name.", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
httpBackend.flush().done(function() {
|
||||
@@ -244,8 +397,8 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
|
||||
it("should set the right user's typing flag.", function(done) {
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
@@ -270,6 +423,182 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline", function() {
|
||||
beforeEach(function() {
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on new rooms", function(done) {
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "roomtwo"
|
||||
}),
|
||||
],
|
||||
prev_batch: "roomtwotok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomTwo);
|
||||
var 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 = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world"
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
var resetCallCount = 0;
|
||||
// the token should be set *before* timelineReset is emitted
|
||||
client.on("Room.timelineReset", function(room) {
|
||||
resetCallCount++;
|
||||
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(0);
|
||||
var tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("newerTok");
|
||||
});
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
expect(resetCallCount).toEqual(1);
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("receipts", function() {
|
||||
var syncData = {
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomOne] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "hello"
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomOne, user: otherUserId, msg: "world"
|
||||
})
|
||||
]
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: otherUserId
|
||||
}),
|
||||
utils.mkMembership({
|
||||
room: roomOne, mship: "join", user: selfUserId
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: roomOne, user: selfUserId,
|
||||
content: {
|
||||
creator: selfUserId
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
syncData.rooms.join[roomOne].ephemeral = {
|
||||
events: []
|
||||
};
|
||||
});
|
||||
|
||||
it("should sync receipts from /sync.", function(done) {
|
||||
var ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
|
||||
var receipt = {};
|
||||
receipt[ackEvent.event_id] = {
|
||||
"m.read": {}
|
||||
};
|
||||
receipt[ackEvent.event_id]["m.read"][userC] = {
|
||||
ts: 176592842636
|
||||
};
|
||||
syncData.rooms.join[roomOne].ephemeral.events = [{
|
||||
content: receipt,
|
||||
room_id: roomOne,
|
||||
type: "m.receipt"
|
||||
}];
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
|
||||
type: "m.read",
|
||||
userId: userC,
|
||||
data: {
|
||||
ts: 176592842636
|
||||
}
|
||||
}]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", function() {
|
||||
xit("should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)", function() {
|
||||
@@ -280,4 +609,82 @@ describe("MatrixClient syncing", function() {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncLeftRooms", function() {
|
||||
beforeEach(function(done) {
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().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.
|
||||
httpBackend.when("GET", "/sync").respond(200, {});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and use an appropriate filter", function(done) {
|
||||
httpBackend.when("POST", "/filter").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
room: { timeline: {limit: 1},
|
||||
include_leave: true }});
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
done();
|
||||
}).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);
|
||||
});
|
||||
|
||||
it("should set the back-pagination token on left rooms", function(done) {
|
||||
var syncData = {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
leave: {}
|
||||
},
|
||||
};
|
||||
|
||||
syncData.rooms.leave[roomTwo] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomTwo, user: otherUserId, msg: "hello"
|
||||
}),
|
||||
],
|
||||
prev_batch: "pagTok",
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("POST", "/filter").respond(200, {
|
||||
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);
|
||||
|
||||
expect(tok).toEqual("pagTok");
|
||||
done();
|
||||
}).catch(utils.failTest).done();
|
||||
|
||||
// first flush the filter request; this will make syncLeftRooms
|
||||
// make its /sync call
|
||||
httpBackend.flush("/filter").then(function() {
|
||||
return httpBackend.flush();
|
||||
}).catch(utils.failTest);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,893 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
|
||||
try {
|
||||
var Olm = require('olm');
|
||||
} catch (e) {}
|
||||
|
||||
var anotherjson = require('another-json');
|
||||
var q = require('q');
|
||||
|
||||
var sdk = require('../..');
|
||||
var utils = require('../../lib/utils');
|
||||
var test_utils = require('../test-utils');
|
||||
var MockHttpBackend = require('../mock-request');
|
||||
|
||||
var ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} userId
|
||||
* @param {string} deviceId
|
||||
* @param {string} accessToken
|
||||
*/
|
||||
function TestClient(userId, deviceId, accessToken) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
this.storage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
this.client = sdk.createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: this.storage,
|
||||
request: this.httpBackend.requestFn,
|
||||
});
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*
|
||||
* @param {object?} deviceQueryResponse the list of our existing devices to return from
|
||||
* the /query request. Defaults to empty device list
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function(existingDevices) {
|
||||
var self = this;
|
||||
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
this.httpBackend.when('POST', '/keys/query').respond(200, function(path, content) {
|
||||
expect(content.device_keys[self.userId]).toEqual({});
|
||||
var res = existingDevices;
|
||||
if (!res) {
|
||||
res = { device_keys: {} };
|
||||
res.device_keys[self.userId] = {};
|
||||
}
|
||||
return res;
|
||||
});
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toBeDefined();
|
||||
expect(content.device_keys).toBeDefined();
|
||||
self.deviceKeys = content.device_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
});
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.device_keys).not.toBeDefined();
|
||||
expect(content.one_time_keys).toBeDefined();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
self.oneTimeKeys = content.one_time_keys;
|
||||
return {one_time_key_counts: {
|
||||
signed_curve25519: utils.keys(self.oneTimeKeys).length
|
||||
}};
|
||||
});
|
||||
|
||||
this.client.startClient();
|
||||
|
||||
return this.httpBackend.flush();
|
||||
};
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
*/
|
||||
TestClient.prototype.stop = function() {
|
||||
this.client.stopClient();
|
||||
};
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getDeviceKey = function() {
|
||||
var key_id = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[key_id];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getSigningKey = function() {
|
||||
var key_id = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[key_id];
|
||||
};
|
||||
|
||||
/**
|
||||
* start an Olm session with a given recipient
|
||||
*
|
||||
* @param {Olm.Account} olmAccount
|
||||
* @param {TestClient} recipientTestClient
|
||||
* @return {Olm.Session}
|
||||
*/
|
||||
function createOlmSession(olmAccount, recipientTestClient) {
|
||||
var otk_id = utils.keys(recipientTestClient.oneTimeKeys)[0];
|
||||
var otk = recipientTestClient.oneTimeKeys[otk_id];
|
||||
|
||||
var session = new 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).toBeDefined();
|
||||
expect(opts.p2pSession).toBeDefined();
|
||||
expect(opts.recipient).toBeDefined();
|
||||
|
||||
var plaintext = {
|
||||
content: opts.plaincontent || {},
|
||||
recipient: opts.recipient.userId,
|
||||
recipient_keys: {
|
||||
ed25519: opts.recipient.getSigningKey(),
|
||||
},
|
||||
sender: opts.sender || '@bob:xyz',
|
||||
type: opts.plaintype || 'm.test',
|
||||
};
|
||||
|
||||
var 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).toBeDefined();
|
||||
expect(opts.groupSession).toBeDefined();
|
||||
|
||||
var 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).toBeDefined();
|
||||
plaintext.room_id = opts.room_id;
|
||||
}
|
||||
|
||||
return {
|
||||
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) {
|
||||
var roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
test_utils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: roomMembers[i],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = roomResponse;
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var testOlmAccount;
|
||||
var testSenderKey;
|
||||
var aliceTestClient;
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
* response to /keys/query
|
||||
*/
|
||||
function getTestKeysQueryResponse(userId) {
|
||||
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
var 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,
|
||||
};
|
||||
var j = anotherjson.stringify(testDeviceKeys);
|
||||
var sig = testOlmAccount.sign(j);
|
||||
testDeviceKeys.signatures = {};
|
||||
testDeviceKeys.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
var 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
|
||||
*/
|
||||
function getTestKeysClaimResponse(userId) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
var testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
var keyId = utils.keys(testOneTimeKeys.curve25519)[0];
|
||||
var oneTimeKey = testOneTimeKeys.curve25519[keyId];
|
||||
var keyResult = {
|
||||
'key': oneTimeKey,
|
||||
};
|
||||
var j = anotherjson.stringify(keyResult);
|
||||
var sig = testOlmAccount.sign(j);
|
||||
keyResult.signatures = {};
|
||||
keyResult.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
var 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(function() {
|
||||
test_utils.beforeEach(this);
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs"
|
||||
);
|
||||
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
var roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
var messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
var 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.httpBackend.flush("/sync", 1);
|
||||
}).then(function() {
|
||||
var room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
var event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("Alice gets a second room_key message", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
var roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
var messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// make a second room_key event now that we have advanced the group
|
||||
// session.
|
||||
var 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.
|
||||
var 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);
|
||||
|
||||
return aliceTestClient.httpBackend.flush("/sync", 2);
|
||||
}).then(function() {
|
||||
var room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
var event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it('Alice sends a megolm message', function(done) {
|
||||
var p2pSession;
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
var inboundGroupSession;
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
var m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
var 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) {
|
||||
var ct = content.ciphertext;
|
||||
var r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
var decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
}
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log('Forcing alice to download our device keys');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
console.log('Telling alice to send a megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("We should start a new megolm session when a device is blocked", function(done) {
|
||||
var p2pSession;
|
||||
var megolmSessionId;
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to send a megolm message');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('sendToDevice: ', content);
|
||||
var m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(1); // normal message
|
||||
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
console.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) {
|
||||
console.log('/send:', content);
|
||||
expect(content.session_id).toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
console.log('Telling alice to send another megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('/send:', content);
|
||||
expect(content.session_id).not.toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/2676
|
||||
it("Alice should send to her other devices", function(done) {
|
||||
// for this test, we make the testOlmAccount be another of Alice's devices.
|
||||
// it ought to get include in messages Alice sends.
|
||||
|
||||
var p2pSession;
|
||||
var inboundGroupSession;
|
||||
var decrypted;
|
||||
|
||||
return aliceTestClient.start(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId)
|
||||
).then(function() {
|
||||
// an encrypted room with just alice
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
state: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
test_utils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: aliceTestClient.userId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush();
|
||||
}).then(function() {
|
||||
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) {
|
||||
console.log("sendToDevice: ", content);
|
||||
var m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
p2pSession = new Olm.Session();
|
||||
p2pSession.create_inbound(testOlmAccount, ct.body);
|
||||
var 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) {
|
||||
var ct = content.ciphertext;
|
||||
var r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
decrypted = JSON.parse(r.plaintext);
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message',
|
||||
function(done) {
|
||||
var p2pSession;
|
||||
var inboundGroupSession;
|
||||
|
||||
var downloadPromise;
|
||||
var sendPromise;
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
var m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
var 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) {
|
||||
var ct = content.ciphertext;
|
||||
var r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
var decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log('Forcing alice to download our device keys');
|
||||
|
||||
// this will block
|
||||
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(function() {
|
||||
|
||||
// so will this.
|
||||
sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
||||
}).then(function() {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
return aliceTestClient.httpBackend.flush();
|
||||
}).then(function() {
|
||||
return q.all([downloadPromise, sendPromise]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
+77
-8
@@ -11,9 +11,23 @@ function HttpBackend() {
|
||||
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;
|
||||
self.requests.push(realReq);
|
||||
var req = new Request(opts, callback);
|
||||
console.log("HTTP backend received request: %s", req);
|
||||
self.requests.push(req);
|
||||
|
||||
var abort = function() {
|
||||
var idx = self.requests.indexOf(req);
|
||||
if (idx >= 0) {
|
||||
console.log("Aborting HTTP request: %s %s", opts.method,
|
||||
opts.uri);
|
||||
self.requests.splice(idx, 1);
|
||||
req.callback("aborted");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
abort: abort
|
||||
};
|
||||
};
|
||||
}
|
||||
HttpBackend.prototype = {
|
||||
@@ -27,6 +41,7 @@ HttpBackend.prototype = {
|
||||
var defer = q.defer();
|
||||
var self = this;
|
||||
var flushed = 0;
|
||||
var triedWaiting = false;
|
||||
console.log(
|
||||
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
|
||||
);
|
||||
@@ -48,6 +63,12 @@ HttpBackend.prototype = {
|
||||
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();
|
||||
@@ -139,22 +160,32 @@ HttpBackend.prototype = {
|
||||
* @return {Request} An expected request.
|
||||
*/
|
||||
when: function(method, path, data) {
|
||||
var pendingReq = new Request(method, path, data);
|
||||
var pendingReq = new ExpectedRequest(method, path, data);
|
||||
this.expectedRequests.push(pendingReq);
|
||||
return pendingReq;
|
||||
}
|
||||
};
|
||||
|
||||
function Request(method, path, data, queryParams) {
|
||||
/**
|
||||
* Represents the expectation of a request.
|
||||
*
|
||||
* <p>Includes the conditions to be matched against, the checks to be made,
|
||||
* and the response to be returned.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} method
|
||||
* @param {string} path
|
||||
* @param {object?} data
|
||||
*/
|
||||
function ExpectedRequest(method, path, data) {
|
||||
this.method = method;
|
||||
this.path = path;
|
||||
this.data = data;
|
||||
this.queryParams = queryParams;
|
||||
this.callback = null;
|
||||
this.response = null;
|
||||
this.checks = [];
|
||||
}
|
||||
Request.prototype = {
|
||||
|
||||
ExpectedRequest.prototype = {
|
||||
/**
|
||||
* Execute a check when this request has been satisfied.
|
||||
* @param {Function} fn The function to execute.
|
||||
@@ -199,6 +230,44 @@ Request.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a request made by the app.
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} opts opts passed to request()
|
||||
* @param {function} callback
|
||||
*/
|
||||
function Request(opts, callback) {
|
||||
this.opts = opts;
|
||||
this.callback = callback;
|
||||
|
||||
Object.defineProperty(this, 'method', {
|
||||
get: function() { return opts.method; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'path', {
|
||||
get: function() { return opts.uri; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'data', {
|
||||
get: function() { return opts.body; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'queryParams', {
|
||||
get: function() { return opts.qs; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'headers', {
|
||||
get: function() { return opts.headers || {}; }
|
||||
});
|
||||
}
|
||||
|
||||
Request.prototype = {
|
||||
toString: function() {
|
||||
return this.method + " " + this.path;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The HttpBackend class.
|
||||
*/
|
||||
|
||||
+57
-7
@@ -48,7 +48,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.
|
||||
@@ -61,11 +61,11 @@ module.exports.mkEvent = function(opts) {
|
||||
var event = {
|
||||
type: opts.type,
|
||||
room_id: opts.room,
|
||||
user_id: opts.user,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
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",
|
||||
@@ -88,8 +88,8 @@ module.exports.mkPresence = function(opts) {
|
||||
var event = {
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
type: "m.presence",
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: {
|
||||
user_id: opts.user,
|
||||
avatar_url: opts.url,
|
||||
displayname: opts.name,
|
||||
last_active_ago: opts.ago,
|
||||
@@ -104,8 +104,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,7 +115,7 @@ 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));
|
||||
@@ -151,3 +151,53 @@ module.exports.mkMessage = function(opts) {
|
||||
};
|
||||
return module.exports.mkEvent(opts);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* make the test fail, with the given exception
|
||||
*
|
||||
* <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} err exception to be reported
|
||||
*
|
||||
* @deprecated
|
||||
* It turns out there are easier ways of doing this. Just use nodeify():
|
||||
*
|
||||
* it("should not throw", function(done) {
|
||||
* asynchronousMethod().then(function() {
|
||||
* // some tests
|
||||
* }).nodeify(done);
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* it("should not throw", function(done) {
|
||||
* asynchronousMethod().then(function() {
|
||||
* // some tests
|
||||
* }).catch(utils.failTest).done(done);
|
||||
* });
|
||||
*/
|
||||
module.exports.failTest = function(err) {
|
||||
expect(true).toBe(false, "Testfunc threw: " + err.stack);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
module.exports.MockStorageApi = function() {
|
||||
this.data = {};
|
||||
};
|
||||
module.exports.MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"use strict";
|
||||
var ContentRepo = require("../../lib/content-repo");
|
||||
var testUtils = require("../test-utils");
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
var baseUrl = "https://my.home.server";
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this);
|
||||
});
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
var httpUrl = "http://example.com/image.jpeg";
|
||||
expect(
|
||||
ContentRepo.getHttpUriForMxc(
|
||||
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";
|
||||
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";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function() {
|
||||
expect(ContentRepo.getHttpUriForMxc(null)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a thumbnail URL if a width/height/resize is specified",
|
||||
function() {
|
||||
var 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"
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/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";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
"use strict";
|
||||
var Crypto = require("../../lib/crypto");
|
||||
var sdk = require("../..");
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
expect(Crypto.getOlmVersion()).toEqual([2, 0, 0]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,370 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
function mockRoomStates(timeline) {
|
||||
timeline._startState = utils.mock(sdk.RoomState, "startState");
|
||||
timeline._endState = utils.mock(sdk.RoomState, "endState");
|
||||
}
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
var roomId = "!foo:bar";
|
||||
var userA = "@alice:bar";
|
||||
var userB = "@bertha:bar";
|
||||
var timeline;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
var 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();
|
||||
expect(v).toEqual(roomId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initialiseState", function() {
|
||||
beforeEach(function() {
|
||||
mockRoomStates(timeline);
|
||||
});
|
||||
|
||||
it("should copy state events to start and end state", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
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" },
|
||||
})
|
||||
];
|
||||
timeline.initialiseState(events);
|
||||
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
|
||||
events
|
||||
);
|
||||
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
|
||||
events
|
||||
);
|
||||
});
|
||||
|
||||
it("should raise an exception if called after events are added", function() {
|
||||
var event =
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "Adam stole the plushies",
|
||||
event: true,
|
||||
});
|
||||
|
||||
var state = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA,
|
||||
event: true,
|
||||
})
|
||||
];
|
||||
|
||||
expect(function() { timeline.initialiseState(state); }).not.toThrow();
|
||||
timeline.addEvent(event, false);
|
||||
expect(function() { timeline.initialiseState(state); }).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginationTokens", function() {
|
||||
it("pagination tokens should start null", function() {
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
|
||||
});
|
||||
|
||||
it("setPaginationToken should set token", function() {
|
||||
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
|
||||
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
|
||||
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("neighbouringTimelines", function() {
|
||||
it("neighbouring timelines should start null", function() {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should set neighbour", function() {
|
||||
var prev = {a: "a"};
|
||||
var next = {b: "b"};
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should throw if called twice", function() {
|
||||
var prev = {a: "a"};
|
||||
var next = {b: "b"};
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(prev);
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).toThrow();
|
||||
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(next);
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEvent", function() {
|
||||
beforeEach(function() {
|
||||
mockRoomStates(timeline);
|
||||
});
|
||||
|
||||
var events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "nom nom nom",
|
||||
event: true,
|
||||
}),
|
||||
];
|
||||
|
||||
it("should be able to add events to the end", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
var initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[0]);
|
||||
expect(timeline.getEvents()[1]).toEqual(events[1]);
|
||||
});
|
||||
|
||||
it("should be able to add events to the start", function() {
|
||||
timeline.addEvent(events[0], true);
|
||||
var initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], true);
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getEvents()[0]).toEqual(events[1]);
|
||||
expect(timeline.getEvents()[1]).toEqual(events[0]);
|
||||
});
|
||||
|
||||
it("should set event.sender for new and old events", function() {
|
||||
var sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
};
|
||||
var oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice"
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
var newEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "New Room Name" }
|
||||
});
|
||||
var oldEv = utils.mkEvent({
|
||||
type: "m.room.name", room: roomId, user: userA, event: true,
|
||||
content: { name: "Old Room Name" }
|
||||
});
|
||||
|
||||
timeline.addEvent(newEv, false);
|
||||
expect(newEv.sender).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, true);
|
||||
expect(oldEv.sender).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
it("should set event.target for new and old m.room.member events",
|
||||
function() {
|
||||
var sentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Alice"
|
||||
};
|
||||
var oldSentinel = {
|
||||
userId: userA,
|
||||
membership: "join",
|
||||
name: "Old Alice"
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCallFake(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
var 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
|
||||
});
|
||||
timeline.addEvent(newEv, false);
|
||||
expect(newEv.target).toEqual(sentinel);
|
||||
timeline.addEvent(oldEv, true);
|
||||
expect(oldEv.target).toEqual(oldSentinel);
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for new events", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
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"
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]]);
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
var events = [
|
||||
utils.mkMembership({
|
||||
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"
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
timeline.addEvent(events[0], true);
|
||||
timeline.addEvent(events[1], true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[0]]);
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
toHaveBeenCalledWith([events[1]]);
|
||||
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeEvent", function() {
|
||||
var events = [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "hungry hungry hungry",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "nom nom nom",
|
||||
event: true,
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: roomId, user: userB, msg: "piiie",
|
||||
event: true,
|
||||
}),
|
||||
];
|
||||
|
||||
it("should remove events", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], false);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
|
||||
var ev = timeline.removeEvent(events[0].getId());
|
||||
expect(ev).toBe(events[0]);
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
|
||||
ev = timeline.removeEvent(events[1].getId());
|
||||
expect(ev).toBe(events[1]);
|
||||
expect(timeline.getEvents().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should update baseIndex", function() {
|
||||
timeline.addEvent(events[0], false);
|
||||
timeline.addEvent(events[1], true);
|
||||
timeline.addEvent(events[2], false);
|
||||
expect(timeline.getEvents().length).toEqual(3);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
timeline.removeEvent(events[2].getId());
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
expect(timeline.getBaseIndex()).toEqual(1);
|
||||
|
||||
timeline.removeEvent(events[1].getId());
|
||||
expect(timeline.getEvents().length).toEqual(1);
|
||||
expect(timeline.getBaseIndex()).toEqual(0);
|
||||
});
|
||||
|
||||
// this is basically https://github.com/vector-im/vector-web/issues/937
|
||||
// - removing the last event got baseIndex into such a state that
|
||||
// further addEvent(ev, false) calls made the index increase.
|
||||
it("should not make baseIndex assplode when removing the last event",
|
||||
function() {
|
||||
timeline.addEvent(events[0], true);
|
||||
timeline.removeEvent(events[0].getId());
|
||||
var initialIndex = timeline.getBaseIndex();
|
||||
timeline.addEvent(events[1], false);
|
||||
timeline.addEvent(events[2], false);
|
||||
expect(timeline.getBaseIndex()).toEqual(initialIndex);
|
||||
expect(timeline.getEvents().length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var Filter = sdk.Filter;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
describe("Filter", function() {
|
||||
var filterId = "f1lt3ring15g00d4ursoul";
|
||||
var userId = "@sir_arthur_david:humming.tiger";
|
||||
var filter;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
describe("fromJson", function() {
|
||||
it("create a new Filter from the provided values", function() {
|
||||
var definition = {
|
||||
event_fields: ["type", "content"]
|
||||
};
|
||||
var f = Filter.fromJson(userId, filterId, definition);
|
||||
expect(f.getDefinition()).toEqual(definition);
|
||||
expect(f.userId).toEqual(userId);
|
||||
expect(f.filterId).toEqual(filterId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTimelineLimit", function() {
|
||||
it("should set room.timeline.limit of the filter definition", function() {
|
||||
filter.setTimelineLimit(10);
|
||||
expect(filter.getDefinition()).toEqual({
|
||||
room: {
|
||||
timeline: {
|
||||
limit: 10
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDefinition/getDefinition", function() {
|
||||
it("should set and get the filter body", function() {
|
||||
var definition = {
|
||||
event_format: "client"
|
||||
};
|
||||
filter.setDefinition(definition);
|
||||
expect(filter.getDefinition()).toEqual(definition);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
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 q = require("q");
|
||||
var sdk = require("../..");
|
||||
var utils = require("../test-utils");
|
||||
|
||||
var InteractiveAuth = sdk.InteractiveAuth;
|
||||
var MatrixError = sdk.MatrixError;
|
||||
|
||||
describe("InteractiveAuth", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
});
|
||||
|
||||
it("should start an auth stage and complete it", function(done) {
|
||||
var doRequest = jasmine.createSpy('doRequest');
|
||||
var startAuthStage = jasmine.createSpy('startAuthStage');
|
||||
|
||||
var ia = new InteractiveAuth({
|
||||
doRequest: doRequest,
|
||||
startAuthStage: startAuthStage,
|
||||
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
|
||||
startAuthStage.andCallFake(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
var requestRes = {"a": "b"};
|
||||
doRequest.andCallFake(function(authData) {
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return q(requestRes);
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(1);
|
||||
expect(startAuthStage.calls.length).toEqual(1);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", function(done) {
|
||||
var doRequest = jasmine.createSpy('doRequest');
|
||||
var startAuthStage = jasmine.createSpy('startAuthStage');
|
||||
|
||||
var ia = new InteractiveAuth({
|
||||
doRequest: doRequest,
|
||||
startAuthStage: startAuthStage,
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toBe(undefined);
|
||||
expect(ia.getStageParams("logintype")).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.andCallFake(function(authData) {
|
||||
console.log("request1", authData);
|
||||
expect(authData).toBe(null);
|
||||
var 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 startAuthStage
|
||||
var requestRes = {"a": "b"};
|
||||
startAuthStage.andCallFake(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.andCallFake(function(authData) {
|
||||
console.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return q(requestRes);
|
||||
});
|
||||
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(2);
|
||||
expect(startAuthStage.calls.length).toEqual(1);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,516 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var MatrixClient = sdk.MatrixClient;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
var userId = "@alice:bar";
|
||||
var identityServerUrl = "https://identity.server";
|
||||
var identityServerDomain = "identity.server";
|
||||
var client, store, scheduler;
|
||||
|
||||
var KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
var PUSH_RULES_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/pushrules/",
|
||||
data: {}
|
||||
};
|
||||
|
||||
var FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
|
||||
|
||||
var FILTER_RESPONSE = {
|
||||
method: "POST",
|
||||
path: FILTER_PATH,
|
||||
data: { filter_id: "f1lt3r" }
|
||||
};
|
||||
|
||||
var SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: { events: [] },
|
||||
rooms: {}
|
||||
};
|
||||
|
||||
var SYNC_RESPONSE = {
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: SYNC_DATA
|
||||
};
|
||||
|
||||
var httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
// method: "GET",
|
||||
// path: "/initialSync",
|
||||
// data: {},
|
||||
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
|
||||
// expectBody: {} // additional expects on the body
|
||||
// expectQueryParams: {} // additional expects on query params
|
||||
// thenCall: function(){} // function to call *AFTER* returning response.
|
||||
// }
|
||||
// items are popped off when processed and block if no items left.
|
||||
];
|
||||
var accept_keepalives;
|
||||
var pendingLookup = null;
|
||||
function httpReq(cb, method, path, qp, data, prefix) {
|
||||
if (path === KEEP_ALIVE_PATH && accept_keepalives) {
|
||||
return q();
|
||||
}
|
||||
var next = httpLookups.shift();
|
||||
var logLine = (
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
console.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (pendingLookup) {
|
||||
if (pendingLookup.method === method && pendingLookup.path === path) {
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path
|
||||
);
|
||||
}
|
||||
pendingLookup = {
|
||||
promise: q.defer().promise,
|
||||
method: method,
|
||||
path: path
|
||||
};
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
console.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response"
|
||||
);
|
||||
if (next.expectBody) {
|
||||
expect(next.expectBody).toEqual(data);
|
||||
}
|
||||
if (next.expectQueryParams) {
|
||||
Object.keys(next.expectQueryParams).forEach(function(k) {
|
||||
expect(qp[k]).toEqual(next.expectQueryParams[k]);
|
||||
});
|
||||
}
|
||||
|
||||
if (next.thenCall) {
|
||||
process.nextTick(next.thenCall, 0); // next tick so we return first.
|
||||
}
|
||||
|
||||
if (next.error) {
|
||||
return q.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error
|
||||
});
|
||||
}
|
||||
return q(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
return q.defer().promise;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
jasmine.Clock.useMock();
|
||||
scheduler = jasmine.createSpyObj("scheduler", [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction"
|
||||
]);
|
||||
store = jasmine.createSpyObj("store", [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
|
||||
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
|
||||
]);
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
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.request.andCallFake(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
accept_keepalives = true;
|
||||
pendingLookup = null;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
var filterId = "ehfewf";
|
||||
store.getFilterIdByName.andReturn(filterId);
|
||||
var 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSyncState", function() {
|
||||
|
||||
it("should return null if the client isn't started", function() {
|
||||
expect(client.getSyncState()).toBeNull();
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
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 : "");
|
||||
}
|
||||
var 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);
|
||||
|
||||
var filterName = getFilterName(client.credentials.userId);
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
var filter = new sdk.Filter(client.credentials.userId);
|
||||
|
||||
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", function() {
|
||||
client.startClient();
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
it("should work on /filter", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jasmine.Clock.tick(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should work on /sync", function(done) {
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push({
|
||||
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"
|
||||
);
|
||||
jasmine.Clock.tick(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jasmine.Clock.tick(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should work on /pushrules", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jasmine.Clock.tick(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
if (!expected) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
expect(state).toEqual(expected[0]);
|
||||
expect(old).toEqual(expected[1]);
|
||||
if (expectedStates.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
}
|
||||
// standard retry time is 5 to 10 seconds
|
||||
jasmine.Clock.tick(10000);
|
||||
};
|
||||
}
|
||||
|
||||
it("should transition null -> PREPARED after the first /sync", function(done) {
|
||||
var 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 = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
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",
|
||||
function(done) {
|
||||
var expectedStates = [];
|
||||
accept_keepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push({
|
||||
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(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["PREPARED", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function(done) {
|
||||
var expectedStates = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
|
||||
accept_keepalives = false;
|
||||
var expectedStates = [];
|
||||
httpLookups.push({
|
||||
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(["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 = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
});
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
|
||||
function(done) {
|
||||
var expectedStates = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
accept_keepalives = false;
|
||||
var expectedStates = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
});
|
||||
httpLookups.push({
|
||||
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(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("inviteByEmail", function() {
|
||||
var roomId = "!foo:bar";
|
||||
|
||||
it("should send an invite HTTP POST", function() {
|
||||
httpLookups = [{
|
||||
method: "POST",
|
||||
path: "/rooms/!foo%3Abar/invite",
|
||||
data: {},
|
||||
expectBody: {
|
||||
id_server: identityServerDomain,
|
||||
medium: "email",
|
||||
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({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
data: SYNC_DATA,
|
||||
thenCall: function() {
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.setGuest(true);
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
xit("should be able to peek into a room using peekInRoom", function(done) {
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -214,25 +214,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.event);
|
||||
var 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.event);
|
||||
var 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.event);
|
||||
var 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.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -240,13 +240,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.event);
|
||||
var 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.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -254,43 +254,43 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should bing on a bing word.', function() {
|
||||
testEvent.event.content.body = "I really like coffee";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var 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.event);
|
||||
var 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.event);
|
||||
var 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.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "Pong!";
|
||||
actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
it('should bing on character range ([a-z]) bing words.', function() {
|
||||
testEvent.event.content.body = "I ate 6 pies";
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var 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.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
testEvent.event.content.body = "bake";
|
||||
actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -298,7 +298,7 @@ describe('NotificationService', function() {
|
||||
|
||||
it('should gracefully handle bad input.', function() {
|
||||
testEvent.event.content.body = { "foo": "bar" };
|
||||
var actions = pushProcessor.actionsForEvent(testEvent.event);
|
||||
var actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"use strict";
|
||||
|
||||
var callbacks = require("../../lib/realtime-callbacks");
|
||||
var test_utils = require("../test-utils.js");
|
||||
|
||||
describe("realtime-callbacks", function() {
|
||||
var clock = jasmine.Clock;
|
||||
var fakeDate;
|
||||
|
||||
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; });
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
callbacks.setNow();
|
||||
});
|
||||
|
||||
describe("setTimeout", function() {
|
||||
it("should call the callback after the timeout", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
callbacks.setTimeout(callback, 100);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should default to a zero timeout", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should pass any parameters to the callback", function() {
|
||||
var callback = jasmine.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();
|
||||
});
|
||||
callbacks.setTimeout(callback);
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle timeouts of several seconds", function() {
|
||||
var callback = jasmine.createSpy();
|
||||
callbacks.setTimeout(callback, 2000);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
for (var 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");
|
||||
callbacks.setTimeout(callback2, 200);
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
callbacks.setTimeout(callback3, 300);
|
||||
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
expect(callback3).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should treat -ve timeouts the same as a zero timeout", function() {
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
|
||||
// check that cb1 is called before cb2
|
||||
callback1.andCallFake(function() {
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
callbacks.setTimeout(callback2, -100);
|
||||
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
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() {
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should be immune to exceptions", function() {
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
callback1.andCallFake(function() {
|
||||
throw new Error("prepare to die");
|
||||
});
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
callbacks.setTimeout(callback1, 0);
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
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);
|
||||
callbacks.clearTimeout(k);
|
||||
tick(0);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not affect sooner timeouts", function() {
|
||||
var callback1 = jasmine.createSpy("callback1");
|
||||
var callback2 = jasmine.createSpy("callback2");
|
||||
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
var k = callbacks.setTimeout(callback2, 200);
|
||||
callbacks.clearTimeout(k);
|
||||
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
|
||||
tick(150);
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,40 @@ describe("RoomMember", function() {
|
||||
member = new RoomMember(roomId, userA);
|
||||
});
|
||||
|
||||
describe("getAvatarUrl", function() {
|
||||
var hsUrl = "https://my.home.server";
|
||||
|
||||
it("should return the URL from m.room.member preferentially", function() {
|
||||
member.events.member = utils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.member",
|
||||
skey: userA,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
membership: "join",
|
||||
avatar_url: "mxc://flibble/wibble"
|
||||
}
|
||||
});
|
||||
var 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);
|
||||
});
|
||||
|
||||
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);
|
||||
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);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPowerLevelEvent", function() {
|
||||
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
|
||||
var event = utils.mkEvent({
|
||||
@@ -68,6 +102,37 @@ describe("RoomMember", function() {
|
||||
member.setPowerLevelEvent(event); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should honour power levels of zero.",
|
||||
function() {
|
||||
var event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": 0,
|
||||
}
|
||||
},
|
||||
event: true
|
||||
});
|
||||
var emitCount = 0;
|
||||
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
member.powerLevel = 1;
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(0);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(0);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTypingEvent", function() {
|
||||
@@ -167,6 +232,9 @@ describe("RoomMember", function() {
|
||||
}),
|
||||
joinEvent
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
}
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
|
||||
@@ -279,4 +279,157 @@ describe("RoomState", function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("should say members with power >=50 may send state with power level event " +
|
||||
"but no state default",
|
||||
function() {
|
||||
var 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;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should obey state_default",
|
||||
function() {
|
||||
var 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;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
events: {
|
||||
"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;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("should say any member may send events with no power level event",
|
||||
function() {
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should obey events_default",
|
||||
function() {
|
||||
var 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;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
|
||||
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
var powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
events: {
|
||||
"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;
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
|
||||
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userB)).toEqual(true);
|
||||
|
||||
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+931
-213
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,464 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var EventTimeline = sdk.EventTimeline;
|
||||
var TimelineWindow = sdk.TimelineWindow;
|
||||
var TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
|
||||
|
||||
var utils = require("../test-utils");
|
||||
|
||||
var ROOM_ID = "roomId";
|
||||
var USER_ID = "userId";
|
||||
|
||||
/*
|
||||
* create a timeline with a bunch (default 3) events.
|
||||
* baseIndex is 1 by default.
|
||||
*/
|
||||
function createTimeline(numEvents, baseIndex) {
|
||||
if (numEvents === undefined) { numEvents = 3; }
|
||||
if (baseIndex === undefined) { baseIndex = 1; }
|
||||
|
||||
// XXX: this is a horrid hack
|
||||
var timelineSet = { room: { roomId: ROOM_ID }};
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
|
||||
|
||||
var timeline = new EventTimeline(timelineSet);
|
||||
|
||||
// add the events after the baseIndex first
|
||||
addEventsToTimeline(timeline, numEvents - baseIndex, false);
|
||||
|
||||
// then add those before the baseIndex
|
||||
addEventsToTimeline(timeline, baseIndex, true);
|
||||
|
||||
expect(timeline.getBaseIndex()).toEqual(baseIndex);
|
||||
return timeline;
|
||||
}
|
||||
|
||||
function addEventsToTimeline(timeline, numEvents, atStart) {
|
||||
for (var i = 0; i < numEvents; i++) {
|
||||
timeline.addEvent(
|
||||
utils.mkMessage({
|
||||
room: ROOM_ID, user: USER_ID,
|
||||
event: true,
|
||||
}), atStart
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* create a pair of linked timelines
|
||||
*/
|
||||
function createLinkedTimelines() {
|
||||
var tl1 = createTimeline();
|
||||
var tl2 = createTimeline();
|
||||
tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS);
|
||||
tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS);
|
||||
return [tl1, tl2];
|
||||
}
|
||||
|
||||
|
||||
describe("TimelineIndex", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
});
|
||||
|
||||
describe("minIndex", function() {
|
||||
it("should return the min index relative to BaseIndex", function() {
|
||||
var timelineIndex = new TimelineIndex(createTimeline(), 0);
|
||||
expect(timelineIndex.minIndex()).toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxIndex", function() {
|
||||
it("should return the max index relative to BaseIndex", function() {
|
||||
var timelineIndex = new TimelineIndex(createTimeline(), 0);
|
||||
expect(timelineIndex.maxIndex()).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("advance", function() {
|
||||
it("should advance up to the end of the timeline", function() {
|
||||
var timelineIndex = new TimelineIndex(createTimeline(), 0);
|
||||
var result = timelineIndex.advance(3);
|
||||
expect(result).toEqual(2);
|
||||
expect(timelineIndex.index).toEqual(2);
|
||||
});
|
||||
|
||||
it("should retreat back to the start of the timeline", function() {
|
||||
var timelineIndex = new TimelineIndex(createTimeline(), 0);
|
||||
var result = timelineIndex.advance(-2);
|
||||
expect(result).toEqual(-1);
|
||||
expect(timelineIndex.index).toEqual(-1);
|
||||
});
|
||||
|
||||
it("should advance into the next timeline", function() {
|
||||
var timelines = createLinkedTimelines();
|
||||
var tl1 = timelines[0], tl2 = timelines[1];
|
||||
|
||||
// initialise the index pointing at the end of the first timeline
|
||||
var timelineIndex = new TimelineIndex(tl1, 2);
|
||||
|
||||
var result = timelineIndex.advance(1);
|
||||
expect(result).toEqual(1);
|
||||
expect(timelineIndex.timeline).toBe(tl2);
|
||||
|
||||
// we expect the index to be the zero (ie, the same as the
|
||||
// BaseIndex), because the BaseIndex points at the second event,
|
||||
// and we've advanced past the first.
|
||||
expect(timelineIndex.index).toEqual(0);
|
||||
});
|
||||
|
||||
it("should retreat into the previous timeline", function() {
|
||||
var timelines = createLinkedTimelines();
|
||||
var tl1 = timelines[0], tl2 = timelines[1];
|
||||
|
||||
// initialise the index pointing at the start of the second
|
||||
// timeline
|
||||
var timelineIndex = new TimelineIndex(tl2, -1);
|
||||
|
||||
var result = timelineIndex.advance(-1);
|
||||
expect(result).toEqual(-1);
|
||||
expect(timelineIndex.timeline).toBe(tl1);
|
||||
expect(timelineIndex.index).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("retreat", function() {
|
||||
it("should retreat up to the start of the timeline", function() {
|
||||
var timelineIndex = new TimelineIndex(createTimeline(), 0);
|
||||
var result = timelineIndex.retreat(2);
|
||||
expect(result).toEqual(1);
|
||||
expect(timelineIndex.index).toEqual(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("TimelineWindow", function() {
|
||||
/**
|
||||
* create a dummy eventTimelineSet and client, and a TimelineWindow
|
||||
* attached to them.
|
||||
*/
|
||||
var timelineSet, client;
|
||||
function createWindow(timeline, opts) {
|
||||
timelineSet = {};
|
||||
client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
return q(timeline);
|
||||
};
|
||||
|
||||
return new TimelineWindow(client, timelineSet, opts);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
});
|
||||
|
||||
describe("load", function() {
|
||||
it("should initialise from the live timeline", function(done) {
|
||||
var liveTimeline = createTimeline();
|
||||
var room = {};
|
||||
room.getLiveTimeline = function() { return liveTimeline; };
|
||||
|
||||
var timelineWindow = new TimelineWindow(undefined, room);
|
||||
timelineWindow.load(undefined, 2).then(function() {
|
||||
var expectedEvents = liveTimeline.getEvents().slice(1);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("should initialise from a specific event", function(done) {
|
||||
var timeline = createTimeline();
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
var timelineSet = {};
|
||||
var client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
expect(eventId0).toEqual(eventId);
|
||||
return q(timeline);
|
||||
};
|
||||
|
||||
var timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("canPaginate should return false until load has returned",
|
||||
function(done) {
|
||||
var timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
|
||||
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
var timelineSet = {};
|
||||
var client = {};
|
||||
|
||||
var timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(false);
|
||||
return q(timeline);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pagination", function() {
|
||||
it("should be able to advance across the initial timeline",
|
||||
function(done) {
|
||||
var timeline = createTimeline();
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
var timelineWindow = createWindow(timeline);
|
||||
|
||||
timelineWindow.load(eventId, 1).then(function() {
|
||||
var expectedEvents = [timeline.getEvents()[1]];
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
var expectedEvents = timeline.getEvents().slice(1);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(false);
|
||||
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(false);
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("should advance into next timeline", function(done) {
|
||||
var tls = createLinkedTimelines();
|
||||
var eventId = tls[0].getEvents()[1].getId();
|
||||
var timelineWindow = createWindow(tls[0], {windowLimit: 5});
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = tls[0].getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
var expectedEvents = tls[0].getEvents()
|
||||
.concat(tls[1].getEvents().slice(0, 2));
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
// the windowLimit should have made us drop an event from
|
||||
// tls[0]
|
||||
var expectedEvents = tls[0].getEvents().slice(1)
|
||||
.concat(tls[1].getEvents());
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(false);
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("should retreat into previous timeline", function(done) {
|
||||
var tls = createLinkedTimelines();
|
||||
var eventId = tls[1].getEvents()[1].getId();
|
||||
var timelineWindow = createWindow(tls[1], {windowLimit: 5});
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = tls[1].getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(false);
|
||||
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
var expectedEvents = tls[0].getEvents().slice(1, 3)
|
||||
.concat(tls[1].getEvents());
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(false);
|
||||
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
// the windowLimit should have made us drop an event from
|
||||
// tls[1]
|
||||
var expectedEvents = tls[0].getEvents()
|
||||
.concat(tls[1].getEvents().slice(0, 2));
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("should make forward pagination requests", function(done) {
|
||||
var timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
var timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
client.paginateEventTimeline = function(timeline0, opts) {
|
||||
expect(timeline0).toBe(timeline);
|
||||
expect(opts.backwards).toBe(false);
|
||||
expect(opts.limit).toEqual(2);
|
||||
|
||||
addEventsToTimeline(timeline, 3, false);
|
||||
return q(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
var expectedEvents = timeline.getEvents().slice(0, 5);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
|
||||
it("should make backward pagination requests", function(done) {
|
||||
var timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
|
||||
|
||||
var timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
client.paginateEventTimeline = function(timeline0, opts) {
|
||||
expect(timeline0).toBe(timeline);
|
||||
expect(opts.backwards).toBe(true);
|
||||
expect(opts.limit).toEqual(2);
|
||||
|
||||
addEventsToTimeline(timeline, 3, true);
|
||||
return q(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(false);
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(true);
|
||||
var expectedEvents = timeline.getEvents().slice(1, 6);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("should limit the number of unsuccessful pagination requests",
|
||||
function(done) {
|
||||
var timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
var timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
var paginateCount = 0;
|
||||
client.paginateEventTimeline = function(timeline0, opts) {
|
||||
expect(timeline0).toBe(timeline);
|
||||
expect(opts.backwards).toBe(false);
|
||||
expect(opts.limit).toEqual(2);
|
||||
paginateCount += 1;
|
||||
return q(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
expect(paginateCount).toEqual(3);
|
||||
var expectedEvents = timeline.getEvents().slice(0, 3);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -132,4 +132,132 @@ describe("utils", function() {
|
||||
}, ["foo"]); }).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepCompare", function() {
|
||||
var assert = {
|
||||
isTrue: function(x) { expect(x).toBe(true); },
|
||||
isFalse: function(x) { expect(x).toBe(false); },
|
||||
};
|
||||
|
||||
it("should handle primitives", function() {
|
||||
assert.isTrue(utils.deepCompare(null, null));
|
||||
assert.isFalse(utils.deepCompare(null, undefined));
|
||||
assert.isTrue(utils.deepCompare("hi", "hi"));
|
||||
assert.isTrue(utils.deepCompare(5, 5));
|
||||
assert.isFalse(utils.deepCompare(5, 10));
|
||||
});
|
||||
|
||||
it("should handle regexps", function() {
|
||||
assert.isTrue(utils.deepCompare(/abc/, /abc/));
|
||||
assert.isFalse(utils.deepCompare(/abc/, /123/));
|
||||
var r = /abc/;
|
||||
assert.isTrue(utils.deepCompare(r, r));
|
||||
});
|
||||
|
||||
it("should handle dates", function() {
|
||||
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
|
||||
new Date("2011-03-31")));
|
||||
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
|
||||
new Date("1970-01-01")));
|
||||
});
|
||||
|
||||
it("should handle arrays", function() {
|
||||
assert.isTrue(utils.deepCompare([], []));
|
||||
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
|
||||
});
|
||||
|
||||
it("should handle simple objects", function() {
|
||||
assert.isTrue(utils.deepCompare({}, {}));
|
||||
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 2}));
|
||||
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {b: 2, a: 1}));
|
||||
assert.isFalse(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 3}));
|
||||
|
||||
assert.isTrue(utils.deepCompare({1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}},
|
||||
{1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}},
|
||||
{1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 27}}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({}, null));
|
||||
assert.isFalse(utils.deepCompare({}, undefined));
|
||||
});
|
||||
|
||||
it("should handle functions", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
var func = function(x) { return true; };
|
||||
var func2 = function(x) { return true; };
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
assert.isFalse(utils.deepCompare(func, func2));
|
||||
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
|
||||
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("extend", function() {
|
||||
var SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
var target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
var merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
var source_orig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
var target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
var merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
var source_orig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
var source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var target = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,36 +5,7 @@ var Room = sdk.Room;
|
||||
var User = sdk.User;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
var keys = [];
|
||||
for (var k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) { continue; }
|
||||
keys.push(k);
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
}
|
||||
};
|
||||
var MockStorageApi = require("../MockStorageApi");
|
||||
|
||||
describe("WebStorageStore", function() {
|
||||
var store, room;
|
||||
|
||||
Reference in New Issue
Block a user