Compare commits
475 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 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 |
+1
-3
@@ -1,5 +1,3 @@
|
||||
# Keep this file in sync with .npmignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
@@ -8,7 +6,7 @@ coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
/dist
|
||||
|
||||
# version file and tarball created by 'npm pack'
|
||||
/git-revision.txt
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
# Keep this file in sync with .gitignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
|
||||
# tarball created by 'npm pack'.
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- node # Latest stable version of nodejs.
|
||||
+338
@@ -1,3 +1,341 @@
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
-5826
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-6490
File diff suppressed because it is too large
Load Diff
-2
File diff suppressed because one or more lines are too long
Vendored
-9900
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-10023
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-10864
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15420
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15577
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-15785
File diff suppressed because it is too large
Load Diff
-6
File diff suppressed because one or more lines are too long
Vendored
-15832
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-16305
File diff suppressed because it is too large
Load Diff
-8
File diff suppressed because one or more lines are too long
Vendored
-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
|
||||
+24
-4
@@ -1,13 +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
|
||||
npm test
|
||||
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml || echo "jshint finished with return code $?"
|
||||
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r lib/ -r spec/ > gjslint.log || echo "gjslint finished with return code $?"
|
||||
|
||||
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
|
||||
npm pack ||
|
||||
fail "npm pack finished with return code $?"
|
||||
|
||||
npm run gendoc || fail "JSDoc failed with code $?"
|
||||
|
||||
exit $RC
|
||||
|
||||
+1134
File diff suppressed because it is too large
Load Diff
+747
-1262
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
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");
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (payloadString === undefined) {
|
||||
throw new Error("payloadString undefined");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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,537 @@
|
||||
/*
|
||||
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 {module:client.Promise?} sharePromise If a share operation is in progress,
|
||||
* a promise which resolves when it is complete.
|
||||
*
|
||||
* @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.sharePromise = null;
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||
// that even if this is non-null, it may not be ready for use (in which
|
||||
// case _outboundSession.sharePromise will be non-null.)
|
||||
this._outboundSession = null;
|
||||
|
||||
// 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(room) {
|
||||
var self = this;
|
||||
|
||||
var session = this._outboundSession;
|
||||
|
||||
// need to make a brand new session?
|
||||
if (!session || session.needsRotation(self._sessionRotationPeriodMsgs,
|
||||
self._sessionRotationPeriodMs)
|
||||
) {
|
||||
this._outboundSession = session = this._prepareNewSession(room);
|
||||
}
|
||||
|
||||
if (session.sharePromise) {
|
||||
// key share already in progress
|
||||
return session.sharePromise;
|
||||
}
|
||||
|
||||
// no share in progress: check if we need to share with any devices
|
||||
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||
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];
|
||||
|
||||
if (deviceInfo.isBlocked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}).finally(function() {
|
||||
session.sharePromise = null;
|
||||
}).then(function() {
|
||||
return session;
|
||||
});
|
||||
|
||||
session.sharePromise = prom;
|
||||
return prom;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = function(room) {
|
||||
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._ensureOutboundSession(room).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;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
|
||||
var newMembership = member.membership;
|
||||
|
||||
if (newMembership === 'join' || newMembership === 'invite') {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise we assume the user is leaving, and start a new outbound session.
|
||||
console.log("Discarding outbound megolm session due to change in " +
|
||||
"membership of " + member.userId + " (" + oldMembership +
|
||||
"->" + newMembership + ")");
|
||||
|
||||
// this ensures that we will start a new session on the next message.
|
||||
this._outboundSession = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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("Unknown inbound session id");
|
||||
}
|
||||
|
||||
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;
|
||||
+1259
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
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) {
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
|
||||
var userRes = res.one_time_keys[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;
|
||||
@@ -18,6 +18,8 @@ limitations under the License.
|
||||
* @module filter
|
||||
*/
|
||||
|
||||
var FilterComponent = require("./filter-component");
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {string} keyNesting
|
||||
@@ -49,6 +51,14 @@ function Filter(userId, 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
|
||||
@@ -63,6 +73,88 @@ Filter.prototype.getDefinition = function() {
|
||||
*/
|
||||
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));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+307
-112
@@ -63,9 +63,11 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
* requests. This function must look like function(opts, callback){ ... }.
|
||||
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
|
||||
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
|
||||
* @param {bool} opts.onlyData True to return only the 'data' component of the
|
||||
* response (e.g. the parsed HTTP body). If false, requests will return status
|
||||
* codes and headers in addition to data. Default: false.
|
||||
*
|
||||
* @param {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
|
||||
@@ -99,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
|
||||
@@ -124,40 +191,60 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
// 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();
|
||||
upload.xhr = xhr;
|
||||
var cb = requestCallback(defer, callback, this.opts.onlyData);
|
||||
var cb = requestCallback(defer, opts.callback, this.opts.onlyData);
|
||||
|
||||
var timeout_fn = function() {
|
||||
xhr.abort();
|
||||
cb(new Error('Timeout'));
|
||||
};
|
||||
|
||||
// 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:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
var err;
|
||||
if (!xhr.responseText) {
|
||||
err = new Error('No response body.');
|
||||
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;
|
||||
}
|
||||
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.content_uri === undefined) {
|
||||
err = Error('Bad response');
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(undefined, xhr, resp.content_uri);
|
||||
cb(undefined, xhr, resp);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -168,58 +255,57 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
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,
|
||||
};
|
||||
upload.request = this.opts.request({
|
||||
uri: url,
|
||||
qs: queryParams,
|
||||
method: "POST"
|
||||
}, requestCallback(defer, callback, this.opts.onlyData));
|
||||
file.stream.pipe(this.opts.request);
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, "POST", "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/v1",
|
||||
headers: {"Content-Type": contentType},
|
||||
json: false,
|
||||
bodyParser: bodyParser,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.uploads.push(upload);
|
||||
|
||||
var self = this;
|
||||
upload.promise = defer.promise.finally(function() {
|
||||
var uploadsKeys = Object.keys(self.uploads);
|
||||
for (var i = 0; i < uploadsKeys.length; ++i) {
|
||||
if (self.uploads[uploadsKeys[i]].promise === defer.promise) {
|
||||
self.uploads.splice(uploadsKeys[i], 1);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
return upload.promise;
|
||||
|
||||
// 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) {
|
||||
var uploadsKeys = Object.keys(this.uploads);
|
||||
for (var i = 0; i < uploadsKeys.length; ++i) {
|
||||
var upload = this.uploads[uploadsKeys[i]];
|
||||
if (upload.promise === promise) {
|
||||
if (upload.xhr !== undefined) {
|
||||
upload.xhr.abort();
|
||||
return true;
|
||||
} else if (upload.request !== undefined) {
|
||||
upload.request.abort();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (promise.abort) {
|
||||
promise.abort();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -270,11 +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 {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
*
|
||||
* @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>
|
||||
@@ -282,18 +379,25 @@ 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, localTimeoutMs) {
|
||||
if (!queryParams) { queryParams = {}; }
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
var self = this;
|
||||
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, localTimeoutMs
|
||||
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;
|
||||
@@ -306,11 +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 {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
*
|
||||
* @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>
|
||||
@@ -318,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, localTimeoutMs) {
|
||||
return this.requestWithPrefix(
|
||||
callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
|
||||
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
|
||||
);
|
||||
},
|
||||
|
||||
@@ -346,16 +465,16 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* 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, localTimeoutMs) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
return this._request(
|
||||
callback, method, fullUri, queryParams, data, localTimeoutMs
|
||||
return this.authedRequest(
|
||||
callback, method, path, queryParams, data, {
|
||||
localTimeoutMs: localTimeoutMs,
|
||||
prefix: prefix,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -381,15 +500,16 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* 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) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
return this._request(
|
||||
callback, method, fullUri, queryParams, data, localTimeoutMs
|
||||
return this.request(
|
||||
callback, method, path, queryParams, data, {
|
||||
localTimeoutMs: localTimeoutMs,
|
||||
prefix: prefix,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -399,11 +519,22 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} uri The HTTP URI
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
*
|
||||
* @param {Object=} 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 {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
*
|
||||
* @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>
|
||||
@@ -412,12 +543,18 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
requestOtherUrl: function(callback, method, uri, queryParams, data,
|
||||
localTimeoutMs) {
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
opts) {
|
||||
if (opts === undefined || opts === null) {
|
||||
opts = {};
|
||||
} else if (isFinite(opts)) {
|
||||
// opts used to be localTimeoutMs
|
||||
opts = {
|
||||
localTimeoutMs: opts
|
||||
};
|
||||
}
|
||||
|
||||
return this._request(
|
||||
callback, method, uri, queryParams, data, localTimeoutMs
|
||||
callback, method, uri, queryParams, data, opts
|
||||
);
|
||||
},
|
||||
|
||||
@@ -440,27 +577,56 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
return this.opts.baseUrl + prefix + path + queryString;
|
||||
},
|
||||
|
||||
_request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
|
||||
/**
|
||||
* @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
|
||||
);
|
||||
}
|
||||
opts = opts || {};
|
||||
|
||||
var self = this;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
if (this.opts.extraParams) {
|
||||
for (var key in this.opts.extraParams) {
|
||||
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
|
||||
queryParams[key] = this.opts.extraParams[key];
|
||||
}
|
||||
}
|
||||
|
||||
var 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;
|
||||
@@ -485,8 +651,9 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
withCredentials: false,
|
||||
qs: queryParams,
|
||||
body: data,
|
||||
json: true,
|
||||
json: json,
|
||||
timeout: localTimeoutMs,
|
||||
headers: opts.headers || {},
|
||||
_matrix_opts: this.opts
|
||||
},
|
||||
function(err, response, body) {
|
||||
@@ -496,7 +663,15 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
return; // already rejected promise
|
||||
}
|
||||
}
|
||||
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
|
||||
|
||||
// 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);
|
||||
}
|
||||
);
|
||||
@@ -523,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) {
|
||||
|
||||
@@ -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;
|
||||
+38
-2
@@ -30,11 +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/event-timeline~EventTimeline} class. */
|
||||
module.exports.EventTimeline = require("./models/event-timeline");
|
||||
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
|
||||
/** 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");
|
||||
@@ -53,6 +55,9 @@ module.exports.ContentRepo = require("./content-repo");
|
||||
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.
|
||||
@@ -77,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.
|
||||
@@ -124,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,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
|
||||
*/
|
||||
@@ -25,16 +25,17 @@ var MatrixEvent = require("./event").MatrixEvent;
|
||||
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
||||
* doubly-linked list.
|
||||
*
|
||||
* @param {string} roomId the ID of the room where this timeline came from
|
||||
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
|
||||
* @constructor
|
||||
*/
|
||||
function EventTimeline(roomId) {
|
||||
this._roomId = roomId;
|
||||
function EventTimeline(eventTimelineSet) {
|
||||
this._eventTimelineSet = eventTimelineSet;
|
||||
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
|
||||
this._events = [];
|
||||
this._baseIndex = 0;
|
||||
this._startState = new RoomState(roomId);
|
||||
this._startState = new RoomState(this._roomId);
|
||||
this._startState.paginationToken = null;
|
||||
this._endState = new RoomState(roomId);
|
||||
this._endState = new RoomState(this._roomId);
|
||||
this._endState.paginationToken = null;
|
||||
|
||||
this._prevTimeline = null;
|
||||
@@ -43,7 +44,7 @@ function EventTimeline(roomId) {
|
||||
// this is used by client.js
|
||||
this._paginationRequests = {'b': null, 'f': null};
|
||||
|
||||
this._name = roomId + ":" + new Date().toISOString();
|
||||
this._name = this._roomId + ":" + new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +92,22 @@ 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.
|
||||
*
|
||||
@@ -217,23 +234,29 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
|
||||
EventTimeline.prototype.addEvent = function(event, atStart) {
|
||||
var stateContext = atStart ? this._startState : this._endState;
|
||||
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
// 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)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +274,14 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
|
||||
}
|
||||
};
|
||||
|
||||
function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
||||
/**
|
||||
* 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()
|
||||
@@ -270,7 +300,7 @@ function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
||||
event.forwardLooking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an event from the timeline
|
||||
|
||||
+174
-23
@@ -21,6 +21,10 @@ limitations under the License.
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var utils = require('../utils.js');
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
@@ -29,6 +33,10 @@ limitations under the License.
|
||||
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. */
|
||||
@@ -44,12 +52,14 @@ module.exports.EventStatus = {
|
||||
/**
|
||||
* 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.
|
||||
@@ -59,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.
|
||||
@@ -87,19 +106,22 @@ module.exports.MatrixEvent.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -121,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 || {};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -186,12 +211,95 @@ 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() {
|
||||
@@ -202,13 +310,25 @@ module.exports.MatrixEvent.prototype = {
|
||||
* Update the content of an event in the same way it would be by the server
|
||||
* if it were redacted before it was sent to us
|
||||
*
|
||||
* @param {Object} the raw event causing the redaction
|
||||
* @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;
|
||||
this.event.unsigned.redacted_because = redaction_event.event;
|
||||
|
||||
var key;
|
||||
for (key in this.event) {
|
||||
@@ -219,10 +339,11 @@ module.exports.MatrixEvent.prototype = {
|
||||
}
|
||||
|
||||
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
|
||||
for (key in this.event.content) {
|
||||
if (!this.event.content.hasOwnProperty(key)) { continue; }
|
||||
var content = this.getContent();
|
||||
for (key in content) {
|
||||
if (!content.hasOwnProperty(key)) { continue; }
|
||||
if (!keeps[key]) {
|
||||
delete this.event.content[key];
|
||||
delete content[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -235,7 +356,25 @@ module.exports.MatrixEvent.prototype = {
|
||||
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:
|
||||
@@ -276,3 +415,15 @@ var _REDACT_KEEP_CONTENT_MAP = {
|
||||
},
|
||||
'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
|
||||
*/
|
||||
|
||||
+18
-23
@@ -24,7 +24,10 @@ 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.
|
||||
@@ -77,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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,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;
|
||||
@@ -206,21 +212,6 @@ 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;
|
||||
}
|
||||
@@ -249,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;
|
||||
@@ -260,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;
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -25,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
|
||||
|
||||
+271
-497
File diff suppressed because it is too large
Load Diff
+60
-7
@@ -30,6 +30,7 @@ limitations under the License.
|
||||
* @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 {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
|
||||
@@ -44,7 +45,9 @@ limitations under the License.
|
||||
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;
|
||||
@@ -77,23 +80,39 @@ 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;
|
||||
eventsToFire.push("User.lastPresenceTs");
|
||||
|
||||
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;
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
this._updateModifiedTime();
|
||||
|
||||
for (var i = 0; i < eventsToFire.length; i++) {
|
||||
this.emit(eventsToFire[i], event, this);
|
||||
@@ -113,6 +132,17 @@ User.prototype.setDisplayName = function(name) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -157,6 +187,18 @@ User.prototype.getLastActiveTs = function() {
|
||||
*/
|
||||
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"
|
||||
@@ -168,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"
|
||||
|
||||
+52
-21
@@ -122,7 +122,7 @@ 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).filter(function(m) {
|
||||
@@ -152,11 +152,12 @@ 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; }
|
||||
|
||||
@@ -165,7 +166,7 @@ function PushProcessor(client) {
|
||||
// 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 ev.content.body.search(pat) > -1;
|
||||
return content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
var eventFulfillsDeviceCondition = function(cond, ev) {
|
||||
@@ -204,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; }
|
||||
@@ -215,7 +230,7 @@ function PushProcessor(client) {
|
||||
|
||||
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets || !rulesets.device) { return null; }
|
||||
if (ev.user_id == client.credentials.userId) { return null; }
|
||||
if (ev.getSender() == client.credentials.userId) { return null; }
|
||||
|
||||
var allDevNames = Object.keys(rulesets.device);
|
||||
for (var i = 0; i < allDevNames.length; ++i) {
|
||||
@@ -228,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) {
|
||||
@@ -258,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}
|
||||
@@ -276,3 +306,4 @@ function PushProcessor(client) {
|
||||
|
||||
/** The PushProcessor class. */
|
||||
module.exports = PushProcessor;
|
||||
|
||||
|
||||
+31
-1
@@ -43,6 +43,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
// filterId: Filter
|
||||
// }
|
||||
};
|
||||
this.accountData = {
|
||||
// type : content
|
||||
};
|
||||
this.localStorage = opts.localStorage;
|
||||
};
|
||||
|
||||
@@ -98,6 +101,11 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
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);
|
||||
@@ -244,7 +252,29 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
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) {},
|
||||
|
||||
@@ -62,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.
|
||||
@@ -104,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.
|
||||
@@ -124,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;
|
||||
@@ -133,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;
|
||||
}
|
||||
|
||||
+17
-1
@@ -162,7 +162,23 @@ StubStore.prototype = {
|
||||
*/
|
||||
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) {},
|
||||
|
||||
+170
-98
@@ -60,18 +60,25 @@ function debuglog() {
|
||||
function SyncApi(client, opts) {
|
||||
this.client = client;
|
||||
opts = opts || {};
|
||||
opts.initialSyncLimit = opts.initialSyncLimit || 8;
|
||||
opts.initialSyncLimit = (
|
||||
opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit
|
||||
);
|
||||
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
|
||||
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
this.opts = opts;
|
||||
this._peekRoomId = null;
|
||||
this._syncConnectionLost = false;
|
||||
this._currentSyncRequest = null;
|
||||
this._syncState = null;
|
||||
this._running = false;
|
||||
this._keepAliveTimer = null;
|
||||
this._connectionReturnedDefer = null;
|
||||
this._notifEvents = []; // accumulator of sync events in the current sync response
|
||||
|
||||
if (client.getNotifTimelineSet()) {
|
||||
reEmit(client, client.getNotifTimelineSet(),
|
||||
["Room.timeline", "Room.timelineReset"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +95,7 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
"Room.receipt", "Room.tags",
|
||||
"Room.timelineReset",
|
||||
"Room.localEchoUpdated",
|
||||
"Room.accountData",
|
||||
]);
|
||||
this._registerStateListeners(room);
|
||||
return room;
|
||||
@@ -147,7 +155,7 @@ SyncApi.prototype.syncLeftRooms = function() {
|
||||
timeout: 0 // don't want to block since this is a single isolated req
|
||||
};
|
||||
|
||||
return this._getOrCreateFilter(
|
||||
return client.getOrCreateFilter(
|
||||
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
|
||||
).then(function(filterId) {
|
||||
qps.filter = filterId;
|
||||
@@ -385,12 +393,23 @@ SyncApi.prototype.sync = function() {
|
||||
}
|
||||
|
||||
function getFilter() {
|
||||
var filter = new Filter(client.credentials.userId);
|
||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||
var filter;
|
||||
if (self.opts.filter) {
|
||||
filter = self.opts.filter;
|
||||
} else {
|
||||
filter = new Filter(client.credentials.userId);
|
||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||
}
|
||||
|
||||
self._getOrCreateFilter(
|
||||
client.getOrCreateFilter(
|
||||
getFilterName(client.credentials.userId), filter
|
||||
).done(function(filterId) {
|
||||
// reset the notifications timeline to prepare it to paginate from
|
||||
// the current point in time.
|
||||
// The right solution would be to tie /sync pagination tokens into
|
||||
// /notifications API somehow.
|
||||
client.resetNotifTimelineSet();
|
||||
|
||||
self._sync({ filterId: filterId });
|
||||
}, function(err) {
|
||||
self._startKeepAlives().done(function() {
|
||||
@@ -454,6 +473,7 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
self._connectionReturnedDefer = null;
|
||||
}
|
||||
this._updateSyncState("STOPPED");
|
||||
return;
|
||||
}
|
||||
|
||||
var filterId = syncOptions.filterId;
|
||||
@@ -477,7 +497,7 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
qps._cacheBuster = Date.now();
|
||||
}
|
||||
|
||||
if (self._syncConnectionLost) {
|
||||
if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
|
||||
// we think the connection is dead. If it comes back up, we won't know
|
||||
// about it till /sync returns. If the timeout= is high, this could
|
||||
// be a long time. Set it to 0 when doing retries so we don't have to wait
|
||||
@@ -493,38 +513,6 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
);
|
||||
|
||||
this._currentSyncRequest.done(function(data) {
|
||||
self._syncConnectionLost = false;
|
||||
// data looks like:
|
||||
// {
|
||||
// next_batch: $token,
|
||||
// presence: { events: [] },
|
||||
// rooms: {
|
||||
// invite: {
|
||||
// $roomid: {
|
||||
// invite_state: { events: [] }
|
||||
// }
|
||||
// },
|
||||
// join: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token, limited: true },
|
||||
// ephemeral: { events: [] },
|
||||
// account_data: { events: [] },
|
||||
// unread_notifications: {
|
||||
// highlight_count: 0,
|
||||
// notification_count: 0,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// leave: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// set the sync token NOW *before* processing the events. We do this so
|
||||
// if something barfs on an event we can skip it rather than constantly
|
||||
// polling with the same token.
|
||||
@@ -556,18 +544,25 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
self._connectionReturnedDefer.reject();
|
||||
self._connectionReturnedDefer = null;
|
||||
}
|
||||
self._updateSyncState("STOPPED");
|
||||
return;
|
||||
}
|
||||
console.error("/sync error %s", err);
|
||||
console.error(err);
|
||||
|
||||
debuglog("Starting keep-alive");
|
||||
self._syncConnectionLost = true;
|
||||
// Note that we do *not* mark the sync connection as
|
||||
// lost yet: we only do this if a keepalive poke
|
||||
// fails, since long lived HTTP connections will
|
||||
// go away sometimes and we shouldn't treat this as
|
||||
// erroneous. We set the state to 'reconnecting'
|
||||
// instead, so that clients can onserve this state
|
||||
// if they wish.
|
||||
self._startKeepAlives().done(function() {
|
||||
self._sync(syncOptions);
|
||||
});
|
||||
self._currentSyncRequest = null;
|
||||
self._updateSyncState("ERROR", { error: err });
|
||||
self._updateSyncState("RECONNECTING");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -583,6 +578,39 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
var client = this.client;
|
||||
var self = this;
|
||||
|
||||
// data looks like:
|
||||
// {
|
||||
// next_batch: $token,
|
||||
// presence: { events: [] },
|
||||
// account_data: { events: [] },
|
||||
// to_device: { events: [] },
|
||||
// rooms: {
|
||||
// invite: {
|
||||
// $roomid: {
|
||||
// invite_state: { events: [] }
|
||||
// }
|
||||
// },
|
||||
// join: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token, limited: true },
|
||||
// ephemeral: { events: [] },
|
||||
// account_data: { events: [] },
|
||||
// unread_notifications: {
|
||||
// highlight_count: 0,
|
||||
// notification_count: 0,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// leave: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
|
||||
// TODO-arch:
|
||||
// - Each event we pass through needs to be emitted via 'event', can we
|
||||
// do this in one place?
|
||||
@@ -605,7 +633,44 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
});
|
||||
}
|
||||
|
||||
// the returned json structure is abit crap, so make it into a
|
||||
// handle non-room account_data
|
||||
if (data.account_data && utils.isArray(data.account_data.events)) {
|
||||
var events = data.account_data.events.map(client.getEventMapper());
|
||||
client.store.storeAccountDataEvents(events);
|
||||
events.forEach(
|
||||
function(accountDataEvent) {
|
||||
if (accountDataEvent.getType() == 'm.push_rules') {
|
||||
client.pushRules = accountDataEvent.getContent();
|
||||
}
|
||||
client.emit("accountData", accountDataEvent);
|
||||
return accountDataEvent;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// handle to-device events
|
||||
if (data.to_device && utils.isArray(data.to_device.events)) {
|
||||
data.to_device.events
|
||||
.map(client.getEventMapper())
|
||||
.forEach(
|
||||
function(toDeviceEvent) {
|
||||
var content = toDeviceEvent.getContent();
|
||||
if (
|
||||
toDeviceEvent.getType() == "m.room.message" &&
|
||||
content.msgtype == "m.bad.encrypted"
|
||||
) {
|
||||
console.warn(
|
||||
"Unable to decrypt to-device event: " + content.body
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("toDeviceEvent", toDeviceEvent);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// the returned json structure is a bit crap, so make it into a
|
||||
// nicer form (array) after applying sanity to make sure we don't fail
|
||||
// on missing keys (on the off chance)
|
||||
var inviteRooms = [];
|
||||
@@ -624,6 +689,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
}
|
||||
}
|
||||
|
||||
this._notifEvents = [];
|
||||
|
||||
// Handle invites
|
||||
inviteRooms.forEach(function(inviteObj) {
|
||||
var room = inviteObj.room;
|
||||
@@ -709,6 +776,12 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
room.currentState.paginationToken = syncToken;
|
||||
self._deregisterStateListeners(room);
|
||||
room.resetLiveTimeline(joinObj.timeline.prev_batch);
|
||||
|
||||
// We have to assume any gap in any timeline is
|
||||
// reason to stop incrementally tracking notifications and
|
||||
// reset the timeline.
|
||||
client.resetNotifTimelineSet();
|
||||
|
||||
self._registerStateListeners(room);
|
||||
}
|
||||
}
|
||||
@@ -757,6 +830,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
timelineEvents.forEach(function(e) { client.emit("event", e); });
|
||||
accountDataEvents.forEach(function(e) { client.emit("event", e); });
|
||||
});
|
||||
|
||||
// update the notification timeline, if appropriate.
|
||||
// we only do this for live events, as otherwise we can't order them sanely
|
||||
// in the timeline relative to ones paginated in by /notifications.
|
||||
// XXX: we could fix this by making EventTimeline support chronological
|
||||
// ordering... but it doesn't, right now.
|
||||
if (syncToken && this._notifEvents.length) {
|
||||
this._notifEvents.sort(function(a, b) {
|
||||
return a.getTs() - b.getTs();
|
||||
});
|
||||
this._notifEvents.forEach(function(event) {
|
||||
client.getNotifTimelineSet().addLiveEvent(event);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -768,17 +855,21 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
*/
|
||||
SyncApi.prototype._startKeepAlives = function(delay) {
|
||||
if (delay === undefined) {
|
||||
delay = 5000 + Math.floor(Math.random() * 5000);
|
||||
delay = 2000 + Math.floor(Math.random() * 5000);
|
||||
}
|
||||
|
||||
if (this._keepAliveTimer !== null) {
|
||||
clearTimeout(this._keepAliveTimer);
|
||||
}
|
||||
var self = this;
|
||||
self._keepAliveTimer = setTimeout(
|
||||
self._pokeKeepAlive.bind(self),
|
||||
delay
|
||||
);
|
||||
if (delay > 0) {
|
||||
self._keepAliveTimer = setTimeout(
|
||||
self._pokeKeepAlive.bind(self),
|
||||
delay
|
||||
);
|
||||
} else {
|
||||
self._pokeKeepAlive();
|
||||
}
|
||||
if (!this._connectionReturnedDefer) {
|
||||
this._connectionReturnedDefer = q.defer();
|
||||
}
|
||||
@@ -798,9 +889,15 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
||||
}
|
||||
}
|
||||
|
||||
this.client._http.requestWithPrefix(
|
||||
undefined, "GET", "/_matrix/client/versions", undefined,
|
||||
undefined, "", 15 * 1000
|
||||
this.client._http.request(
|
||||
undefined, // callback
|
||||
"GET", "/_matrix/client/versions",
|
||||
undefined, // queryParams
|
||||
undefined, // data
|
||||
{
|
||||
prefix: '',
|
||||
localTimeoutMs: 15 * 1000,
|
||||
}
|
||||
).done(function() {
|
||||
success();
|
||||
}, function(err) {
|
||||
@@ -816,57 +913,17 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
||||
self._pokeKeepAlive.bind(self),
|
||||
5000 + Math.floor(Math.random() * 5000)
|
||||
);
|
||||
// A keepalive has failed, so we emit the
|
||||
// error state (whether or not this is the
|
||||
// first failure).
|
||||
// Note we do this after setting the timer:
|
||||
// this lets the unit tests advance the mock
|
||||
// clock when the get the error.
|
||||
self._updateSyncState("ERROR", { error: err });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} filterName
|
||||
* @param {Filter} filter
|
||||
* @return {Promise<String>} Filter ID
|
||||
*/
|
||||
SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
|
||||
var client = this.client;
|
||||
|
||||
var filterId = client.store.getFilterIdByName(filterName);
|
||||
var promise = q();
|
||||
|
||||
if (filterId) {
|
||||
// check that the existing filter matches our expectations
|
||||
promise = client.getFilter(client.credentials.userId,
|
||||
filterId, true
|
||||
).then(function(existingFilter) {
|
||||
var oldDef = existingFilter.getDefinition();
|
||||
var newDef = filter.getDefinition();
|
||||
|
||||
if (utils.deepCompare(oldDef, newDef)) {
|
||||
// super, just use that.
|
||||
debuglog("Using existing filter ID %s: %s", filterId,
|
||||
JSON.stringify(oldDef));
|
||||
return q(filterId);
|
||||
}
|
||||
debuglog("Existing filter ID %s: %s; new filter: %s",
|
||||
filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(function(existingId) {
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// create a new filter
|
||||
return client.createFilter(filter.getDefinition()
|
||||
).then(function(createdFilter) {
|
||||
debuglog("Created new filter ID %s: %s", createdFilter.filterId,
|
||||
JSON.stringify(createdFilter.getDefinition()));
|
||||
client.store.setFilterIdByName(filterName, createdFilter.filterId);
|
||||
return createdFilter.filterId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @return {Object[]}
|
||||
@@ -992,6 +1049,18 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
||||
// may make notifications appear which should have the right name.
|
||||
room.recalculate(this.client.credentials.userId);
|
||||
|
||||
// gather our notifications into this._notifEvents
|
||||
if (client.getNotifTimelineSet()) {
|
||||
for (var i = 0; i < timelineEventList.length; i++) {
|
||||
var pushActions = client.getPushActionsForEvent(timelineEventList[i]);
|
||||
if (pushActions && pushActions.notify &&
|
||||
pushActions.tweaks && pushActions.tweaks.highlight)
|
||||
{
|
||||
this._notifEvents.push(timelineEventList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// execute the timeline events, this will begin to diverge the current state
|
||||
// if the timeline has any state events in it.
|
||||
room.addLiveEvents(timelineEventList);
|
||||
@@ -1040,7 +1109,10 @@ SyncApi.prototype._onOnline = function() {
|
||||
|
||||
function createNewUser(client, userId) {
|
||||
var user = new User(userId);
|
||||
reEmit(client, user, ["User.avatarUrl", "User.displayName", "User.presence"]);
|
||||
reEmit(client, user, [
|
||||
"User.avatarUrl", "User.displayName", "User.presence",
|
||||
"User.currentlyActive", "User.lastPresenceTs"
|
||||
]);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
+10
-11
@@ -41,7 +41,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
* Construct a TimelineWindow.
|
||||
*
|
||||
* <p>This abstracts the separate timelines in a Matrix {@link
|
||||
* module:models/room~Room|Room} into a single iterable thing. It keeps track of
|
||||
* 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.
|
||||
*
|
||||
@@ -56,7 +56,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
||||
* requests.
|
||||
*
|
||||
* @param {Room} room The room to track
|
||||
* @param {EventTimelineSet} timelineSet The timelineSet to track
|
||||
*
|
||||
* @param {Object} [opts] Configuration options for this window
|
||||
*
|
||||
@@ -66,10 +66,10 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function TimelineWindow(client, room, opts) {
|
||||
function TimelineWindow(client, timelineSet, opts) {
|
||||
opts = opts || {};
|
||||
this._client = client;
|
||||
this._room = room;
|
||||
this._timelineSet = timelineSet;
|
||||
|
||||
// these will be TimelineIndex objects; they delineate the 'start' and
|
||||
// 'end' of the window.
|
||||
@@ -113,7 +113,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
// 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._room, 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++) {
|
||||
@@ -126,7 +126,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
});
|
||||
} else {
|
||||
// start with the most recent events
|
||||
var tl = this._room.getLiveTimeline();
|
||||
var tl = this._timelineSet.getLiveTimeline();
|
||||
initFields(tl, tl.getEvents().length);
|
||||
return q();
|
||||
}
|
||||
@@ -234,7 +234,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
// remove some events from the other end, if necessary
|
||||
var excess = this._eventCount - this._windowLimit;
|
||||
if (excess > 0) {
|
||||
this._unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
}
|
||||
return q(true);
|
||||
}
|
||||
@@ -254,6 +254,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
|
||||
debuglog("TimelineWindow: starting request");
|
||||
var self = this;
|
||||
|
||||
var prom = this._client.paginateEventTimeline(tl.timeline, {
|
||||
backwards: direction == EventTimeline.BACKWARDS,
|
||||
limit: size
|
||||
@@ -286,15 +287,13 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
|
||||
|
||||
/**
|
||||
* Trim the window to the windowlimit
|
||||
* 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.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
TimelineWindow.prototype._unpaginate = function(delta, startOfTimeline) {
|
||||
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
|
||||
var tl = startOfTimeline ? this._start : this._end;
|
||||
|
||||
// sanity-check the delta
|
||||
|
||||
+29
-1
@@ -204,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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -332,6 +333,33 @@ var deepCompare = module.exports.deepCompare = function(x, y) {
|
||||
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.
|
||||
|
||||
+128
-18
@@ -60,6 +60,12 @@ function MatrixCall(opts) {
|
||||
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. */
|
||||
@@ -144,6 +150,86 @@ MatrixCall.prototype.placeScreenSharingCall =
|
||||
_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
|
||||
@@ -180,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);
|
||||
}
|
||||
@@ -205,9 +293,11 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
@@ -414,16 +504,20 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
videoEl.autoplay = true;
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video element");
|
||||
videoEl.src = this.URL.createObjectURL(this.screenSharingStream);
|
||||
this.assignElement(videoEl,
|
||||
this.URL.createObjectURL(this.screenSharingStream),
|
||||
"localVideo");
|
||||
}
|
||||
else {
|
||||
videoEl.src = this.URL.createObjectURL(stream);
|
||||
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);
|
||||
}
|
||||
@@ -460,12 +554,14 @@ 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);
|
||||
}
|
||||
@@ -700,6 +796,7 @@ MatrixCall.prototype._onAddStream = function(event) {
|
||||
if (s.getVideoTracks().length > 0) {
|
||||
this.type = 'video';
|
||||
this.remoteAVStream = s;
|
||||
this.remoteAStream = s;
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
this.remoteAStream = s;
|
||||
@@ -712,12 +809,20 @@ MatrixCall.prototype._onAddStream = function(event) {
|
||||
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);
|
||||
|
||||
if (this.type === 'video') {
|
||||
_tryPlayRemoteStream(this);
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
}
|
||||
else {
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
@@ -824,21 +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.getRemoteAudioElement().pause();
|
||||
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
|
||||
}
|
||||
self.getRemoteAudioElement().src = "";
|
||||
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;
|
||||
@@ -896,11 +1001,13 @@ 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()) {
|
||||
@@ -914,11 +1021,13 @@ var _tryPlayRemoteAudioStream = function(self) {
|
||||
if (self.getRemoteAudioElement() && self.remoteAStream) {
|
||||
var player = self.getRemoteAudioElement();
|
||||
player.autoplay = true;
|
||||
player.src = self.URL.createObjectURL(self.remoteAStream);
|
||||
self.assignElement(player,
|
||||
self.URL.createObjectURL(self.remoteAStream),
|
||||
"remoteAudio");
|
||||
setTimeout(function() {
|
||||
var ael = self.getRemoteAudioElement();
|
||||
if (ael.play) {
|
||||
ael.play();
|
||||
self.playElement(ael, "remoteAudio");
|
||||
}
|
||||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||||
if (self.webRtc.isOpenWebRTC()) {
|
||||
@@ -1102,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.
|
||||
|
||||
+31
-10
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.5.2",
|
||||
"version": "0.7.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
|
||||
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
|
||||
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
|
||||
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
|
||||
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
|
||||
"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/",
|
||||
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt",
|
||||
"version": "npm run release && git add dist/$npm_package_version"
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
@@ -22,19 +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",
|
||||
"uglifyjs": "^2.4.10"
|
||||
"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-1.3.0.tgz"
|
||||
}
|
||||
}
|
||||
|
||||
+90
-8
@@ -1,13 +1,18 @@
|
||||
#!/bin/sh
|
||||
#!/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
|
||||
# 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() {
|
||||
@@ -20,6 +25,13 @@ $USAGE
|
||||
EOF
|
||||
}
|
||||
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
changelog_file="CHANGELOG.md"
|
||||
@@ -47,11 +59,27 @@ if [ $# -ne 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
# see if the version has a hyphen in it. Crude,
|
||||
# but semver doesn't support postreleases so anything
|
||||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
if ! command -v update_changelog >/dev/null 2>&1; then
|
||||
echo "release.sh requires github-changelog-generator. Try:" >&2
|
||||
@@ -61,9 +89,16 @@ if [ -z "$skip_changelog" ]; then
|
||||
fi
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
|
||||
# If we're on any branch starting with 'release', we don't create
|
||||
# a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
echo "Using current branch ($curbranch) for release"
|
||||
rel_branch=$curbranch
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
@@ -81,7 +116,54 @@ set -x
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
echo "npm version"
|
||||
npm version "$release"
|
||||
# npm version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because github will do that, 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"
|
||||
|
||||
# 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"
|
||||
done
|
||||
fi
|
||||
|
||||
# push the release branch (github can't release from
|
||||
# a branch it doesn't have)
|
||||
git push origin "$rel_branch"
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
hub release create $hubflags $assets -m "$tag" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
@@ -90,7 +172,7 @@ if [ -z "$skip_jsdoc" ]; then
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -ar ".jsdoc/matrix-js-sdk/$release" .
|
||||
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
|
||||
@@ -104,8 +186,8 @@ git checkout master
|
||||
git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
|
||||
# push everything to github
|
||||
git push origin master "$rel_branch" "$tag"
|
||||
# push master and docs (if generated) to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
@@ -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,249 +1,769 @@
|
||||
"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);
|
||||
startClient(aliHttpBackend, aliClient);
|
||||
return aliHttpBackend.flush().then(function() {
|
||||
console.log("Ali client started");
|
||||
});
|
||||
}
|
||||
|
||||
function bobStartClient() {
|
||||
expectBobKeyUpload().catch(test_utils.failTest);
|
||||
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, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
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 syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: aliMessage
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,7 +121,8 @@ describe("getEventTimeline support", function() {
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||
.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
@@ -137,7 +138,8 @@ describe("getEventTimeline support", function() {
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||
.not.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
@@ -242,6 +244,7 @@ describe("MatrixClient event timelines", function() {
|
||||
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 {
|
||||
@@ -257,7 +260,7 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(room, "event1:bar").then(function(tl) {
|
||||
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]);
|
||||
@@ -274,6 +277,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
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: {
|
||||
@@ -291,7 +295,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
httpBackend.flush("/sync").then(function() {
|
||||
return client.getEventTimeline(room, EVENTS[0].event_id);
|
||||
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]);
|
||||
@@ -305,6 +309,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
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: {
|
||||
@@ -335,7 +340,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(room, EVENTS[2].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
@@ -354,6 +359,7 @@ describe("MatrixClient event timelines", function() {
|
||||
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.
|
||||
@@ -410,19 +416,19 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
var tl0, tl2, tl3;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl0 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[2].event_id);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl2 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[3].event_id);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl3 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[1].event_id);
|
||||
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);
|
||||
@@ -447,6 +453,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
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")
|
||||
@@ -460,7 +467,7 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
client.getEventTimeline(room, "event1"
|
||||
client.getEventTimeline(timelineSet, "event1"
|
||||
).then(function(tl) {
|
||||
// could do with a fail()
|
||||
expect(true).toBeFalsy();
|
||||
@@ -475,6 +482,7 @@ describe("MatrixClient event timelines", function() {
|
||||
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))
|
||||
@@ -503,7 +511,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
@@ -525,6 +533,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
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))
|
||||
@@ -553,7 +562,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
@@ -607,10 +616,11 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
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(room, 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);
|
||||
@@ -619,7 +629,7 @@ describe("MatrixClient event timelines", function() {
|
||||
// now let the sync complete, and check it again
|
||||
return httpBackend.flush("/sync", 1);
|
||||
}).then(function() {
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
expect(tl.getEvents()[1].event).toEqual(event);
|
||||
@@ -630,13 +640,14 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
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(room, event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (2)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
@@ -644,7 +655,7 @@ describe("MatrixClient event timelines", function() {
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (1)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
@@ -655,4 +666,70 @@ describe("MatrixClient event timelines", function() {
|
||||
}).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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,10 +6,11 @@ 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";
|
||||
|
||||
@@ -17,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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +37,117 @@ 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";
|
||||
@@ -173,4 +291,110 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,20 +66,23 @@ describe("MatrixClient retrying", function() {
|
||||
ev2 = tl[1];
|
||||
|
||||
expect(ev1.status).toEqual(EventStatus.SENDING);
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() { client.cancelPendingEvent(ev1); })
|
||||
.toThrow();
|
||||
// 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
|
||||
|
||||
// fail the first send
|
||||
httpBackend.when("PUT", "/send/m.room.message/")
|
||||
.respond(400);
|
||||
httpBackend.flush().then(function() {
|
||||
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
/*
|
||||
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');
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
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/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',
|
||||
});
|
||||
}
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ROOM_ID = "!room:id";
|
||||
|
||||
var testOlmAccount;
|
||||
var testSenderKey;
|
||||
var aliceTestClient;
|
||||
var testDeviceKeys;
|
||||
|
||||
beforeEach(function(done) {
|
||||
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;
|
||||
|
||||
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: '@bob:xyz',
|
||||
};
|
||||
var j = anotherjson.stringify(testDeviceKeys);
|
||||
var sig = testOlmAccount.sign(j);
|
||||
testDeviceKeys.signatures = {
|
||||
'@bob:xyz': {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
},
|
||||
};
|
||||
|
||||
return aliceTestClient.start().nodeify(done);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", function(done) {
|
||||
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) {
|
||||
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) {
|
||||
// establish an olm session with alice
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [olmEvent],
|
||||
},
|
||||
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: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
var inboundGroupSession;
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {
|
||||
'DEVICE_ID': testDeviceKeys,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
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 decrypted = JSON.parse(inboundGroupSession.decrypt(ct));
|
||||
|
||||
console.log('Decrypted received megolm message', decrypted);
|
||||
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) {
|
||||
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: '@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) {
|
||||
// establish an olm session with alice
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [olmEvent],
|
||||
},
|
||||
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: '@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': {
|
||||
'DEVICE_ID': testDeviceKeys,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
+58
-11
@@ -11,18 +11,17 @@ 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;
|
||||
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
|
||||
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(realReq);
|
||||
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);
|
||||
realReq.callback("aborted");
|
||||
req.callback("aborted");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,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.
|
||||
@@ -221,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.
|
||||
*/
|
||||
|
||||
+40
-10
@@ -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,
|
||||
sender: 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,7 +88,7 @@ module.exports.mkPresence = function(opts) {
|
||||
var event = {
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
type: "m.presence",
|
||||
sender: opts.user,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: {
|
||||
avatar_url: opts.url,
|
||||
displayname: opts.name,
|
||||
@@ -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));
|
||||
@@ -159,7 +159,16 @@ module.exports.mkMessage = function(opts) {
|
||||
* <p>This is useful for use with integration tests which use asyncronous
|
||||
* methods: it can be added as a 'catch' handler in a promise chain.
|
||||
*
|
||||
* @param {Error} error exception to be reported
|
||||
* @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) {
|
||||
@@ -168,6 +177,27 @@ module.exports.mkMessage = function(opts) {
|
||||
* }).catch(utils.failTest).done(done);
|
||||
* });
|
||||
*/
|
||||
module.exports.failTest = function(error) {
|
||||
expect(error.stack).toBe(null);
|
||||
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,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([1, 3, 0]);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,12 @@ describe("EventTimeline", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
timeline = new EventTimeline(roomId);
|
||||
|
||||
// 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() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -51,9 +51,10 @@ describe("MatrixClient", function() {
|
||||
// }
|
||||
// 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) {
|
||||
if (path === KEEP_ALIVE_PATH && accept_keepalives) {
|
||||
return q();
|
||||
}
|
||||
var next = httpLookups.shift();
|
||||
@@ -103,6 +104,7 @@ describe("MatrixClient", function() {
|
||||
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
|
||||
@@ -143,8 +145,10 @@ describe("MatrixClient", function() {
|
||||
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);
|
||||
@@ -204,6 +208,44 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -250,6 +292,8 @@ describe("MatrixClient", function() {
|
||||
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();
|
||||
@@ -329,17 +373,25 @@ describe("MatrixClient", function() {
|
||||
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(["ERROR", null]);
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["PREPARED", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
@@ -354,14 +406,19 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
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(["ERROR", "SYNCING"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
@@ -394,18 +451,23 @@ describe("MatrixClient", function() {
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
|
||||
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: "/sync", error: { errcode: "NONONONONO" }
|
||||
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,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() {
|
||||
|
||||
+23
-21
@@ -479,14 +479,14 @@ describe("Room", function() {
|
||||
it("should handle events in the same timeline", function() {
|
||||
room.addLiveEvents(events);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
.toBeLessThan(0);
|
||||
expect(room.compareEventOrdering(events[2].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
|
||||
events[1].getId()))
|
||||
.toBeGreaterThan(0);
|
||||
expect(room.compareEventOrdering(events[1].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
events[1].getId()))
|
||||
.toEqual(0);
|
||||
});
|
||||
|
||||
@@ -498,11 +498,11 @@ describe("Room", function() {
|
||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||
room.addLiveEvents([events[1]]);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
.toBeLessThan(0);
|
||||
expect(room.compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
.toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -512,24 +512,26 @@ describe("Room", function() {
|
||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||
room.addLiveEvents([events[1]]);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
.toBe(null);
|
||||
expect(room.compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
.toBe(null);
|
||||
});
|
||||
|
||||
it("should return null for unknown events", function() {
|
||||
room.addLiveEvents(events);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(), "xxx"))
|
||||
.toBe(null);
|
||||
expect(room.compareEventOrdering("xxx", events[0].getId()))
|
||||
.toBe(null);
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[0].getId()))
|
||||
.toBe(0);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering(events[0].getId(), "xxx"))
|
||||
.toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering("xxx", events[0].getId()))
|
||||
.toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering(events[0].getId(), events[0].getId()))
|
||||
.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ function createTimeline(numEvents, baseIndex) {
|
||||
if (numEvents === undefined) { numEvents = 3; }
|
||||
if (baseIndex === undefined) { baseIndex = 1; }
|
||||
|
||||
var timeline = new EventTimeline(ROOM_ID);
|
||||
// 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);
|
||||
@@ -133,19 +137,19 @@ describe("TimelineIndex", function() {
|
||||
|
||||
describe("TimelineWindow", function() {
|
||||
/**
|
||||
* create a dummy room and client, and a TimelineWindow
|
||||
* create a dummy eventTimelineSet and client, and a TimelineWindow
|
||||
* attached to them.
|
||||
*/
|
||||
var room, client;
|
||||
var timelineSet, client;
|
||||
function createWindow(timeline, opts) {
|
||||
room = {};
|
||||
timelineSet = {};
|
||||
client = {};
|
||||
client.getEventTimeline = function(room0, eventId0) {
|
||||
expect(room0).toBe(room);
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
return q(timeline);
|
||||
};
|
||||
|
||||
return new TimelineWindow(client, room, opts);
|
||||
return new TimelineWindow(client, timelineSet, opts);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -169,15 +173,15 @@ describe("TimelineWindow", function() {
|
||||
var timeline = createTimeline();
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
var room = {};
|
||||
var timelineSet = {};
|
||||
var client = {};
|
||||
client.getEventTimeline = function(room0, eventId0) {
|
||||
expect(room0).toBe(room);
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
expect(eventId0).toEqual(eventId);
|
||||
return q(timeline);
|
||||
};
|
||||
|
||||
var timelineWindow = new TimelineWindow(client, room);
|
||||
var timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
@@ -192,12 +196,12 @@ describe("TimelineWindow", function() {
|
||||
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
var room = {};
|
||||
var timelineSet = {};
|
||||
var client = {};
|
||||
|
||||
var timelineWindow = new TimelineWindow(client, room);
|
||||
var timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
|
||||
client.getEventTimeline = function(room0, eventId0) {
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
|
||||
@@ -200,4 +200,64 @@ describe("utils", function() {
|
||||
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