Compare commits

...

1101 Commits

Author SHA1 Message Date
David Baker 7d5c107fb8 v0.8.3 2017-09-20 15:04:11 +01:00
David Baker d855a6ea0f Prepare changelog for v0.8.3 2017-09-20 15:04:10 +01:00
David Baker 464f84d8cd v0.8.3-rc.1 2017-09-19 10:44:58 +01:00
David Baker fe0ee6402a Prepare changelog for v0.8.3-rc.1 2017-09-19 10:44:57 +01:00
David Baker 068939f790 APIs for flair (#542) 2017-09-18 14:44:40 +01:00
Matthew Hodgson 35f48d1c8e Merge pull request #526 from t3chguy/allow_trailing_slash
consume trailing slash when creating Matrix Client in HS and IS urls
2017-09-17 22:53:48 +01:00
Matthew Hodgson 52adde2501 Merge pull request #539 from turt2live/travis/ignored_users
Add ignore users API
2017-09-17 21:47:03 +01:00
David Baker 0ddc4eceaf Merge pull request #540 from matrix-org/dbkr/jsdoc_355
Upgrade to jsdoc 3.5.5
2017-09-15 09:19:21 +01:00
turt2live b0ab8c750d Add isUserIgnored convenience method
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 19:47:16 -06:00
Travis Ralston b17dd8351f Make linter happy
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 19:24:18 -06:00
Travis Ralston 0ceb8d159a Generate the ignored users event content correctly
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 16:06:55 -06:00
David Baker 402b943ddb Upgrade to jsdoc 3.5.5
Because of https://github.com/jsdoc3/jsdoc/issues/1438
2017-09-14 22:33:10 +01:00
Travis Ralston be55451c90 Add ignore users API
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 12:55:48 -06:00
Matthew Hodgson c51c1a2ae6 Merge pull request #538 from matrix-org/dbkr/reemit_use_fewer_closures
Make re-emitting events much more memory efficient
2017-09-13 12:51:26 +01:00
David Baker 845c796b96 Make re-emitting events much more memory efficient
The previous impl bluntly created a closure for every event type
and source emitter we set up a re-emit for. We can do much better
than this fairly easily by having one bound handler for each event
name and moving it into a class so we have one emitter per target,
since 99% of the time the target is the client object.
2017-09-13 11:55:03 +01:00
David Baker b0918ef293 Merge pull request #536 from matrix-org/dbkr/only_reemit_events_when_needed
Only re-emit events from Event objects if needed
2017-09-12 19:26:58 +01:00
David Baker 102572b088 Empty commit to get Travis to re-test 2017-09-12 17:37:16 +01:00
David Baker 63076e77f5 Only re-emit events from Event objects if needed
The only event and Event emits is 'Event.decrypted', so don't
bother to add listeners if the event isn't encrypted.
2017-09-12 17:09:01 +01:00
Richard van der Hoff 8e48ee5f66 Merge pull request #535 from matrix-org/rav/devicelist_bits
Handle 'left' users in the deviceList mananagement
2017-09-08 16:21:58 +01:00
Richard van der Hoff 1a55f550c0 Handle 'left' users in the deviceList mananagement
When we no longer share any rooms with a given user, the server will stop
sending us updates on their device list, and will (once synapse is updated)
send us a notification of that fact via the 'left' field in the device_lists
field in /sync, or the response from /keys/changes.
2017-09-08 15:40:26 +01:00
Richard van der Hoff ae8fc64394 Do /keys/changes before second /sync
This will avoid races between /keys/changes and /syncs.
2017-09-08 15:40:26 +01:00
David Baker 5e8e56caf9 Merge pull request #534 from matrix-org/rav/factor_out_device_tests
Factor out devicelist integration tests to a separate file
2017-09-08 13:51:11 +01:00
David Baker c075c161c2 Merge pull request #533 from matrix-org/rav/refactor_sync
Refactor sync._sync as an async function
2017-09-08 13:51:05 +01:00
Richard van der Hoff 237a553d15 sync: s/self/this/ 2017-09-08 12:07:19 +01:00
Richard van der Hoff ca8674e0de Factor out devicelist integration tests to a separate file
There's a fuzzy line between the megolm tests and the devicelist ones, but
since I want to add more tests for devicelists, we might as well put the ones
which are definitely about devicelists in their own file
2017-09-08 11:54:16 +01:00
Richard van der Hoff 0511a1172f Refactor sync._sync as an async function
This stuff is much more clearly expressed as an async function than as a
promise chain, and I'm about to add more stuff to the chain, so let's refactor
now.

There is plenty more that could be done here (self -> this, for instance), but
I've gone for keeping the diff minimal.
2017-09-08 11:53:10 +01:00
David Baker e07b304914 Merge pull request #532 from matrix-org/rav/eslint_es6
Add es6 to eslint environments
2017-09-07 10:33:52 +01:00
Richard van der Hoff 17364e72ec Add es6 to eslint environments 2017-09-07 10:29:27 +01:00
David Baker 22b213ae26 v0.8.2 2017-08-24 14:45:24 +01:00
David Baker 7d5936a9e9 Prepare changelog for v0.8.2 2017-08-24 14:45:24 +01:00
David Baker ab8f466f53 Merge pull request #530 from matrix-org/rav/fix_encrypted_calls
Handle m.call.* events which are decrypted asynchronously
2017-08-24 14:09:12 +01:00
David Baker 201177e7f0 Merge pull request #529 from matrix-org/dbkr/event_object_reemit
Re-emit events from, er, Event objects
2017-08-24 13:35:30 +01:00
Richard van der Hoff ec5f9a2892 Handle m.call.* events which are decrypted asynchronously
Handle the case where received m.call.* events are not decrypted at the point
of the 'event' notification by adding an 'Event.decrypted' listener for them.
2017-08-24 13:35:02 +01:00
Richard van der Hoff ee5b8748b5 Add MatrixEvent.isDecryptionFailure() 2017-08-24 13:35:02 +01:00
David Baker 8d04f8b8b5 Re-emit events from, er, Event objects
We do create Events in more places, but this is probably the only
place that matters since the only event is 'decrypted' which won't
fire for, eg. events we send.
2017-08-24 11:29:48 +01:00
David Baker 033babfbfc Groups: Sync Stream, Accept Invite & Leave (#528)
* WIP support for reading groups from sync stream

Only does invites currently

* More support for parsing groups in the sync stream

* Fix jsdoc
2017-08-24 10:24:24 +01:00
David Baker 15b77861ea v0.8.1 2017-08-23 15:51:31 +01:00
David Baker c4721850ce Prepare changelog for v0.8.1 2017-08-23 15:51:31 +01:00
David Baker b325aad5c9 v0.8.1-rc.1 2017-08-22 18:40:29 +01:00
David Baker 92e616f18e Prepare changelog for v0.8.1-rc.1 2017-08-22 18:40:29 +01:00
David Baker f7fee29c76 Merge pull request #527 from matrix-org/rav/fix_interactive_auth_error_handling
Fix error handling in interactive-auth
2017-08-21 16:43:19 +01:00
Richard van der Hoff eccea7411f Fix error handling in interactive-auth
Now that we are using bluebird, `defer.reject` is not implicitly bound, so we
need to call it properly rather than just passing it into the catch handler.

This fixes an error:

   promise.js:711 Uncaught TypeError: Cannot read property 'promise' of undefined
2017-08-21 16:31:42 +01:00
Michael Telatynski 01f93e0970 consume trailing slash when creating Matrix Client in HS and IS urls
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-08-19 11:00:46 +01:00
Richard van der Hoff 2d82a7bc2e Merge pull request #524 from matrix-org/rav/async_crypto/1
Make lots of OlmDevice asynchronous
2017-08-17 13:16:57 +01:00
Richard van der Hoff ca91fba071 Crypto test: Bump the timeout when waiting for Ali to claim keys
This failed a test, so let's just bump up the timeout a bit more.
2017-08-16 21:02:52 +01:00
Richard van der Hoff 9f2fce4d87 Try harder to wait for megolm decryption
Ok, this *really* ought to fix the racy test.
2017-08-16 19:01:47 +01:00
David Baker e1942267c5 Add API to invite & remove users from groups (#525)
* Add API invite & remove users from groups

* lint
2017-08-16 14:45:15 +01:00
Richard van der Hoff 12212409c7 Hopefully, fix racy megolm test
I couldn't repro the failure locally, but this looks like it should fix the
test failures.
2017-08-15 19:09:50 +01:00
Richard van der Hoff e5565c6bdb review comments 2017-08-15 18:34:04 +01:00
Richard van der Hoff f00558d840 Merge remote-tracking branch 'origin/develop' into rav/async_crypto/1 2017-08-15 18:31:02 +01:00
Richard van der Hoff da0dc5ed11 Merge pull request #523 from matrix-org/rav/fix_decryption_race
Make crypto.decryptMessage return decryption results
2017-08-15 18:07:33 +01:00
Luke Barnard b417492fad v0.8.0 2017-08-15 17:11:05 +01:00
Luke Barnard d3ee532624 Prepare changelog for v0.8.0 2017-08-15 17:11:05 +01:00
Richard van der Hoff e8be38ce5a Add delays to tests to wait for things to decrypt
Prepare for some refactoring which will add an extra tick to decryption by
adding some `awaitDecryption` calls in the integration tests.
2017-08-14 18:39:45 +01:00
Richard van der Hoff 38c9a05a0c Make Event.attemptDecryption return useful promises
Even if a decryption attempt is in progress, return a promise which blocks
until the attempt is complete.
2017-08-14 18:38:29 +01:00
Richard van der Hoff 110bd332f4 Make OlmDevice.exportInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 8a0f73bf81 Make some OlmDevice megolm methods async
* OlmDevice.hasInboundSessionKeys
* OlmDevice.getInboundGroupSessionKey

The latter means that MegolmDecryption.shareKeysWithDevice takes longer before
it sends out the keyshare, so means the unit test needed an update
2017-08-10 15:01:56 +01:00
Richard van der Hoff 337c9cbea3 Make OlmDevice.decryptGroupMessage async 2017-08-10 15:01:56 +01:00
Richard van der Hoff cfd61096d9 Make OlmDevice.importInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 2894e253a2 Make OlmDevice.addInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff e52985e082 Olm session creation async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 7d2bc12bb7 Make OlmDevice key generation async
* OlmDevice.generateOneTimeKeys becomes async
* Stash maxOneTimeKeys at init so that maxNumberOfOneTimeKeys can remain sync
2017-08-10 15:01:56 +01:00
Richard van der Hoff a5f397b26d OlmDevice.oneTimeKeys async
* OlmDevice.getOneTimeKeys
* OlmDevice.markKeysAsPublished
2017-08-10 15:01:56 +01:00
Richard van der Hoff 5b93d5210e Make OlmDevice.sign async 2017-08-10 15:01:56 +01:00
Richard van der Hoff e943a6e09c Make OlmDevice olmSession methods asynchronous
* OlmDevice.encryptMessage
* OlmDevice.decryptMessage
* OlmDevice.matchesSession
2017-08-10 15:01:56 +01:00
Richard van der Hoff 8f527a6212 make session tracking methods in OlmDevice async
* OlmDevice.getSessionIdsForDevice
* OlmDevice.getSessionIdForDevice
* OlmDevice.getSessionInfoForDevice
2017-08-10 15:01:56 +01:00
Richard van der Hoff f2f8ad6b65 Make OlmDevice initialisation asynchronous
Add an asynchronous `init` method to OlmDevice which initialises the OlmAccount.
2017-08-10 15:01:56 +01:00
Richard van der Hoff c870930bc0 Add delays to tests to wait for things to decrypt
Prepare for some refactoring which will add an extra tick to decryption by
adding some `awaitDecryption` calls in the integration tests.
2017-08-10 15:01:56 +01:00
Richard van der Hoff b26b1caa86 fix jsdoc 2017-08-10 14:56:42 +01:00
Richard van der Hoff 6613ee6b0d Make crypto.decryptMessage return decryption results
... instead of having it call event.setClearData.

The main advantage of this is that it fixes a race condition, wherein apps
could see `event.isDecrypting()` to be true, but in fact the event had been
decrypted (and there was no `Event.decrypted` event on its way).

We're also fixing another race, wherein if the first attempt to decrypt failed,
a call to `attemptDecryption` would race against the first call and a second
attempt to decrypt would never happen.

This also gives a cleaner interface to MatrixEvent, at the expense of making
the `megolm` unit test a bit more hoop-jumpy.
2017-08-10 13:05:35 +01:00
Richard van der Hoff 9550bca099 Megolm: remove redundant requestKeysOnFail
We now *always* requestKeysOnFail, so this was dead code which we can remove.
2017-08-10 13:00:27 +01:00
Richard van der Hoff 92a75aaa08 Merge pull request #521 from matrix-org/rav/async_crypto/olmlib
Make bits of `olmlib` asynchronous
2017-08-10 11:07:42 +01:00
Richard van der Hoff 906bf88450 Merge remote-tracking branch 'origin/develop' into rav/async_crypto/olmlib 2017-08-09 18:11:48 +01:00
Richard van der Hoff d7157843f4 Merge pull request #520 from matrix-org/rav/async_crypto/devicelist
Make some of DeviceList asynchronous
2017-08-09 18:02:53 +01:00
Richard van der Hoff d317c1ff08 Merge pull request #519 from matrix-org/rav/async_crypto/algorithms
Make methods in crypto/algorithms async
2017-08-09 18:02:30 +01:00
Richard van der Hoff ef889963d9 Rewrite olmlib.ensureOlmSessionsForDevices as async
This is non-functional. It just looks a lot prettier.
2017-08-09 10:46:19 +01:00
Richard van der Hoff a2d7b221ee Make olmlib.verifySignature async 2017-08-09 10:46:18 +01:00
Richard van der Hoff aff32afefa Make olmlib.encryptMessageForDevice async 2017-08-09 10:46:18 +01:00
Richard van der Hoff 0943e0c60f Make some of DeviceList asynchronous
* DeviceList._updateStoredDeviceKeysForUser
 * DeviceList._processQueryResponseForUser
 * DeviceList._storeDeviceKeys
2017-08-08 18:28:53 +01:00
Richard van der Hoff 18f75ec61c make algorithm.hasKeysForKeyRequest async 2017-08-08 18:26:31 +01:00
Richard van der Hoff d821082843 Prepare megolm.js for async
Make internal methods of megolm.js ready for asynchronous olmdevice
2017-08-08 18:25:16 +01:00
Richard van der Hoff 366a88cc5c make olm._decryptMessage asynchronous 2017-08-08 18:22:55 +01:00
David Baker 951df61aa0 Merge pull request #518 from matrix-org/rav/no_plain_messages_in_e2e_room
Avoid sending unencrypted messages in e2e room
2017-08-08 12:48:49 +01:00
Richard van der Hoff 3e79575602 Avoid sending unencrypted messages in e2e room
Reshuffle the logic for determining whether to encrypt a message so that it can
run independently of whether our app actually supports e2e - and then throw an
error if it looks like we should be encrypting but don't support it.

This seems a preferable situation to just falling back to plain text if we get
a dodgy build.
2017-08-08 12:29:26 +01:00
David Baker 92e24777c0 Merge pull request #517 from matrix-org/rav/test_robustness
Make tests wait for syncs to happen
2017-08-08 11:27:13 +01:00
Richard van der Hoff ab8d06bb86 Make tests wait for syncs to happen
Add lots of calls to `syncPromise` to cope with the fact that sync responses
are now handled asynchronously, which makes them prone to races otherwise.

Also a quick sanity-check in crypto to make one of the test failures less
cryptic.
2017-08-08 10:58:19 +01:00
Richard van der Hoff 8563dd5860 Merge pull request #510 from matrix-org/rav/async_crypto/crypto_methods
Make a load of methods in the 'Crypto' module asynchronous
2017-08-07 17:14:07 +01:00
Richard van der Hoff 1f6153fa82 Make Crypto.setRoomEncryption asynchronous 2017-08-07 17:13:09 +01:00
Richard van der Hoff 23d66b9746 Make Crypto.setDeviceVerification async 2017-08-07 17:13:09 +01:00
Richard van der Hoff 25ccd6bc6d Make Crypto._processReceivedRoomKeyRequests async
This is slightly complicated by the fact that it's initiated from a synchronous
process which we don't want to make async (processing the /sync response) and
we want to avoid racing two copies of the processor.
2017-08-07 17:13:09 +01:00
Richard van der Hoff 14ad32bcd2 Make Crypto.importRoomKeys async 2017-08-07 17:13:09 +01:00
Richard van der Hoff 9ab9b9d75a Make crypto._signObject async 2017-08-07 17:13:09 +01:00
Richard van der Hoff b7a3c4557f Make crypto.getOlmSessionsForUser async
This is snever used anywhere (it's mosdly for debug), so this is trivial
2017-08-07 17:13:08 +01:00
David Baker bdb90b4b33 Merge pull request #515 from matrix-org/luke/fix-null-rawDisplayName
Set `rawDisplayName` to `userId` if membership has `displayname=null`
2017-07-27 16:23:31 +01:00
Luke Barnard 85d0935e97 Set rawDisplayName to userId if membership has displayname=null
This mirrors the behaviour of `name` such that the default is always `userId` but if the membership event has a `displayname`, we use that.
2017-07-27 16:15:32 +01:00
Richard van der Hoff 86ad75d27b Merge pull request #508 from matrix-org/rav/async_crypto_event_handling
Refactor handling of crypto events for async
2017-07-26 09:07:59 +01:00
Richard van der Hoff b40473aa3b Fix broken event-emitter test
We need to wait for two syncs, not just one, here.
2017-07-26 07:27:08 +01:00
Richard van der Hoff 3bd5ffc5cd Fix broken crypto test
Now that sync takes a bit longer to send out Event events, the encrypted events
have already been decrypted by the time the test sees them - so we no longer
need to await their decryption.
2017-07-26 07:20:02 +01:00
Richard van der Hoff 10aafd3738 Merge branch 'develop' into rav/async_crypto_event_handling 2017-07-26 07:11:48 +01:00
Richard van der Hoff c055765bfe Merge pull request #509 from rav/async_crypto/async_decryption 2017-07-26 07:09:12 +01:00
Richard van der Hoff d8f486fc0d Verbose logging to see what's up with indexeddb (#514)
In an attempt to see why our tests sometimes time out, add a load of logging to
confirm exactly where it is happening.
2017-07-25 11:38:27 +01:00
Luke Barnard 06eea71a37 Add rawDisplayName to RoomMember (#513)
* Add rawDisplayName to RoomMember

This will at first be the `userId`, but when the members membership event is set, `rawDisplayName` will be assigned to the raw `displayname` of the membership event. This deliberately avoids disambiguation so that clients can disambiguate themselves (via a tooltip or otherwise).

* Clarify docs
2017-07-24 17:35:53 +01:00
Richard van der Hoff 3effb9ec29 Merge pull request #511 from matrix-org/rav/async_to_bluebird
Transform `async` functions to bluebird promises
2017-07-24 10:26:26 +01:00
David Baker 6603a2300b Merge pull request #512 from matrix-org/dbkr/groupview_edit
Add more group APIs
2017-07-24 10:00:06 +01:00
David Baker ed029fe348 More useful doc 2017-07-24 09:55:04 +01:00
Richard van der Hoff b497bc5eb9 Fix broken test: wait for sync to complete 2017-07-21 16:04:53 +01:00
Richard van der Hoff 8a4a1dfadf Transform async functions to bluebird promises
Now that we use transform-runtime instead of regenerator-runtime, we need to
use the async-to-bluebird transform to make sure that `async` functions get
transformed into bluebird promises.
2017-07-21 15:59:30 +01:00
Richard van der Hoff 8bbf14acbf Let event decryption be asynchronous
Once everything moves to indexeddb, it's going to require callbacks and the
like, so let's make the decrypt API asynchronous in preparation.
2017-07-21 14:41:22 +01:00
Richard van der Hoff 86f2c86440 Add MatrixEvent.attemptDecryption
... and use it from both MatrixClient and the megolm re-decryption code.

This will help us avoid races when decryption is asynchronous.
2017-07-21 14:41:22 +01:00
Richard van der Hoff cfb29f1339 Refactor handling of crypto events for async
We're going to need to handle m.room.crypto events asynchronously, so
restructure the way we do that.
2017-07-21 14:41:01 +01:00
Richard van der Hoff 63a28d8e34 Fix lint in /sync 2017-07-21 14:41:01 +01:00
Richard van der Hoff d37cbb10a5 Merge pull request #507 from matrix-org/rav/fix_racy_cancellation_test
Retrying test: wait for localEchoUpdated event
2017-07-21 13:19:55 +01:00
David Baker 055590c0c6 Add more group APIs 2017-07-21 11:13:27 +01:00
David Baker 6aaac45468 Merge pull request #504 from matrix-org/dbkr/fix_member_events_timeline_reset_2
Fix member events breaking on timeline reset, 2
2017-07-20 14:25:41 +01:00
David Baker 34adaae5af Add helpfully named variable for old timeline 2017-07-20 14:17:14 +01:00
Richard van der Hoff 9c6f004f7f Retrying test: wait for localEchoUpdated event
We need to wait for the js-sdk to have an opportunity to process the 400 from
the /send/ request before checking the event state
2017-07-20 13:12:21 +01:00
David Baker ff685e33d5 clarify comment 2017-07-20 11:00:50 +01:00
David Baker 2999603b28 more commentage 2017-07-20 10:43:47 +01:00
Richard van der Hoff 32d8f4b084 Fix jsdoc failure on async code (#506)
We need jsdoc 3.5 to support the async/await syntax.
2017-07-20 09:45:37 +01:00
Richard van der Hoff 0fb0c1b71b Use babel transform-runtime instead of regenerator-runtime (#505)
Attempting to use the regnerator-runtime ourselves led to a fight with riot-web
about whether `global.regeneratorRuntime` could be defined. By using the
transform-runtime plugin, references to `global.regeneratorRuntime` which are
created by the transform-regenerator plugin are turned into references to an
imported module, which works much better.

(The full tragic tale went as follows:

- riot-web uses transform-runtime, which adds an import of
  `regenerator-runtime` to index.js
- `regenerator-runtime`:
   - loads `regenerator-runtime/runtime`, which defines
     `global.regeneratorRuntime`
   - then clears the global property and returns the regeneratorRuntime object
     as the exported value from the module
- later, the js-sdk tried to import `regenerator-runtime/runtime`, which then
  did nothing because the module had already been loaded once.

For added fun, this only manifested itself when riot-web and js-sdk shared an
instance of the `regenerator-runtime` package, which happens on proper builds,
but not a normal development setup.)
2017-07-20 09:18:37 +01:00
Richard van der Hoff 2ac34dbab0 Merge pull request #503 from matrix-org/rav/async_crypto/public_api
Make bits of the js-sdk api asynchronous
2017-07-19 21:14:40 +01:00
Richard van der Hoff 986fb12543 Fix some typos in comments 2017-07-19 21:13:06 +01:00
krombel e686eb750f use device_one_time_keys_count transmitted by /sync (#493)
Where it is available, use the one_time_keys_count returned by /sync instead of polling the server for it.

This was added to synapse in matrix-org/synapse#2237.
2017-07-19 16:27:05 +01:00
David Baker 8ac15068ee more comments 2017-07-19 16:24:42 +01:00
David Baker 5e4cd6cf11 Add hopefully clearer comments 2017-07-19 16:20:40 +01:00
David Baker 39d694de8c No longer need RoomState 2017-07-19 14:58:18 +01:00
David Baker 342f5c01e0 Update tests for new resetLiveTimeline interface 2017-07-19 14:54:18 +01:00
David Baker f91293c6c5 Set the start state of the new timeline correctly 2017-07-19 14:52:27 +01:00
David Baker 1ce4977a70 get state events before we nuke the roomstate 2017-07-19 12:00:04 +01:00
David Baker f4b25b59e5 Lint 2017-07-19 11:56:58 +01:00
David Baker b33a47e253 Fix member events breaking on timeline reset, 2
Re-use the same RoomState from the old live timeline so we re-use
all the same member objects etc, so all the listeners stay attached
2017-07-19 11:49:20 +01:00
Richard van der Hoff ccd4d4263d Changelog: breaking e2e changes 2017-07-18 23:35:33 +01:00
Richard van der Hoff 2ff9a36eed Make a number of the crypto APIs asynchronous
Make the following return Promises:

* `MatrixClient.getStoredDevicesForUser`
* `MatrixClient.getStoredDevice`
* `MatrixClient.setDeviceVerified`
* `MatrixClient.setDeviceBlocked`
* `MatrixClient.setDeviceKnown`
* `MatrixClient.getEventSenderDeviceInfo`
* `MatrixClient.isEventSenderVerified`
* `MatrixClient.importRoomKeys`

Remove `listDeviceKeys` altogether: it's been deprecated for ages, and since
applications are going to have to be changed anyway, they might as well use its
replacement (`getStoredDevices`).
2017-07-18 23:35:33 +01:00
Richard van der Hoff d1e91cd702 Add MatrixClient.initCrypto
initialising the crypto layer needs to become asynchronous. Rather than making
`sdk.createClient` asynchronous, which would break every single app in the
world, add `initCrypto`, which will only break those attempting to do e2e (and
in a way which will fall back to only supporting unencrypted events).
2017-07-18 23:35:33 +01:00
Richard van der Hoff e2599071c5 Merge pull request #499 from matrix-org/rav/omfg_when_will_it_end
Yet more js-sdk test deflakification
2017-07-17 11:57:05 +01:00
Richard van der Hoff fc38b89aee Merge pull request #497 from matrix-org/rav/yet_another_flakey_test
Fix racy 'matrixclient retrying' test
2017-07-17 11:56:39 +01:00
Richard van der Hoff 5688286a79 Merge pull request #495 from matrix-org/rav/fix_key_requests_race
Fix spamming of key-share-requests
2017-07-17 11:56:05 +01:00
David Baker 7eb10ab7ac Merge pull request #500 from matrix-org/rav/upload_progress
Add progress handler to `uploadContent`
2017-07-14 17:18:41 +01:00
Richard van der Hoff 34b31865c5 Add progress handler to uploadContent
bluebird doesn't support promise progression (or rather, it does, but it's
heavily deprecated and doesn't use the same API as q), so replace the
(undocumented) promise progression on uploadFile with a callback.
2017-07-14 16:51:43 +01:00
Richard van der Hoff f1c5b632cc Deflake the matrixclient syncing tests (#498)
All of these tests were vulnerable to a race wherein we would flush the /sync
request, but the client had not yet processed the results before we checked
them. We can solve all of this by waiting for the client to emit a "sync"
event.
2017-07-14 16:09:28 +01:00
Richard van der Hoff 04ca0ac2b5 Give the megolm tests longer to complete
All that crypto stuff takes a while, so give it longer than 100ms.
2017-07-14 15:22:08 +01:00
Richard van der Hoff 8b2fdf3a75 Deflake megolm unit test
Waiting for 1ms isn't actually good enough. wait for the actual thing we are
actually waiting for.
2017-07-14 15:22:08 +01:00
Richard van der Hoff adca75b7d8 Deflake matrix-client-timeline tests
These guys do a flush("/sync"), without waiting for it to complete, and then in
the afterEach, check that the sync has been flushed, which it may not have
been. So we should make sure we wait for the flush.
2017-07-14 15:22:08 +01:00
Richard van der Hoff 504fa2a1d3 Fix racy 'matrixclient retrying' test
when a message send fails, the promise returned by `sendMessage` is
rejected. Until we switched to bluebird, the rejection was happily being
swallowed, but with bluebird, there's a better chance of the unhandled
rejection being caught by the runtime and mocha and failing the test.
2017-07-13 18:18:21 +01:00
Richard van der Hoff 266a062a5d Fix spamming of key-share-requests
Fixes a race in the memory-backed crypto store which meant that we would spam
out multiple key-share-requests for the same session.

(This didn't happen very often in practice, because normally we use the
indexeddb-backed store, which is race-free. Or at least, doesn't have this
race.)
2017-07-13 13:29:56 +01:00
Richard van der Hoff 652a9452c2 Merge pull request #490 from matrix-org/rav/bluebird
Switch matrix-js-sdk to bluebird
2017-07-12 23:34:40 +01:00
Richard van der Hoff 503b6ea6c8 Correct incorrect Promise() invocation
you're supposed to call Promise() as a constructor rather than a static
function.
2017-07-12 23:33:55 +01:00
Richard van der Hoff 547501ba81 Replace promise.inspect()
Bluebird promises don't have an `inspect()` method, but do have an
`isFulfilled()` and a `value()` method, so use them instead.
2017-07-12 23:33:55 +01:00
Richard van der Hoff cfffbc4a09 replace q method calls with bluebird ones
```
find src spec -name '*.js' |
    xargs perl -i -pe 's/q\.(all|defer|reject|delay|try)\(/Promise.$1(/'
```
2017-07-12 23:33:55 +01:00
Richard van der Hoff b58d84fba1 q.Promise -> Promise
```
find src spec -name '*.js' |
    xargs perl -i -pe 's/q\.Promise/Promise/'
```
2017-07-12 23:32:28 +01:00
Richard van der Hoff a5d3dd942e q(...) -> Promise.resolve
```
find src spec -name '*.js' |
    xargs perl -i -pe 's/\bq(\([^(]*\))/Promise.resolve$1/'
```
2017-07-12 23:32:28 +01:00
Richard van der Hoff b96062b6de replace imports of q with bluebird
```
find src spec -name '*.js' |
   xargs perl -i -pe 'if (/require\(.q.\)/) { $_ = "import Promise from '\''bluebird'\'';\n"; }'

find src spec -name '*.js' |
   xargs perl -i -pe 'if (/import q/) { $_ = "import Promise from '\''bluebird'\'';\n"; }'
```
2017-07-12 23:32:28 +01:00
Richard van der Hoff 04b71c11e1 Merge pull request #492 from matrix-org/rav/even_more_flakey_tests
Fix some more flakey tests
2017-07-12 18:17:16 +01:00
Richard van der Hoff 651baefb1d Remove redundant expectations
Apparently we weren't hitting these expected requests, so let's get rid of them.
2017-07-12 17:25:59 +01:00
Richard van der Hoff ff7e845615 remove redundant flushAllExpected
Turned out this flush was completely redundant
2017-07-12 17:23:11 +01:00
Richard van der Hoff f0612a1407 Fix some more flakey tests
switch a bunch of `flush()`es to `flushAllExpected()`s
2017-07-12 16:28:21 +01:00
Richard van der Hoff 83bd24adf8 More test deflakifying (#491)
Call `flushAllExpected()` from some more places. In a couple of places, we were
apparently calling `flush()` redundantly, so remove it altogether.
2017-07-12 14:05:39 +01:00
Richard van der Hoff b5a8e6bbdf Merge pull request #489 from t3chguy/t3chguy/test-crossplatform
make the npm test script windows-friendly
2017-07-12 13:49:43 +01:00
Michael Telatynski 9798fcf839 make the npm test script windows-friendly 2017-07-11 23:08:28 +01:00
David Baker 15556b6797 Merge pull request #488 from matrix-org/rav/deflakify_tests
Fix a bunch of races in the tests
2017-07-11 13:22:27 +01:00
Richard van der Hoff 0ca4d728d8 Fix a bunch of races in the tests
Once we switch to bluebird, suddenly a load of timing issues come out of the
woodwork. Basically, we need to try harder when flushing requests. Bump to
matrix-mock-request 1.1.0, which provides `flushAllExpected`, and waits for
requests to arrive when given a `numToFlush`; then use `flushAllExpected` in
various places to make the tests more resilient.
2017-07-11 12:09:21 +01:00
David Baker b2c7804032 Merge pull request #487 from matrix-org/rav/fix_bad_all_usage
Fix early return in MatrixClient.setGuestAccess
2017-07-11 11:21:45 +01:00
David Baker e091dc0294 Merge pull request #486 from matrix-org/rav/kill_failTest
Remove testUtils.failTest
2017-07-11 11:21:14 +01:00
Richard van der Hoff 3bfb4595cf Remove redundant calls to done
These tests which return a promise already don't need to call `done`.
2017-07-10 17:40:23 +01:00
Richard van der Hoff 8955d8de23 remove utils.failTest
this is no longer used, so kill it
2017-07-10 17:25:56 +01:00
Richard van der Hoff 1372b298bb kill off more utils.failTest refs
manual replacement of some more complicated utils.failTest usages with q.all()
invocations.
2017-07-10 17:25:48 +01:00
Richard van der Hoff 9558845e6e Fix early return in MatrixClient.setGuestAccess
(as well as a similar bug in the test suite)

Turns out that `q.all(a, b)` === `q.all([a])`, rather than `q.all([a,b])`: it
only waits for the *first* promise - which means that `client.setGuestAccess`
would swallow any errors returned from the API.
2017-07-10 17:14:52 +01:00
Richard van der Hoff 5ab0930de8 utils.failTest -> nodeify
Automated replacement of utils.failTest with nodeify

This was done with the perl incantation:

```
    find spec -name '*.js' |
        xargs perl -i -pe 's/catch\((testUtils|utils).failTest\).done\(done\)/nodeify(done)/'
```

more auto
2017-07-10 16:37:31 +01:00
David Baker 3294f4858a Merge pull request #485 from matrix-org/rav/test_watch
Add test:watch script
2017-07-07 14:25:36 +01:00
Richard van der Hoff eea9a3ba59 Add test:watch script
... to run the tests in a loop.
2017-07-07 14:19:41 +01:00
Richard van der Hoff 753974d663 Merge pull request #484 from matrix-org/rav/enable_async
Make it possible to use async/await
2017-07-07 13:47:59 +01:00
David Baker 527cd0a6e5 Implement 'joined_groups' API (#477)
* Add group summary api

* Add doc for group summary API

and remove callback param as it's deprecated

* API for /joined_groups

* Create group API

* Make doc marginally more helpful
2017-07-06 22:02:17 +01:00
Richard van der Hoff 24f70387d2 Make it possible to use async/await
Enables the babel plugin that transpiles async/await to generator functions,
and load the regenerator runtime so that generator functions work.
2017-07-06 18:52:37 +01:00
Richard van der Hoff adc2070ac1 Merge pull request #483 from matrix-org/rav/remove_new_device_support
Remove m.new_device support
2017-07-06 17:10:37 +01:00
Richard van der Hoff a8642682d0 Remove m.new_device support
We now rely on the server to track new devices, and tell us about them when
users add them, rather than forcing devices to announce themselves (see
https://github.com/vector-im/riot-web/issues/2305 for the whole backstory
there).

The necessary support for that has now been in all the clients and the server
for several months (since March or so). I now want to get rid of the
localstorage store, which this code is relying on, so now seems like a good
time to get rid of it. Yay.
2017-07-06 16:05:40 +01:00
Kegsay d66e6db480 Merge pull request #478 from krombel/access_token_header
Use access-token in header
2017-07-06 13:44:31 +01:00
Krombel dc66bbc3dc pass useAuthorizationHeader from constructor; add docs 2017-07-06 13:47:54 +02:00
Krombel 6e7f5feea5 remove fallback to query-params and set Authorization-Header based on construcor-option 2017-07-05 17:04:40 +02:00
Richard van der Hoff f21ea6c065 Extend timeout in megolm test
Use the default timeout of 100ms when waiting for the /send request, instead of
clamping it to 20ms.
2017-07-05 15:08:50 +01:00
Richard van der Hoff 6af56b56bc Merge pull request #482 from matrix-org/rav/sanity_check_protocols
Sanity-check response from /thirdparty/protocols
2017-07-05 11:09:20 +01:00
Richard van der Hoff 598d40b0b7 Sanity-check response from /thirdparty/protocols
Check that /thirdparty/protocols gives us an object (rather than a string, for
instance). I saw a test explode, apparently because it gave us a string. Which
is odd, but in general we ought to be sanity-checking the things coming back
from the server.
2017-07-05 10:51:08 +01:00
Richard van der Hoff 6ae714f51f Merge pull request #479 from matrix-org/rav/error_parsing
Avoid parsing plain-text errors as JSON
2017-07-04 17:11:47 +01:00
Richard van der Hoff b0661bb586 Update to matrix-mock-request 1.0
-- to pick up on the json parsing differences
2017-07-04 16:35:33 +01:00
Richard van der Hoff b6a165f1f8 Merge branch 'develop' into rav/error_parsing 2017-07-04 16:03:33 +01:00
Richard van der Hoff 8fe4a36b68 Merge pull request #481 from matrix-org/rav/use_external_mock_request
Use external mock-request
2017-07-04 16:01:21 +01:00
Richard van der Hoff 0d24f2d4c1 Use external mock-request
mock-request is now factored out to matrix-mock-request; use it
2017-07-04 15:45:22 +01:00
Krombel dd0ff3eeb5 intercept first authedRequest to determine if accessToken can be send by header (clearer structure) 2017-07-04 16:16:10 +02:00
Krombel 07868f701a Merge remote-tracking branch 'upstream/develop' into access_token_header 2017-07-04 15:28:17 +02:00
Richard van der Hoff f4f0e4b60f Merge pull request #480 from matrix-org/rav/fix_test_races
Fix some races in the tests
2017-07-04 14:04:24 +01:00
Richard van der Hoff ae950a2ff4 Fix some races in the tests
There is a common pattern in the tests which is, when we want to mock a /sync,
to flush it, and then, in the next tick of the promise loop, to wait for the
syncing event. However, this is racy: there is no guarantee that the syncing
event will not happen before the next tick of the promise loop.

Instead, we should set the expectation of the syncing event, then do the flush.
(Technically we only need to wait for the syncing event, but by waiting for
both we'll catch any errors thrown by the flush, and make sure we don't have
any outstanding flushes before proceeding).

Add a utility method to TestClient to do the above, and use it where we have a
TestClient.

(Also fixes a couple of other minor buglets in the tests).
2017-07-04 13:48:26 +01:00
Richard van der Hoff 5f6e4bdfe9 Avoid parsing plain-text errors as JSON
It's somewhat unhelpful to spam over the actual error from the reverse-proxy or
whatever with a SyntaxError.
2017-07-03 19:30:23 +01:00
Krombel c6d2d4ccda readd failover if server does not handle access-token via header 2017-07-01 14:30:37 +02:00
Krombel 59160a5d42 Implement failover when server does not allow setting the Authorized-header (CORS) 2017-07-01 12:16:46 +02:00
Krombel 5da6423fd6 Added failover if server does not recognize the auth header 2017-06-27 13:29:08 +02:00
Krombel d36b8721ca Allow Authorization-Header in tests 2017-06-23 15:49:07 +02:00
Krombel 539abffe0e Merge remote-tracking branch 'upstream/develop' into access_token_header 2017-06-23 15:16:58 +02:00
Krombel 9b24e66441 Merge branch 'develop' into access_token_header 2017-06-23 15:16:41 +02:00
Richard van der Hoff cc16cb9281 Merge pull request #475 from matrix-org/rav/fallback_to_memorystore
Fall back to MemoryCryptoStore if indexeddb fails
2017-06-22 16:05:30 +01:00
Richard van der Hoff 45fe4846f2 Fall back to MemoryCryptoStore if indexeddb fails
If we get an error when connecting to th indexeddb, fall back to a
MemoryCryptoStore.

This takes a bit of reorganising, because we don't get the error until we try
to connect to the database.
2017-06-22 15:22:55 +01:00
David Baker 3ca2779d9c Merge pull request #474 from matrix-org/rav/fix_braindead_firefox
Fix load failure in firefox when indexedDB is disabled
2017-06-22 15:22:21 +01:00
Richard van der Hoff 967341b127 fix build error
browser-index isn't transpiled, so can't use var there.
2017-06-22 15:16:23 +01:00
Richard van der Hoff 4e7f9fb805 Fix load failure in firefox when indexedDB is disabled 2017-06-22 15:05:02 +01:00
David Baker f3eb661aad Merge branch 'master' into develop 2017-06-22 11:51:24 +01:00
David Baker 1abf8e23a4 v0.7.13 2017-06-22 11:48:30 +01:00
David Baker 9f1f476f43 Prepare changelog for v0.7.13 2017-06-22 11:48:29 +01:00
David Baker 1a9d61c92a Merge pull request #473 from matrix-org/rav/no_require_indexeddb
Fix failure on Tor browser
2017-06-22 11:22:36 +01:00
David Baker 6ad465e3c0 Merge pull request #472 from matrix-org/rav/indexeddb_fixes
Fix issues with firefox private browsing
2017-06-22 11:15:03 +01:00
Richard van der Hoff 8ef947722f Fail gracefully on browsers without indexeddb
If we don't have indexeddb at all, don't try to make an indexeddb crypto store.
2017-06-22 07:49:28 +01:00
Richard van der Hoff 6e6b5c95a3 indexeddb worker: make clearDatabase work without having connected
... so that we can clear the database during login from a temporary client.
2017-06-21 21:13:41 +01:00
Richard van der Hoff fa593a7a37 Treat errors when deleting indexeddb as non-fatal
If we get an error when vaping the indexeddb, carry on regardless
2017-06-21 18:06:21 +01:00
Richard van der Hoff 7fcccad0ae Fix another round of test failures
'blocked' is *not* a fatal situation when opening or deleting databases.
2017-06-21 11:26:02 +01:00
David Baker e8ce94ade2 Merge pull request #471 from matrix-org/rav/fix_test_race
Fix a race in a test
2017-06-21 09:28:16 +01:00
Richard van der Hoff 6055f038ee Fix a race in a test
startClient was written in such a way that it would leave a flush() running,
which could sometimes interfere with the rest of the test (or even subsequent
tests), causing sporadic test failures. Make sure that the flush completes
before we move on.

Fix a test which turned out to be relying on that behaviour (there was a flush
which ended up being a no-op, thus effectively inserting a pause allowing the
sync promise to complete.

Fix a beforeEach handler which was relying on startClient resolving to
undefined.
2017-06-21 07:57:38 +01:00
Richard van der Hoff 6a1f40eeab Make sure we shut down the crypto module properly
listening to the sync STOPPED event doesn't cut it, because the app might (and
does, in the case of react-sdk) do a removeAllListeners.
2017-06-20 23:51:25 +01:00
Richard van der Hoff ca01589e50 Fix another round of test failures
'blocked' is *not* a fatal situation when opening or deleting databases.
2017-06-20 17:36:35 +01:00
David Baker cca891644d Merge pull request #470 from matrix-org/rav/fix_error_on_shutdown
Avoid throwing an unhandled error when the indexeddb is deleted
2017-06-20 15:46:55 +01:00
Richard van der Hoff cd19578d80 Avoid throwing an unhandled error when the indexeddb is deleted
Hopefully this will fix the vector-web test failures (the
OutgoingRoomRequestManager throws an exception because the indexeddb is being
deleted just as it's getting started).
2017-06-20 15:36:05 +01:00
Richard van der Hoff c96f7e5a13 Merge pull request #469 from matrix-org/rav/fix_jsdoc_build
fix jsdoc
2017-06-20 14:01:14 +01:00
Richard van der Hoff d7f92b4f72 fix jsdoc 2017-06-20 13:51:08 +01:00
Richard van der Hoff 70a5208fcc Run gendoc as part of the travis build
... so that I don't get surprised by it not working when it lands on develop
2017-06-20 13:33:04 +01:00
Richard van der Hoff 8c9150db66 Merge pull request #468 from matrix-org/rav/handle_forwarded_room_key_2
Handle m.forwarded_room_key events
2017-06-20 13:18:44 +01:00
Richard van der Hoff 1f86dbd12f Add support for forwarding room keys to megolm
when we receive a m.forwarded_room_key, add it to the crypto store, but
remember who forwarded it to us, so we can decide whether to trust them
separately.
2017-06-20 12:39:36 +01:00
Richard van der Hoff cfa871c076 event.js: Add support for forwardingCurve25519KeyChain 2017-06-20 11:51:30 +01:00
Richard van der Hoff f355661522 fix a lint error 2017-06-20 11:51:30 +01:00
Richard van der Hoff be3fb0f917 Make a start on a unit test for megolm alg impl
not much here yet, but it's a start at least.
2017-06-20 11:51:30 +01:00
Richard van der Hoff e2f4c0ffd1 Rename megolm integration tests
I'm going to introduce some separate unit tests, so let's give this a different
filename to reduce confusion.
2017-06-20 11:51:11 +01:00
Richard van der Hoff 210a53a3a5 Refactor internal OlmDevice methods
Rearrange the way _getInboundGroupSession and _saveInboundGroupSession work, so
that we can add more things to the storage without growing the parameter list
forever.
2017-06-20 11:51:11 +01:00
Richard van der Hoff 5049919855 Replace keysProved and keysClaimed
These terms were somewhat confusing (and, in the case of megolm, misleading),
so replace them with explicit senderCurve25519Key and claimedEd25519Key fields.
2017-06-20 11:51:11 +01:00
Richard van der Hoff ce187786cb Merge remote-tracking branch 'origin/develop' into room_key_sharing 2017-06-20 11:28:53 +01:00
Richard van der Hoff e6b35a9237 Run the crypto tests under travis (#467)
The crypto tests haven't been running since things got rearranged to expect
Olm in a global (41864d4). Reinstate them.
2017-06-20 10:44:03 +01:00
Richard van der Hoff 82e5e9cf4a Merge branch 'develop' into room_key_sharing 2017-06-19 17:38:35 +01:00
David Baker d7e1910076 Merge pull request #466 from matrix-org/rav/improve_indexeddb_errors
Improve error reporting from indexeddbstore.clearDatabase
2017-06-19 16:00:38 +01:00
Richard van der Hoff 009c28ae50 Improve error reporting from indexeddbstore.clearDatabase
- to help understand when it gets stuck in tests
2017-06-19 15:51:55 +01:00
David Baker db66023102 v0.7.12 2017-06-19 11:58:56 +01:00
David Baker 4d8dc1a0c4 Prepare changelog for v0.7.12 2017-06-19 11:58:55 +01:00
Krombel e0a5edeb04 implement usage of Authorization-Header instead of query-param for access_token 2017-06-16 12:33:42 +02:00
David Baker ffd9a01e2f v0.7.12-rc.1 2017-06-15 17:13:26 +01:00
David Baker 25a8c79951 Prepare changelog for v0.7.12-rc.1 2017-06-15 17:13:25 +01:00
Matthew Hodgson c8674ff104 Merge pull request #462 from t3chguy/t3chguy/voip/force_turn
allow setting iceTransportPolicy to relay through forceTURN option
2017-06-12 21:43:50 +01:00
Michael Telatynski a40b10f53c allow setting iceTransportPolicy to relay through forceTURN option
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-06-12 20:23:57 +01:00
David Baker 79fa944402 v0.7.11 2017-06-12 15:17:53 +01:00
David Baker ed3cdeec74 Prepare changelog for v0.7.11 2017-06-12 15:17:53 +01:00
David Baker 05d50d457c Merge remote-tracking branch 'origin/develop' into release-v0.7.11 2017-06-12 15:16:18 +01:00
David Baker 2531db84a6 Merge pull request #460 from matrix-org/rav/send_message_logging
Add a bunch of logging around sending messages
2017-06-12 13:37:50 +01:00
Richard van der Hoff 96c1126fe5 Add a bunch of logging around sending messages
In an attempt to diagnose https://github.com/vector-im/riot-web/issues/4278,
add some debug to make the rageshakes more useful.
2017-06-12 13:32:10 +01:00
David Baker bb5038b8b2 v0.7.11-rc.1 2017-06-09 20:23:30 +01:00
David Baker 0c65162349 Prepare changelog for v0.7.11-rc.1 2017-06-09 20:23:29 +01:00
David Baker 17cc12844d Merge pull request #458 from matrix-org/rav/resolve_timeline_window_quickly
Make TimelineWindow.load resolve quicker if we have the events
2017-06-09 20:06:28 +01:00
Richard van der Hoff 6cfcf92a28 Make TimelineWindow.load resolve quicker if we have the events
If we have the events in memory, let TimelineWindow.load() return
a resolved promise, so that the UI can show the view straight away instead
of showing the spinner.
2017-06-09 14:59:11 +01:00
Luke Barnard 6ed9a85dca Add API for POST /user_directory/search (#457)
* Add API for POST /user_directory/search

This takes a JSON body of the form:
```json
{
    "term": "search term",
    "limit": 42,
}
```
where "term" is the term to match against user IDs, display names and domains and "limit" is the maximum number of results to return (which is defaulted server-side).

The response body looks like
```json
{
    "results ": [
        { "user_id": "@someid:mydomain.com", "display_name": "Some Name", "avatar_url": "mx://..." },
        ...
    ],
    "limited": false
}
```
where "limited" indicates whether the "limit" was used to truncate the list.
2017-06-07 15:34:07 +01:00
Richard van der Hoff 0371265fea Send a cancellation for room key requests (#456)
* Send a cancellation for room key requests

When we receive a room key, cancel any pending requests we have open for that
key.
2017-06-07 14:00:47 +01:00
Richard van der Hoff de257b34c0 Merge pull request #454 from matrix-org/rav/key_share/incoming
Implement sharing of megolm keys
2017-06-07 13:17:43 +01:00
Richard van der Hoff 4b6575d94a Fix jsdocs 2017-06-07 11:02:27 +01:00
Richard van der Hoff 2c54d76085 Implement sharing of megolm keys 2017-06-06 14:46:54 +01:00
Richard van der Hoff 70f39ed760 Fix lint failure 2017-06-06 14:46:24 +01:00
Richard van der Hoff 1c6652483b Merge pull request #449 from matrix-org/rav/handle_room_key_requests
Process received room key requests
2017-06-06 14:30:56 +01:00
Richard van der Hoff ab7e0a9266 Merge branch 'room_key_sharing' into rav/handle_room_key_requests 2017-06-06 14:30:34 +01:00
Richard van der Hoff ff323d00af Merge pull request #448 from matrix-org/rav/send_room_key_requests
Send m.room_key_request events when we fail to decrypt an event
2017-06-06 14:25:40 +01:00
Richard van der Hoff ea2a04135f Send a room key request on decryption failure
When we are missing the keys to decrypt an event, send out a request for those
keys to our other devices and to the original sender.
2017-06-06 14:24:19 +01:00
Richard van der Hoff 6d88c76464 Storage layer for management of outgoing room key requests 2017-06-06 14:24:19 +01:00
Luke Barnard 9b188ca87d Use single room object for duration of peek (#453)
Use single room object for duration of peek

Instead of getting the room by ID every time the room is polled for events, which could cause issues if the state of the room is modified from under the peeking logic (if the user joined the room or registered etc.)
2017-06-06 12:15:30 +01:00
Richard van der Hoff 1664312c80 Address review comments
Avoid gut-wrenching properties on IncomingRoomKeyRequest.
2017-06-05 16:07:38 +01:00
David Baker 38baa42ebb Merge pull request #451 from matrix-org/dbkr/stop_peeking
Stop peeking when a matrix client is stopped
2017-06-05 14:16:35 +01:00
David Baker 654322e896 Stop peeking when a matrix client is stopped
Otherwise we get very confused when the peek poll returns after
the client is stopped.
2017-06-05 14:04:41 +01:00
Richard van der Hoff 3f70f532b7 Update README.md
lack of olm is a warning, not an exception
2017-06-05 09:24:59 +01:00
Richard van der Hoff 6ba214a259 Merge pull request #450 from arxcode/develop
Update README: Clarify how to install libolm
2017-06-05 09:22:34 +01:00
arxcode caf73f387f Update README: Clarify how to install libolm 2017-06-04 23:24:11 +02:00
Matthew Hodgson 9a81ca9fab v0.7.10 2017-06-02 01:02:01 +01:00
Matthew Hodgson 0edf19a871 Prepare changelog for v0.7.10 2017-06-02 01:02:01 +01:00
Matthew Hodgson 6989f6c835 switch to using new media constraints to allow device selection to work 2017-06-01 21:57:58 +01:00
Richard van der Hoff 2daa39520a Room key request cancellation handling 2017-06-01 18:30:32 +01:00
Richard van der Hoff c8eca50f43 Processing of received room key requests
Doesn't actually do any of the crypto magic yet.
2017-06-01 18:30:26 +01:00
Richard van der Hoff de844f1a32 Merge pull request #447 from matrix-org/rav/fix_indexeddb_deletion
indexeddb-crypto-store: fix db deletion
2017-06-01 17:34:37 +01:00
Richard van der Hoff 97951e1c1a Merge pull request #446 from matrix-org/rav/load_olm_from_global
Load Olm from the global rather than requiring it.
2017-06-01 15:41:22 +01:00
Richard van der Hoff 2edbed8528 indexeddb-crypto-store: fix db deletion
Add an `onversionchange` listener to close the db, so that we can delete it
without blocking.
2017-06-01 15:37:27 +01:00
Richard van der Hoff 24937910c7 Merge remote-tracking branch 'origin/develop' into rav/load_olm_from_global 2017-06-01 15:31:27 +01:00
Richard van der Hoff 5cd441fb48 Add a warning to the changelog 2017-06-01 15:30:00 +01:00
Richard van der Hoff 06b956bd75 disable e2e test when there is no e2e 2017-06-01 13:16:10 +01:00
Richard van der Hoff 41864d46c3 Load Olm from the global rather than requiring it.
This means that we can avoid confusing everybody in the world about how to
webpack js-sdk apps.
2017-06-01 13:09:48 +01:00
Matthew Hodgson f6622e0bcd unbreak riot-web release process 2017-06-01 02:41:47 +01:00
Matthew Hodgson 0f30d21fa2 v0.7.9 2017-06-01 01:41:06 +01:00
Matthew Hodgson 4257c8c9f5 Prepare changelog for v0.7.9 2017-06-01 01:41:06 +01:00
Richard van der Hoff 331859d383 Merge pull request #445 from matrix-org/rav/indexeddb_crypto_store
Initial framework for indexeddb-backed crypto store
2017-05-31 18:06:45 +01:00
Richard van der Hoff ef03b708a8 Add MatrixClient.clearStores
- to clear both sets of storage on logout
2017-05-31 17:22:07 +01:00
Richard van der Hoff 716d098361 Address Kegan's review comments
jsdoc mostly.
2017-05-31 16:05:00 +01:00
Richard van der Hoff d887057660 Merge pull request #444 from matrix-org/rav/factor_out_reemit
Factor out reEmit to a common module
2017-05-31 14:22:34 +01:00
Richard van der Hoff 7efbfebb4d Factor out reEmit to a common module
and rewrite it to use modern JS while we're at it
2017-05-31 11:01:48 +01:00
Richard van der Hoff 4c7afe5af0 Initial framework for indexeddb-backed crypto store
Doesn't do anything useful yet - just demonstrates a framework for how I hope
it will fit into the sdk.
2017-05-30 23:25:07 +01:00
Richard van der Hoff 676515cf27 Merge pull request #443 from matrix-org/rav/es6ify_algorithm_base
crypto/algorithms/base.js: Convert to es6
2017-05-23 16:36:42 +01:00
Richard van der Hoff 0eb5b0fdfa Merge pull request #435 from t3chguy/maySendRedactionForEvent
maySendRedactionForEvent for userId
2017-05-23 15:45:46 +01:00
Richard van der Hoff 2feba4787f Merge pull request #441 from matrix-org/rav/get_userid
MatrixClient: add getUserId()
2017-05-23 15:44:34 +01:00
Michael Telatynski 516dc1043e prevent powerLevels being undef
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-23 14:53:11 +01:00
Richard van der Hoff b26c1c57dc crypto/algorithms/base.js: Convert to es6
Convert base to an es6 module with es6 classes, for clarity and to help with
jsdoccing.

Complications are:

* jsdoc gets confused by `export class`, so the exports are separated.

* turns out that extending Error is a bit difficult, so instanceof doesn't work
  on derived Error classes. This only really affects us in one place (app-side
  code shouldn't be doing instanceofs anyway), so just use `name` instead.
2017-05-23 14:32:13 +01:00
Richard van der Hoff 0945ba9e90 Merge pull request #442 from matrix-org/rav/custom_babel_for_jsdoc
Run jsdoc on a custom babeling of the source
2017-05-23 14:28:41 +01:00
Michael Telatynski 69ed6f283d fix based on rich's feedback
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-23 12:55:05 +01:00
Richard van der Hoff 9eef850d0c Run jsdoc on a custom babeling of the source
jsdoc can't read our raw source, because of our dangling commas in function
calls. On the other hand, running on /lib means that a lot of the useful
information about exports is lost and you end up having to jump through hoops
to get jsdoc to generate the right thing.

This uses a separate run of babel (with all the presets turned off) to generate
source which is almost identical to the input, but lacks trailing commas.

(https://babeljs.io/blog/2015/10/31/setting-up-babel-6 says 'Babel 6 ships
without any default transforms, so when you run Babel on a file it will just
print it back out to you without changing anything.' - however, that is,
empirically, not entirely true.)
2017-05-23 12:26:17 +01:00
Richard van der Hoff cf1574d690 MatrixClient: add getUserId()
... I'm amazed we got this far without it.
2017-05-23 10:37:26 +01:00
David Baker d6913e41a0 Merge branch 'master' into develop 2017-05-22 11:33:20 +01:00
David Baker 3c81c295c7 v0.7.8 2017-05-22 11:31:48 +01:00
David Baker 56dfa0c755 Prepare changelog for v0.7.8 2017-05-22 11:31:47 +01:00
Richard van der Hoff 43989be768 Merge pull request #439 from kscz/add_getstoreddeviceforuser
Add in a public api getStoredDevice allowing clients to get a specific device
2017-05-22 09:36:38 +01:00
Kit Sczudlo 822380ac38 Add in a public api getStoredDevice allowing clients to get a specific device
Signed-off-by: Kit Sczudlo <kit@kitscz.com>
2017-05-21 00:30:40 -07:00
David Baker 8c37d9ac9a v0.7.8-rc.1 2017-05-19 10:34:31 +01:00
David Baker e40b8461f7 Prepare changelog for v0.7.8-rc.1 2017-05-19 10:34:30 +01:00
David Baker a3f45b466a Merge pull request #438 from matrix-org/rav/release_signing
Attempt to rework the release-tarball-signing stuff
2017-05-19 10:03:36 +01:00
Richard van der Hoff 672ad68c64 release.sh: download the tarball from git to verify it 2017-05-18 18:58:50 +01:00
David Baker 4ccec13739 Fix build: move uglifyjs dep to uglify-js
uglifyjs have gained a hyphen for some reason, and replaced th
old one with a stub package.
2017-05-17 11:21:20 +01:00
Michael Telatynski 09529a1aa8 lets please the ESLint gods
`--max-warnings 115` :')

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:16:42 +01:00
Michael Telatynski d182fd6bb7 can't redact queued/not_sent
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:12:29 +01:00
Michael Telatynski 36bf123e2b maySendRedactionForEvent for userId
done using a private helper so kick/ban etc perms can be done
easily at a later stage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:04:55 +01:00
Matthew Hodgson 92cfbf655f Merge pull request #427 from t3chguy/electron_media_select
ability to specify webrtc audio/video inputs for the lib to request
2017-05-15 02:10:01 +01:00
Matthew Hodgson fbef701179 Merge pull request #434 from t3chguy/t3chguy/screen_share_firefox
make screen sharing call FF friendly :D
2017-05-15 00:16:33 +01:00
Michael Telatynski 0415b9cf4c make screen sharing call FF friendly :D
FF is uber nice that it lets us select the display
does not seem to allow the composite ALL displays though

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-14 23:48:26 +01:00
Luke Barnard cb9a9e8d50 Implement API for username availability (#432)
Requires synapse with https://github.com/matrix-org/synapse/pull/2183, https://github.com/matrix-org/synapse/pull/2209 and https://github.com/matrix-org/synapse/pull/2213
2017-05-11 09:14:45 +01:00
Richard van der Hoff 6021c1c6b1 Merge pull request #431 from matrix-org/rav/fix_device_list_yet_again
Fix race in device list updates
2017-05-05 13:29:53 +01:00
Richard van der Hoff 655be2fa2e Fix race in device list updates
Don't consider device lists up-to-date when we have another request for the
relevant user in the queue.

Fixes https://github.com/vector-im/riot-web/issues/3796.
2017-05-05 12:34:00 +01:00
Michael Telatynski 98491a63a7 ability to specify webrtc audio/video inputs for the lib to request 2017-04-27 16:06:34 +01:00
David Baker acd7f15c83 Merge pull request #424 from matrix-org/rob/nocam
WebRTC: Support recvonly for video for those without a webcam
2017-04-26 18:23:52 +01:00
Richard van der Hoff 5020d4e99f Rework device list tracking logic (#425)
Yet another attempt at fixing
https://github.com/vector-im/riot-web/issues/2305.

This now implements the algorithm described at
http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#tracking-the-device-list-for-a-user:

* We now keep a flag to tell us which users' device lists we are tracking. That
  makes it much easier to figure out whether we should care about device-update
  notifications from /sync (thereby fixing
  https://github.com/vector-im/riot-web/issues/3588).

* We use the same flag to indicate whether the device list for a particular
  user is out of date. Previously we did this implicitly by only updating the
  stored sync token when the list had been updated, but that was somewhat
  complicated, and in any case didn't help in cases where we initiated the key
  download due to a user joining an encrypted room.

Also fixes https://github.com/vector-im/riot-web/issues/3310.
2017-04-25 17:56:01 +01:00
David Baker 9693c30209 Merge branch 'master' into develop 2017-04-25 10:51:08 +01:00
David Baker 2b6f8adc64 v0.7.7 2017-04-25 10:49:28 +01:00
David Baker 822f5927e5 Prepare changelog for v0.7.7 2017-04-25 10:49:26 +01:00
David Baker 0f6e9d7b9d v0.7.7-rc.1 2017-04-21 18:15:49 +01:00
David Baker 99f3e3f09e Prepare changelog for v0.7.7-rc.1 2017-04-21 18:15:48 +01:00
David Baker aa81c96a98 Automatically complete dummy auth
Dummy auth flows, bu definition, do not require a response from
the user, and so should just be completed automatically by
interactive-auth.
2017-04-21 18:06:57 +01:00
Richard van der Hoff 9d532b6c72 Merge pull request #422 from t3chguy/develop
Update istanbul to remove minimatch DoS Warning
2017-04-21 12:12:32 +01:00
Luke Barnard 4c63906b8f Implement API for setting RM (#419)
* Implement API for setting RM

This is now stored on the server with similar treatment to RRs. The server will only store the specified eventId as the current read marker for a room if the event is ahead in the stream when compared to the existing RM. The exception is when the RM has never been set for this room for this user, in which case the event ID will be stored as the RM without any comparison.

This API also allows for an optional RR event ID to be sent in the same request. This is because it might be the common case for some clients to update the RM at the same time as updating the RR.

See design: https://docs.google.com/document/d/1UWqdS-e1sdwkLDUY0wA4gZyIkRp-ekjsLZ8k6g_Zvso/edit

See server-side PRs: https://github.com/matrix-org/synapse/pull/2120, https://github.com/matrix-org/synapse/pull/2128
2017-04-20 09:43:33 +01:00
Robert Swain dd2a870227 webrtc/call: Unmute remote audio element when setting 2017-04-20 06:41:29 +02:00
Robert Swain 88948c3cfd webrtc/call: Always offer to receive audio/video for video call
This allows people without (or denying access to) a webcam to make a
video call and receive audio and video from the peer.
2017-04-20 06:35:03 +02:00
Robert Swain b33dcfe6ff webrtc/call: Fall back to recvonly if camera/mic access is denied
Users of MatrixCall will need to present some sensible UX for this.
2017-04-20 06:32:52 +02:00
Robert Swain 2c15bdae04 Merge pull request #423 from matrix-org/rob/more-distinct-callid
webrtc/call: Make it much less likely that callIds collide locally
2017-04-19 17:34:15 +02:00
Robert Swain 2f45633312 webrtc/call: Make it much less likely that callIds collide locally
Previously if two calls were constructed within 1ms they could have the
same id.
2017-04-19 16:51:23 +02:00
Michael Telatynski fdd42fbc6d Update dependencies to remove minimatch DoS Warning
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-04-19 12:50:21 +01:00
Richard van der Hoff 54a6f5d425 Merge pull request #420 from matrix-org/dbkr/fix_dummy_auth
Automatically complete dummy auth
2017-04-13 14:23:33 +01:00
David Baker 68d9662fe5 Automatically complete dummy auth
Dummy auth flows, bu definition, do not require a response from
the user, and so should just be completed automatically by
interactive-auth.
2017-04-12 18:36:23 +01:00
David Baker 4f0987da01 Merge branch 'master' into develop 2017-04-12 09:58:26 +01:00
David Baker 625697e097 v0.7.6 2017-04-12 09:56:46 +01:00
David Baker 92b14f20d2 Prepare changelog for v0.7.6 2017-04-12 09:56:45 +01:00
Richard van der Hoff dd069647d1 Merge pull request #418 from matrix-org/dbkr/release_script_dont_leave_me_in_gh_pages
Don't leave the gh-pages branch checked out
2017-04-10 16:36:25 +01:00
David Baker 4523ae7d29 Checkout release branch *before* exiting script 2017-04-10 16:15:06 +01:00
David Baker 19e5eda773 Don't leave the gh-pages branch checked out
After a pre-release, check out the release branch again rather
than leaving the working copy on the gh-pages branch
2017-04-10 15:52:37 +01:00
David Baker 070d58ac0e v0.7.6-rc.2 2017-04-10 14:52:10 +01:00
David Baker 2d70f69857 Prepare changelog for v0.7.6-rc.2 2017-04-10 14:52:09 +01:00
David Baker 7a3acfa6a7 Merge remote-tracking branch 'origin/develop' into release-v0.7.6 2017-04-10 14:50:16 +01:00
David Baker 8ac0d12d1e Merge pull request #416 from matrix-org/dbkr/feature_detect_webworker
Add feature detection for webworkers
2017-04-10 11:28:40 +01:00
David Baker 86164103f0 Allow webworker API to be passed in
So it can be used from Node with one of the compatible APIs
2017-04-10 10:02:06 +01:00
David Baker b9c71ef03f Add feature detection for webworkers
Only use web worker store if we have web workers available
2017-04-07 17:45:45 +01:00
David Baker 7ffff761d5 Merge remote-tracking branch 'origin/develop' into release-v0.7.6 2017-04-07 17:01:25 +01:00
Richard van der Hoff 7d4366473d Merge pull request #415 from matrix-org/dbkr/fix_release_script
Fix release script
2017-04-07 17:00:12 +01:00
David Baker e63c660162 Fix release script
Publish to npm before switching to the doc branch: previously we
published from master, but since we now now longer merge
pre-releases to master, publish from the release branch (just
not the doc branch because that won't work).
2017-04-07 16:55:38 +01:00
David Baker 1762f9d68e v0.7.6-rc.1 2017-04-07 16:44:21 +01:00
David Baker 76287eed2c Prepare changelog for v0.7.6-rc.1 2017-04-07 16:44:20 +01:00
David Baker 5a764bbaa2 Merge pull request #414 from matrix-org/dbkr/indexeddb_save_after_first_sync
Make indexeddb save after the first sync
2017-04-07 16:26:03 +01:00
David Baker ce9e69c9e0 Merge remote-tracking branch 'origin/develop' into dbkr/indexeddb_save_after_first_sync 2017-04-07 16:21:54 +01:00
David Baker 3a74e1f154 Merge remote-tracking branch 'origin/dbkr/indexeddb_webworker_dont_transfer_sync' into dbkr/indexeddb_save_after_first_sync 2017-04-07 16:21:21 +01:00
David Baker 6df4a36da9 Merge pull request #413 from matrix-org/dbkr/indexeddb_webworker_dont_transfer_sync
Make indexeddb startup faster
2017-04-07 16:20:44 +01:00
David Baker 4e38b51958 Merge remote-tracking branch 'origin/develop' into dbkr/indexeddb_webworker_dont_transfer_sync 2017-04-07 15:12:54 +01:00
David Baker b6c036af25 Merge pull request #412 from matrix-org/dbkr/indexeddb_webworker
Add ability to do indexeddb sync work in webworker
2017-04-07 15:12:22 +01:00
David Baker dd789a8dcc Merge branch 'dbkr/indexeddb_webworker_dont_transfer_sync' into dbkr/indexeddb_save_after_first_sync 2017-04-07 15:10:17 +01:00
David Baker ca83b858c0 lint 2017-04-07 15:09:42 +01:00
David Baker 0f29952a1c Smush connect() and init() together 2017-04-07 15:06:38 +01:00
David Baker e2d7b465ae Merge branch 'dbkr/indexeddb_webworker' into dbkr/indexeddb_webworker_dont_transfer_sync 2017-04-07 15:01:42 +01:00
David Baker 8985dc2f7e Update import in example 2017-04-07 14:42:26 +01:00
David Baker cf1731792c Add separate import file for indxexeddb worker
And removing it from the main one
2017-04-07 14:41:12 +01:00
David Baker 4c200cdd49 lint 2017-04-07 14:26:43 +01:00
David Baker 5c8eacddde Remove old comment 2017-04-07 12:01:31 +01:00
David Baker 2668177210 Oops, moved the variable 2017-04-07 11:58:16 +01:00
David Baker 62be08f063 Make sure worker setup doesn't race 2017-04-07 11:55:13 +01:00
David Baker ab2a67a012 Don't try to send Error objects 2017-04-07 11:42:18 +01:00
David Baker 3ceeee7298 Typos 2017-04-07 11:40:21 +01:00
David Baker 2d7576f29b Doc usage of the webworker class 2017-04-07 11:37:31 +01:00
David Baker 6e25a17afb Typos 2017-04-07 11:26:52 +01:00
David Baker 0715682a8b Name IndexedDBStoreWorker consistently 2017-04-07 11:22:07 +01:00
David Baker cfff30c314 Use once to clean up listener 2017-04-07 11:10:57 +01:00
David Baker b53318ecb7 Make indexeddb save after the first sync
Save the sync data to indexeddb after the first catch-up

Fixes https://github.com/vector-im/riot-web/issues/3527
2017-04-06 18:48:54 +01:00
David Baker 039a3e258b Make indexeddb startup faster
Don't needlessly transfer data from the worker to the main script
only to immediately transfer it right back again.
2017-04-06 18:09:12 +01:00
David Baker 18806e5524 lint 2017-04-06 16:58:13 +01:00
David Baker 0594a8d03a Don't set the sync token when loading indexeddb
Setting the sync token here marks the memory store part as being
synced up to that point, but it isn't because the sync data hasn't
yet been injected into it.

The sync token will be set in the normal way when the cached sync
response is processed, at which point it will be accurate because
the cached sync data will actually have been processed by the sync
code and saved to the memory store.
2017-04-06 16:41:49 +01:00
David Baker 5a575d61b6 Comment really inefficient bit 2017-04-06 16:33:52 +01:00
David Baker 42c3cf2545 Use correct variable name
From https://github.com/matrix-org/matrix-js-sdk/pull/401
2017-04-06 16:09:28 +01:00
David Baker bf6490739d Fix tests
Make setSyncData return promises in a few places and fix all the
places the tests assume that /sync being flushed == the sync
result being processed.
2017-04-06 15:20:50 +01:00
David Baker 9815c0a866 Remove debug exception catching 2017-04-06 11:47:55 +01:00
David Baker f5f05a9a91 Add ability to do indexeddb sync work in webworker 2017-04-06 11:09:11 +01:00
David Baker b392656d60 Merge pull request #409 from matrix-org/dbkr/indexeddb_worker_localsplit
Move more functionality to the indexeddb backend
2017-04-06 10:58:37 +01:00
David Baker 6737d091fd Merge pull request #410 from matrix-org/luke/fix-indicate-error-when-reconnecting
Indicate syncState ERROR after many failed /syncs
2017-04-05 16:07:53 +01:00
Luke Barnard 9f924c3510 Respond to code review
- failedSyncCount -> this._failedSyncCount
- Reset at a point which is more obviously after a successful sync, and add comments to clarify the recursive syncage.
- Sync state will now flip from RECONNECTING to ERROR after FAILED_SYNC_ERROR_THRESHOLD syncs
2017-04-05 09:40:06 +01:00
Luke Barnard 2ea66d2e81 Indicate syncState ERROR after many failed /syncs
when a /sync leads to an error, increase a counter and use the counter to decide which state to be in when starting keepAlives. If the count is above a certain threshold (arbitrary 3 chosen here), switch from RECONNECTING to ERROR to show that the server is facing difficulties that aren't affeting is ability to return on /versions.
2017-04-04 18:12:20 +01:00
David Baker d18c238938 Dangling comma 2017-04-04 16:35:39 +01:00
David Baker 8c92e221a3 long line 2017-04-04 16:29:05 +01:00
David Baker bafe9c06d4 Move more functionality to the indexeddb backend
Now the backend drives the sync accumulator as well. Also moves
the backend out to a separate file.
2017-04-04 16:25:58 +01:00
David Baker cad6ec854e Merge pull request #407 from matrix-org/dbkr/indexeddb_refactor_2
Further reorganising of indexeddb sync code
2017-04-04 13:13:18 +01:00
David Baker 47a0398b62 Remove other two lines 2017-04-04 10:43:57 +01:00
David Baker 11f5ae3c20 Merge remote-tracking branch 'origin/dbkr/indexeddb_hide_internals' into dbkr/indexeddb_refactor_2 2017-04-04 10:39:23 +01:00
David Baker 61cf853eb5 Merge pull request #406 from matrix-org/dbkr/indexeddb_hide_internals
Change interface of IndexedDBStore: hide internals
2017-04-04 10:38:43 +01:00
David Baker f72884ac19 Spelling 2017-04-04 10:36:31 +01:00
David Baker b72b38b0a3 Add missed methods to stub/memory store
and fix tests
2017-04-03 17:45:58 +01:00
David Baker 6a2465329a Add jsdoc 2017-04-03 17:12:30 +01:00
David Baker 4cb80588e9 Merge branch 'dbkr/indexeddb_hide_internals' into dbkr/indexeddb_refactor_2 2017-04-03 16:41:02 +01:00
David Baker 753f11e0e9 Stray jsdoc line that didn't get removed 2017-04-03 15:39:08 +01:00
David Baker c0bd2c8945 Further reorganising of indexeddb sync code
* Make sync communicate with the sync accumulator via the store
 * Consequently get rid of getSyncAccumulator as it's now
   unnecessary.
 * Make the bit that gets the saved sync response async, because
   we'll need it to be when it's coming over postMessage from a
   webworker.
2017-03-31 18:18:53 +01:00
David Baker aebbe4f254 Change interface of IndexedDBStore: hide internals
Hide the IndexedDBBackend and SyncAccumulator objects and have
indexeddbstore instaniate them itself, rather than making the app
instantiate them. Pass the approipriate options through. This
gives us much more flexibility to move things around under the
hood.
2017-03-31 14:53:13 +01:00
David Baker 68948dbaeb Merge pull request #405 from matrix-org/dbkr/fix_notifs_on_refresh
Don't be SYNCING until updating from the server
2017-03-28 15:16:01 +01:00
David Baker a38917f920 Don't be SYNCING until updating from the server
Syncing should probably mean the stream is up to date and
streaming messages in real-time from the server, which is not the
case if we've only loaded the cached response. Stay PREPARED until
we actually get the latest from the server.
2017-03-28 14:57:11 +01:00
Kegsay f52e198b17 Merge pull request #403 from matrix-org/kegan/dont-log-store-data
Don't log the entire /sync response
2017-03-24 15:09:14 +00:00
Kegan Dougal dec734346b Don't log the entire /sync response
The console will maintain a strong ref to this object, which may exacerbate
memory leaks.
2017-03-24 14:15:35 +00:00
David Baker a73f10edd4 Merge pull request #402 from matrix-org/rob/webrtc-srcobject
webrtc/call: Assign MediaStream to video element srcObject
2017-03-24 14:14:22 +00:00
Robert Swain 59a7232016 webrtc/call: Wrap long line 2017-03-24 15:00:49 +01:00
Robert Swain d9e6aed9da webrtc/call: Assign MediaStream to video element srcObject
video.src = URL.createObjectURL(stream) is on the way out. Firefox will
complain with errors about not being able to play media of type
"text/html" for example.
2017-03-24 14:53:43 +01:00
David Baker e4f52dd1c7 Merge pull request #400 from matrix-org/dbkr/fix_requests_null_deref
Fix undefined reference in http-api
2017-03-23 15:45:35 +00:00
David Baker 2c1e3416e3 Fix undefined reference in http-api
Put the check for 'onprogress' within the check to see if req is
defined, because sometimes it isn't, apparently.
2017-03-23 15:38:15 +00:00
Richard van der Hoff 62090ef119 Merge pull request #382 from fred-wang/master
Add copyright header to event-timeline.js
2017-03-22 17:31:34 +00:00
Richard van der Hoff 52ef8a635f Merge pull request #397 from williamboman/docs/user-global-account-data-event
client: fix docs for user-scoped account_data events
2017-03-22 17:10:52 +00:00
William Boman bf26ccd0a5 client: fix docs for user-scoped account_data events
Signed-off-by: William Boman <william@redwill.se>
2017-03-22 18:02:24 +01:00
Richard van der Hoff 5a55b98650 Merge remote-tracking branch 'origin/master' into develop 2017-03-22 16:55:09 +00:00
David Baker 547333c946 Merge pull request #399 from matrix-org/rav/contributing
Add a CONTRIBUTING for js-sdk
2017-03-22 16:46:21 +00:00
Kegsay 1ed105cb79 Merge pull request #395 from matrix-org/kegan/memleaks
Fix leaking room state objects on limited sync responses
2017-03-22 16:38:42 +00:00
Richard van der Hoff 2ce2928170 Add a CONTRIBUTING for js-sdk
... inspired by synapse's.
2017-03-22 16:36:58 +00:00
Kegan Dougal 14727d75ac Review comments 2017-03-22 15:13:21 +00:00
Kegan Dougal ccbc0b79b8 Add getter/setter for the callback on the MatrixClient instance rather than a startClient opt for ease of gluing code in 2017-03-22 14:29:59 +00:00
Kegan Dougal 5bee0004b2 Revert test as nothing has changed 2017-03-22 13:51:00 +00:00
Kegan Dougal 86fd42dcb5 linting 2017-03-22 12:01:58 +00:00
Kegan Dougal 1e05e0d6f8 Review comments 2017-03-22 11:56:10 +00:00
David Baker 821e0ed6ce Merge pull request #396 from matrix-org/dbkr/ui_auth_bg_requests
Extend 'ignoreFailure' to be 'background'
2017-03-22 11:56:07 +00:00
David Baker 66ce31f6d6 Add docs. 2017-03-21 18:40:00 +00:00
David Baker cf486aedbd Extend 'ignoreFailure' to be 'background'
This allows us to also use it to decide whether or not to show
the app as busy in the UI. We pass this flag up into the
makeRequest callback so it can use it as such.
2017-03-21 18:37:08 +00:00
David Baker 1b0f22c4ae Merge pull request #388 from matrix-org/dbkr/x_show_msisdn
Add x_show_msisdn parameter to register calls
2017-03-21 13:41:34 +00:00
Kegan Dougal 55acf21aa6 Linting 2017-03-20 12:06:37 +00:00
Kegan Dougal dc8a2670ab Unbreak tests 2017-03-20 12:05:22 +00:00
Kegan Dougal b666ec1f4d Fix memory leak on limited room responses 2017-03-20 11:46:50 +00:00
Kegan Dougal 999fc07683 Explain the memory hack 2017-03-20 11:34:50 +00:00
Kegan Dougal 37a186696a Remove spurious changes 2017-03-20 11:27:59 +00:00
Kegan Dougal 107ef27f69 Remove spurious changes 2017-03-20 11:26:59 +00:00
Kegan Dougal 0c1c10a0e0 WIP memleak fixes (341->295MB) 2017-03-16 18:02:17 +00:00
Richard van der Hoff 89de1f9a01 Merge pull request #394 from matrix-org/luke/redact-keep-sender-ts
Update event redaction to keep sender and origin_server_ts
2017-03-16 16:31:34 +00:00
Luke Barnard 602e91da40 Update event redaction to keep sender and origin_server_ts
Required for fixing https://github.com/matrix-org/matrix-js-sdk/issues/387
2017-03-16 16:29:55 +00:00
Kegsay dec4e67135 Merge pull request #393 from matrix-org/kegan/sync-accumulator-limited
Handle 'limited' timeline responses in the SyncAccumulator
2017-03-16 13:52:41 +00:00
Kegan Dougal c30670000d Handle 'limited' timeline responses in the SyncAccumulator
Fixes vector-im/riot-web#3375
2017-03-16 13:20:27 +00:00
David Baker 9d8e81d79c Merge pull request #391 from matrix-org/dbkr/msisdn_better_error_message
Give a better error message if the HS doesn't support msisdn registeration
2017-03-16 12:44:58 +00:00
David Baker 421a35c201 Stray brace 2017-03-16 12:33:24 +00:00
David Baker fcfc7b6cec Better doc & throw consistently 2017-03-16 12:02:06 +00:00
Luke Barnard 2f5da3851b Use XHR onprogress to debounce http._request timeout (#392)
Instead of just using a timeout to reject ongoing requests, reset the timeout when progress is observed (at least when requests are done from browsers).

This is to fix https://github.com/vector-im/riot-web/issues/2737
2017-03-16 09:50:24 +00:00
David Baker 6c2e8eba1c Do no-auth-flow error handling more properly 2017-03-15 16:42:57 +00:00
David Baker c9c3937f4b Move exception throw into _chooseFlow 2017-03-15 14:33:31 +00:00
David Baker 7777cbf6da Lint 2017-03-15 14:21:34 +00:00
David Baker a8a7d327ff Give a better error message
if the HS doesn't support msisdn registeration
2017-03-15 14:14:04 +00:00
David Baker 571fcbe98d Merge remote-tracking branch 'origin/develop' into dbkr/x_show_msisdn 2017-03-15 11:28:24 +00:00
David Baker 8b4b0e0d39 Save the completed flows (#389)
Otherwise we get very confused and go back to the start when given
a response with no flows etc.

Only copy data if none of the 3 fields are defined, since that's
more the situation we actually want to handle.
2017-03-15 11:15:35 +00:00
David Baker c8868a393b Add x_show_msisdn parameter to rregister calls
Request the msisdn stages in the rtegister UI auth, as commented
2017-03-13 17:51:59 +00:00
Matthew Hodgson 49be37dcf9 remove incredibly verbose start/complete sync logging 2017-03-11 15:06:29 +00:00
David Baker 2cd5fe2fec Support msisdn registration and signin (#384)
* Functionality for msisdn signin

 * Add methods to request tokens from synapse to do msisdn
   verification
 * Extend interactive-auth to work with m.email.identity (which
   is significant since email auth is quite a chunk more complex).

* Oops, fix merge

* Fix lint

* Add submitMsisdnToken

* Support the bind_msisdn param to register

Change the bind_email flag to an object with keys 'email' and
'msisdn', backwards compatibly.
2017-03-09 10:56:50 +00:00
David Baker b52a674c1a Merge pull request #383 from matrix-org/dbkr/fix_teamserver
Add getEmailSid
2017-03-06 17:57:06 +00:00
David Baker 8f790d406f Fix lint (add jsdoc) 2017-03-06 09:27:29 +00:00
David Baker 7e2a256229 Add getEmailSid
Other places sometimes need to re-use the email sid to send proof
of ownership of a email address to somewhere else.
2017-03-03 17:40:26 +00:00
David Baker c7a0a560d8 Merge pull request #380 from matrix-org/dbkr/register_ui_auth
Add m.login.email.identity support to UI auth
2017-03-03 13:37:35 +00:00
David Baker 1d591034ff Doc sid param 2017-03-03 10:15:09 +00:00
Frédéric Wang f5ceaffc5c Add copyright header to event-timeline.js 2017-03-02 21:27:51 +01:00
David Baker 3c246a97e8 More docs 2017-03-02 14:15:43 +00:00
David Baker ca395541b6 Backwards compat 2017-03-02 14:09:08 +00:00
David Baker 30d9dec438 Doc error objects pass to stage update 2017-03-02 14:07:25 +00:00
David Baker ff3606478c Merge pull request #381 from matrix-org/rob/maybe-fix-glare
src/client.js: Fix incorrect roomId reference in VoIP glare code
2017-03-02 13:12:29 +00:00
Robert Swain 72caf1886d src/client.js: Fix incorrect roomId reference in VoIP glare code
MatrixCall has a roomId property, but not a room_id property.
2017-03-02 13:36:49 +01:00
David Baker bdae9521cb Copy session ID too 2017-03-01 17:35:18 +00:00
David Baker cb7fb6c7be Keep data from response in any case
Fix up flows if they're absent
2017-03-01 17:10:08 +00:00
David Baker a33e4477af Comment the ignoring of failures 2017-03-01 16:35:37 +00:00
David Baker d435192e22 Remove unnecesary check 2017-03-01 16:33:55 +00:00
David Baker 9480447637 Comment stageState 2017-03-01 16:09:43 +00:00
David Baker 7dbe852606 Comment why we choose flows 2017-03-01 16:07:31 +00:00
David Baker d7216f44f5 PR feedback: Move requestEmailToken...
...to the UI component (in react-sdk)
2017-03-01 16:02:50 +00:00
Luke Barnard fdf09d46af Emit send_event_error when UnknownDeviceError occurs during VoIP operations (#378) 2017-03-01 09:13:57 +00:00
David Baker 32360e7473 Lint 2017-02-28 14:13:50 +00:00
David Baker 90482377b7 Fix linting 2017-02-28 14:08:41 +00:00
David Baker 08a3aea1c7 Fix tests 2017-02-28 13:44:44 +00:00
David Baker 6eebd1e957 Ignore failures when polling for auth completion 2017-02-27 17:23:02 +00:00
David Baker 033bd9bbdc Support polling
for out-of-band auth completion
2017-02-24 17:22:57 +00:00
David Baker af634d3a7d Add m.login.email.identity support to UI auth
Extends the interactive-auth to support m.login.email.identity
This also includes the ability to resume a UI auth session given
the approirpiate information (ie. to resume the auth flow having
click a link in a verification email).
2017-02-24 11:18:21 +00:00
David Baker d42ce3935b Merge pull request #379 from aviraldg/develop
add .editorconfig
2017-02-21 18:18:51 +00:00
Aviral Dasgupta 7a239c81f7 add .editorconfig
Signed-off-by: Aviral Dasgupta <me@aviraldg.com>
2017-02-21 23:40:00 +05:30
David Baker 2b96cd7059 Merge pull request #377 from matrix-org/kegan/indexeddb-account-data
Store account data in the same way as room data
2017-02-21 11:57:35 +00:00
Kegan Dougal 36b8b2c679 Unbreak tests. Add UT for account data 2017-02-21 11:51:45 +00:00
Kegan Dougal 69b3be2419 Store account data in the same way as room data
Previously, we treated the `MatrixEvents` that were in `this.accountData` in
`MatrixInMemoryStore` as the ground truth and saved those to disk and restored
them back upon load. This did not consider that there are **no emitted events**
when they are restored. Riot-Web was listening for a specific account data
event in order to dynamically update the theme. When the page was reloaded, we
dutifully put the right event in `MatrixInMemoryStore`, but failed to emit an
event to tell Riot-Web this. This led to vector-im/riot-web#3247

This patch fixes it by treating the `/sync` response as the ground truth and
ignoring `this.accountData` entirely. This means that upon load, we will be
injecting an `account_data` key into the initial `/sync` response. This will
cause it to be added to `this.accountData` in the store AND cause the event to
be emitted.
2017-02-21 11:39:34 +00:00
Richard van der Hoff bbe74e6987 Merge pull request #376 from matrix-org/rav/delay_otk_generation
Upload one-time keys on /sync rather than a timer
2017-02-21 08:46:59 +00:00
Richard van der Hoff 98d606fca4 Upload one-time keys on /sync rather than a timer
Delay the upload of one-time keys until we have received a sync *without any
to-device messages*. Doing so means that we can try to avoid throwing away our
private keys just before we receive the to-device messages which use them.

Once we've decided to go ahead and upload them, we keep uploading them in
batches of 5 until we get to the desired 50 keys on the server. We then
periodically check that there are still enough on the server.
2017-02-20 16:26:24 +00:00
David Baker 5c4472492a Un-hardcode home directory in jenkins.sh 2017-02-17 19:28:47 +00:00
David Baker a5c726eef9 Merge pull request #374 from matrix-org/kegan/sync-frequency
Increase the WRITE_DELAY on database syncing
2017-02-17 16:54:51 +00:00
Kegan Dougal d23d5b50a3 Increase the WRITE_DELAY on database syncing 2017-02-17 16:49:49 +00:00
David Baker 37e507707d Merge pull request #373 from matrix-org/kegan/delete-all-data-promise
Make deleteAllData() return a Promise
2017-02-17 15:11:09 +00:00
David Baker 44d889b99a Merge pull request #372 from matrix-org/dbkr/room_name_no_banned
Don't include banned users in the room name
2017-02-17 15:10:25 +00:00
Kegan Dougal 491092d040 Make deleteAllDate() return a Promise
It's required for the 'clear cache' button since we only want to refresh the
page after the transaction has gone through.
2017-02-17 15:03:50 +00:00
David Baker b296a1dabe Don't include banned users in the room name 2017-02-17 11:28:28 +00:00
Kegsay 63b8d45ef2 Merge pull request #363 from matrix-org/kegan/indexeddb
Support IndexedDB as a backing store
2017-02-17 11:17:57 +00:00
Kegan Dougal 8220fad855 Informative logging given it won't be spammy 2017-02-17 11:14:02 +00:00
Kegan Dougal 5aa146f0a6 Add function to UTs 2017-02-17 10:41:04 +00:00
Kegan Dougal f8d2661426 Add deleteAllData to the store API 2017-02-17 10:39:30 +00:00
Kegan Dougal 25b8027bd6 Pass in an optional database name to allow for more than one 2017-02-17 10:22:19 +00:00
Kegan Dougal c93c8a79b7 Add startup() to store API 2017-02-17 10:14:12 +00:00
Kegan Dougal 29336e260c Merge branch 'develop' into kegan/indexeddb 2017-02-17 09:41:16 +00:00
Richard van der Hoff 29942e5109 Merge pull request #370 from matrix-org/rav/sync_catchup
Poll /sync with a short timeout while catching up
2017-02-16 16:31:45 +00:00
Richard van der Hoff 926fee8493 fix typo 2017-02-16 16:16:13 +00:00
Kegan Dougal eedaacd256 Merge branch 'develop' into kegan/indexeddb 2017-02-16 16:10:56 +00:00
Kegan Dougal e926aa1bf8 Explain limits on maxTimelineEntries 2017-02-16 15:49:13 +00:00
Richard van der Hoff 597f981fec Poll /sync with a short timeout while catching up
On first connect, or after a disconnection, poll /sync with timeout=0 until
we get no to_device messages back. This will allow us to figure out whether we
have more to_device messages queued up for us on the server, which in turn will
help us fix a bug with clearing out one-time-keys too quickly.
2017-02-16 15:21:24 +00:00
Richard van der Hoff 777fdfbcfa Correct/improve comments about sync states 2017-02-16 14:03:41 +00:00
Kegan Dougal aafb587085 Don't break causality when rolling back state
We need to use the *previous* state when rolling back or else
causality breaks. Consider the messages:
 - m.room.member : Alice
 - Alice: 1
 - Alice: 2
 - m.room.member : Alice -> Bob
 - Bob: 3
 - Bob: 4
 If we roll back 4 messages (to Alice: 2), we want the rolled
 back m.room.member value to be "Alice" and NOT Bob. This
 means we need to look at the previous state of the m.room.member
 event and not the current state.
2017-02-16 12:35:34 +00:00
Kegan Dougal 1eb2576dbe More copyyright headers on touched files 2017-02-16 11:31:33 +00:00
Kegan Dougal 9a9646d012 Review comments 2017-02-16 11:28:51 +00:00
Richard van der Hoff 8ecb05a094 Merge pull request #368 from matrix-org/rav/fix_coverage
Make test coverage work again
2017-02-15 20:40:09 +00:00
Richard van der Hoff 8c8ca0584f fix package.json 2017-02-15 19:37:40 +00:00
Kegan Dougal b02ba08abc Remove debug comments 2017-02-15 16:53:31 +00:00
Richard van der Hoff 75d213f6b3 Make test coverage work again
* don't try to run npm under istanbul
* use _mocha instead of mocha because
  https://github.com/gotwarlost/istanbul/issues/44#issuecomment-16093330

Also:
* write a text report for better travis
* On jenkins, clear out old coverage reports so that we don't pick up last
  time's.
2017-02-15 16:52:19 +00:00
Kegan Dougal 9fdeb7a8e3 Add tests for the SyncAccumulator 2017-02-15 16:21:06 +00:00
David Baker 635b3dbccb Merge pull request #367 from matrix-org/dbkr/event_sender_docs
Add docs to event
2017-02-15 15:01:40 +00:00
David Baker e42af06609 spelling 2017-02-15 14:53:58 +00:00
David Baker 0416816329 Add docs to event
to say that sender won't always be set
2017-02-15 14:27:38 +00:00
David Baker 7f8f216263 Merge pull request #366 from matrix-org/rav/update_synctoken_more_often
Keep the device-sync token more up-to-date
2017-02-15 13:10:32 +00:00
Kegan Dougal 69ebcf08fc Bugfix where, upon refresh, it was impossible to write to the db (clone errors) 2017-02-14 17:28:56 +00:00
Kegan Dougal cbdb007b26 Persist receipt data 2017-02-14 16:51:27 +00:00
Kegan Dougal a28c03a2f9 Remove TODO checks and change copyright
Manually tested and it appears that Synapse will indeed send
down the complete state upon rejoining a room. \o/
2017-02-14 15:42:53 +00:00
David Baker 94a8915f6c Merge pull request #365 from matrix-org/rav/fix_devicelist_race_1
Fix race conditions in device list download
2017-02-14 10:46:48 +00:00
Richard van der Hoff be7192082a Keep the device-sync token more up-to-date
Update the sync token whenever the device list is in sync. Fixes
https://github.com/vector-im/riot-web/issues/3127.
2017-02-13 18:56:49 +00:00
Richard van der Hoff 54297cacd1 Fix race when downloading initial device change list
Fixes a problem if the user initiates a device query before the /changes
response comes back.
2017-02-13 18:33:02 +00:00
Richard van der Hoff 82f5997e61 Fix race condition in device list query
Fix a race where device list queries completing out-of-order could lead to us
thinking that we were more in-sync than we actually were.
2017-02-13 18:33:02 +00:00
David Baker 4d2bc88305 Merge pull request #364 from matrix-org/dbkr/fix_unban
Fix the unban method
2017-02-13 18:06:35 +00:00
David Baker cd8dfa331a Fix the unban method
Which didn't work because of
https://github.com/matrix-org/synapse/issues/1860
2017-02-13 18:00:28 +00:00
Richard van der Hoff 45d22c6196 Merge pull request #362 from matrix-org/rav/no_freeze_on_device_download
Spread out device verification work
2017-02-13 11:54:59 +00:00
Richard van der Hoff eeed11e283 Fix integ tests
Two tweaks:
 * `httpBackend.flush()` now returns a value, so we can't pass its result
   straight into `done()`.
 * In one of the megolm tests, we need to wait for the device query to finish
   before marking the relevant device as known. One easy way to do this is
   actually to try sending the message first - that will block until the device
   query completes.
2017-02-13 11:41:13 +00:00
Richard van der Hoff 476333b3fc Fix comment typo 2017-02-13 11:28:19 +00:00
Kegan Dougal c7fdbd1c64 Persist notification counts and treat them as clobbers 2017-02-10 17:15:17 +00:00
Kegan Dougal 0b9bc2a7a7 Remove old (de)serialize stuff. Inline more things 2017-02-10 16:51:55 +00:00
Kegan Dougal 6949b6666e Revert non-functional changes to other files 2017-02-10 16:13:13 +00:00
Kegan Dougal a380b6803a Merge branch 'develop' into kegan/indexeddb 2017-02-10 16:11:05 +00:00
Kegan Dougal 6696c712d3 Bug fixes with the accumualtor
- Only take invite_state as there is a Room attached to invite objects
  which then fail to be persisted.
- Store empty string state keys!
2017-02-10 15:19:50 +00:00
Kegan Dougal 0d4833d6e3 Make it all work! 2017-02-10 14:27:56 +00:00
Richard van der Hoff 207bce61ad Spread out device verification work
Avoid a big freeze when we process the results of a device query, by splitting
the work up by user.
2017-02-10 13:37:41 +00:00
Richard van der Hoff 0f1d367b80 Merge pull request #361 from matrix-org/rav/moar_uisi_logging
Clean up/improve e2e logging
2017-02-10 10:51:19 +00:00
Richard van der Hoff bf2e6a33c2 Minor post-review tweaks 2017-02-10 10:37:46 +00:00
Richard van der Hoff b66fed9ae9 Clean up/improve e2e logging
In an attempt to make the rageshake logs a bit more useful, try to make the
logging a bit saner. Firstly, make sure we log every decryption failure, and
log it exactly once, rather than in several places. Also record when we receive
megolm keys. Also add some more explicit logging in the sync loop.
2017-02-09 17:36:22 +00:00
Richard van der Hoff 4897287fda Merge pull request #360 from matrix-org/rav/fix_decrypt_after_keys_arrive
Fix decryption of events whose key arrives later
2017-02-09 17:33:45 +00:00
Kegan Dougal cc1daa5a54 Fix tests due to API breakage 2017-02-09 17:32:23 +00:00
Kegan Dougal 6e7b9472be Add a save() method to the store interface
Originally I just piggy-backed off setSyncToken() but this was]
non-obvious that depending on the store it might do writes to the
database. This was exacerbated by the fact that the save needs to
be done at the "right time" (after sync data is accumulated and
after the sync response has been processed) and setSyncToken was
being called too early.

A save() method fixes both these things.
2017-02-09 17:25:40 +00:00
Richard van der Hoff e13ed6436e Fix decryption of events whose key arrives later
Re-fixes https://github.com/vector-im/riot-web/issues/2273.

And test it this time.
2017-02-09 16:12:43 +00:00
Kegan Dougal db24690d9b Fix broken tests 2017-02-09 13:37:47 +00:00
Kegan Dougal 3a39fd23c4 Add glue code to hook up the sync accumulator
The user of the SDK is responsible for DIing the main components:

  let store = new IndexedDBStore(
    new IndexedDBStoreBackend(window.indexedDB),
    new SyncAccumulator(),
  });
  await store.startup();
  let client = matrix.createClient({store: store});
2017-02-09 12:49:10 +00:00
Richard van der Hoff d471277031 Merge pull request #359 from matrix-org/rav/remove_redundant_invalidate
Invalidate device lists when encryption is enabled in a room
2017-02-09 12:36:02 +00:00
Kegan Dougal ef57b88343 Remember next_batch tokens in the accumulator. Glue it in to SyncApi 2017-02-09 11:05:16 +00:00
Kegan Dougal 1e3fcdc109 Implement getJSON() 2017-02-09 10:50:31 +00:00
Kegan Dougal 606b9718a3 Implement timeline accumulation
Including pruning based on maxTimelineEntries
2017-02-09 10:14:10 +00:00
Kegan Dougal ab6e30da28 Merge branch 'develop' into kegan/indexeddb 2017-02-09 09:57:36 +00:00
Richard van der Hoff 0baea5c1a6 Invalidate device lists when encryption is enabled in a room
Fixes https://github.com/vector-im/riot-web/issues/2672
2017-02-08 23:23:46 +00:00
Richard van der Hoff bd07310e15 Remove redundant invalidation of our own device list
89ced198 added some code which flagged our own device list as in need of an
update. However, 8d502743 then added code such that we invalidate *all* members
of e2e rooms on the first initialsync - which should include ourselves. We can
therefore remove the redundant special-case, which mostly serves to simplify
the tests.
2017-02-08 23:04:23 +00:00
Richard van der Hoff bf227508ce matrix-client-crypto.spec: check no outstanding http expectations 2017-02-08 18:17:43 +00:00
Richard van der Hoff 1c1ba58579 Don't force device list download on every message in olm room
we only really hit this in the tests, but it's a bit silly to download the
complete device list on every message.
2017-02-08 18:10:01 +00:00
Richard van der Hoff a73f898bb9 try to make mock-request logging saner 2017-02-08 17:26:45 +00:00
Richard van der Hoff ee8a52d0c0 Merge pull request #358 from matrix-org/rav/mocha
Switch from jasmine to mocha + expect + lolex
2017-02-08 16:50:11 +00:00
Richard van der Hoff d5e87a537c Tell eslint we are using mocha rather than jasmine 2017-02-08 16:41:43 +00:00
Richard van der Hoff 18d3786676 Remove jasmine-node 2017-02-08 15:48:53 +00:00
Richard van der Hoff c906dbad75 jenkins.sh: clean out old reports 2017-02-08 15:14:17 +00:00
Kegan Dougal db20fc7831 Accumulate current room state 2017-02-08 15:08:38 +00:00
Richard van der Hoff bd226d94d8 Switch from jasmine to mocha + expect + lolex
Much of this transformation has been done automatically:
 * add expect import to each file
 * replace `not.to` with `toNot`
 * replace `to[Not]Be{Undefined,Null}` with equivalents
 * replace `jasmine.createSpy(...)` with `except.createSpy`, and `andCallFake`
   with `andCall`

Also:
 * replace `jasmine.createSpyObj` with manual alternatives
 * replace `jasmine.Clock` with `lolex`
2017-02-08 14:32:37 +00:00
Richard van der Hoff 8a487ca1bc Merge branch 'rav/source_map_support' into develop 2017-02-08 11:35:27 +00:00
David Baker 7b43a34860 Merge pull request #357 from matrix-org/rav/search_no_undefined_keys
searchMessageText: avoid setting keys=undefined
2017-02-08 10:58:44 +00:00
David Baker 6a60585123 Merge pull request #355 from matrix-org/rav/realtime_callbacks_this
realtime-callbacks: pass `global` as `this`
2017-02-08 10:57:06 +00:00
David Baker 0f7ab32777 Merge pull request #354 from matrix-org/rav/fix_tests_without_olm
Make the tests work without olm
2017-02-08 10:55:41 +00:00
Richard van der Hoff ffeaf2dec0 searchMessageText: avoid setting keys=undefined
This doesn't make any difference to the JSON, but it upsets `expect`.
2017-02-08 09:20:23 +00:00
Richard van der Hoff 3277820381 realtime-callbacks: pass global as this
We had a test for this, but apparently it wasn't testing it right...
2017-02-08 07:34:30 +00:00
Richard van der Hoff 80d0aadbd0 Install source-map-support in each test
This makes exception traces use the source map, which is much more helpful when
debugging.
2017-02-07 22:57:09 +00:00
Richard van der Hoff d744cecbfa Make the tests work without olm
Olm is an optional dependency, so the tests should work without it.
2017-02-07 22:56:56 +00:00
David Baker d453813db4 Merge pull request #353 from matrix-org/rav/refactor_e2e_tests
Tests: Factor out TestClient and use it in crypto tests
2017-02-06 16:45:52 +00:00
Kegan Dougal c06ebd1e4a Implement /sync accumulation for everything but room timelines/state 2017-02-06 16:45:25 +00:00
David Baker 10af2d0560 Merge pull request #352 from matrix-org/rav/device_list_lint
Fix some lint
2017-02-06 16:18:48 +00:00
Richard van der Hoff 42f2dafb40 Tests: Factor out TestClient and use it in crypto tests 2017-02-06 10:50:51 +00:00
Richard van der Hoff 6ca7661510 Fix some lint 2017-02-06 10:33:50 +00:00
David Baker 4388cc207f Merge pull request #351 from matrix-org/rav/sign_source_tarball
Make a sig for source tarballs when releasing
2017-02-06 11:32:45 +01:00
David Baker 07f14538e3 Merge pull request #350 from matrix-org/rav/no_merge_prerel
When doing a pre-release, don't bother merging to master and develop.
2017-02-06 11:28:46 +01:00
Richard van der Hoff 3d9173a877 Make a sig for source tarballs when releasing
When we are doing a signed release, also create a sig for the 'archive' tarball
which github creates for us.

Fixes https://github.com/vector-im/riot-web/issues/3024.
2017-02-04 11:10:40 +00:00
Richard van der Hoff cb88f53587 When doing a pre-release, don't bother merging to master and develop.
This assumes that we'll eventually do a proper release (or merge the prerel
manually), and saves twiddling the package.json on downstream projects for each
prerelease.
2017-02-04 11:07:14 +00:00
Richard van der Hoff d76e8be4ff Merge branch 'release-v0.7.5' 2017-02-04 10:22:04 +00:00
Richard van der Hoff e8c6002d08 v0.7.5 2017-02-04 10:15:09 +00:00
Richard van der Hoff d9033812a2 Prepare changelog for v0.7.5 2017-02-04 10:15:02 +00:00
Richard van der Hoff 2e6b93f886 v0.7.5-rc.3 2017-02-03 15:24:28 +00:00
Richard van der Hoff afc4e145b6 Prepare changelog for v0.7.5-rc.3 2017-02-03 15:24:21 +00:00
Richard van der Hoff cee243a2a2 prep changelog 2017-02-03 15:21:15 +00:00
Richard van der Hoff 5fd74109ff Fix device list update
s/flushNewDeviceRequests/refreshOutdatedDeviceLists/ - this got fixed on one PR
and apparenlty I failed to merge the changes correctly
2017-02-03 14:29:50 +00:00
Richard van der Hoff a3cc8eb1f6 Include DeviceInfo in deviceVerificationChanged events
... to help the UI update itself
2017-02-03 14:27:08 +00:00
David Baker bd4de4832c v0.7.5-rc.2 2017-02-03 13:01:15 +00:00
David Baker 9e74c934a1 Prepare changelog for v0.7.5-rc.2 2017-02-03 13:01:14 +00:00
David Baker a056d4916a Prepare changelog for v0.7.5-rc.2 2017-02-03 13:00:26 +00:00
David Baker 31630859a2 Prepare changelog for v0.7.5-rc.2 2017-02-03 12:57:45 +00:00
David Baker 8cb41f6797 Merge remote-tracking branch 'origin/develop' into release-v0.7.5 2017-02-03 12:54:32 +00:00
Richard van der Hoff c3a8aeca42 Merge pull request #348 from matrix-org/rav/device_list_stream
Use the device change notifications interface
2017-02-03 12:49:33 +00:00
Richard van der Hoff eaa95fb1e5 Merge pull request #347 from matrix-org/rav/rewrite_device_query_logic
Rewrite the device key query logic
2017-02-03 12:49:11 +00:00
David Baker 8e6bca34d7 v0.7.5-rc.1 2017-02-03 12:05:50 +00:00
David Baker 1cdffa2c81 Prepare changelog for v0.7.5-rc.1 2017-02-03 12:05:50 +00:00
Kegan Dougal 0e4eebbb39 Add bare bones SyncAccumulator class with comments 2017-02-03 11:42:04 +00:00
Richard van der Hoff 8441589ce6 Merge pull request #336 from matrix-org/matthew/blacklist-unverified
Support for blacklisting unverified devices, both per-room and globally
2017-02-03 10:26:59 +00:00
David Baker b52ba89cec Merge pull request #349 from matrix-org/matthew/track-event-error2
track errors when events can't be sent
2017-02-03 09:35:24 +00:00
Matthew Hodgson b99e1205c4 track errors when events can't send 2017-02-03 00:44:12 +00:00
Richard van der Hoff 8d502743a5 Refresh device list on startup
On initialsync, call the /keys/changes api to see which users have updated
their devices. (On failure, invalidate all of them).
2017-02-03 00:33:56 +00:00
Richard van der Hoff 732a764ec6 Refactor crypto initialsync handling
Pass a store into the Crypto object so that it doesn't need to make assumptions
about the EventEmitter, and use the new metadata on sync events to distinguish
between initialsyncs and normal syncs
2017-02-03 00:33:54 +00:00
Richard van der Hoff 9975786bac Store the token corresponding to the last device update in localstorage
... so that we can, in future, use it when restarting the client.
2017-02-03 00:32:24 +00:00
Richard van der Hoff 89ef4aa6e7 Handle device change notifications from /sync
When we get a notification from /sync that a user has updated their device
list, mark the list outdated, and then fire off a device query.
2017-02-03 00:32:16 +00:00
Richard van der Hoff 7e82ac3620 Merge branch 'develop' into rav/rewrite_device_query_logic 2017-02-03 00:12:46 +00:00
Richard van der Hoff c3440c506c Address review comments
Update some comments, and s/flushNewDeviceRequests/refreshOutdatedDeviceLists/.
2017-02-03 00:10:13 +00:00
Richard van der Hoff f16ef93cc3 package.json: Add .babelrc to files
... in the hope that this will mean it gets included when `npm install`ing
matrix-js-sdk into riot-web, and hence stopping babel picking up riot-web's
.babelrc.
2017-02-02 22:14:24 +00:00
Matthew Hodgson b6f3fc5466 Merge branch 'develop' into matthew/blacklist-unverified 2017-02-02 22:02:22 +00:00
Richard van der Hoff 6690f59410 Merge pull request #346 from matrix-org/rav/factor_out_devicelist
Factor out device list management
2017-02-02 19:40:27 +00:00
Richard van der Hoff 65e08b9633 Fix copyright 2017-02-02 19:39:40 +00:00
Matthew Hodgson 2e916e63f5 Merge pull request #335 from matrix-org/matthew/warn-unknown-devices
Support for warning users when unknown devices show up
2017-02-02 18:08:39 +00:00
Kegan Dougal f531b9fb21 Linting 2017-02-02 17:47:35 +00:00
Kegan Dougal 9581e48bcb Load/Store account data events 2017-02-02 17:41:49 +00:00
Kegan Dougal ad9d58ebc2 Persist User objects. Back out everything else.
We can reasonable persist User and account data objects. Other
objects get into horrible circular loops. We'll rethink how this
is being done.
2017-02-02 17:25:20 +00:00
Kegan Dougal 896fc5a3f0 Periodically update the database
Triggered by /sync responses, rather than timers, but with a minimum
delay time (1m).
2017-02-02 14:54:07 +00:00
Richard van der Hoff 94addb6315 Rewrite the device key query logic
Only permit one query per user at a time.
2017-02-02 13:49:43 +00:00
Kegan Dougal 2c6d4c5a5c Implement deserialize() on all parts of the object graph 2017-02-02 12:38:03 +00:00
Richard van der Hoff f81d6b6157 Factor out device list management
crypto/index.js was getting huge, so move the device list update management out
to a separate file.

This shouldn't have any effect on functionality.
2017-02-02 10:18:30 +00:00
Kegan Dougal ef17214ae2 Add remaining load functions 2017-02-02 10:18:09 +00:00
Matthew Hodgson 5d544c773d Merge branch 'develop' into matthew/warn-unknown-devices 2017-02-01 22:35:25 +00:00
Kegan Dougal 721b9df35d Add deserialize() static functions for User and MatrixEvent
The intent here is to make it possible to repopulate User objects from an
IndexedDB object store, which stores things according to the structured-clone
algorithm. We can't just `Object.assign` everything because it would assign
JSON objects to fields which should be classes (eg `MatrixEvent`).
2017-02-01 10:35:55 +00:00
David Baker 526fbfa8f1 Merge pull request #345 from matrix-org/rav/browserify_sourcemaps
Enable sourcemaps in browserified distro
2017-02-01 10:14:58 +00:00
Kegan Dougal 0317830b12 Check for a deserialize() function 2017-02-01 09:49:55 +00:00
Kegan Dougal dfd8c56838 Factor out upsert. Check for serialize() function on inserted objects 2017-02-01 09:42:20 +00:00
Richard van der Hoff b7e33b237b Update browserify
... to a more recent release, for compatibility with sourceify.
2017-02-01 00:50:22 +00:00
Kegan Dougal 025cb8bd91 Repopulate as User objects, not JSON objects 2017-01-31 17:33:12 +00:00
Kegan Dougal fa89f2be77 Start loading content from IndexedDB. Add idealised usage example. 2017-01-31 16:53:26 +00:00
Kegan Dougal bf008a1bee Fix build and add stub store connect() 2017-01-31 16:33:43 +00:00
Kegan Dougal 8656ad584b Introduce the concept of IndexedDBStoreBackend
So IndexedDBStore can meet the Store interface and we don't need to mess
with the guts of MatrixInMemoryStore.
2017-01-31 16:08:05 +00:00
Kegan Dougal e316a9a5b3 Merge branch 'develop' into kegan/indexeddb 2017-01-31 15:04:43 +00:00
Richard van der Hoff 7e3a146240 Enable sourcemaps in browserified distro
* use sourceify to read the sourcemaps written by babel
* use {browserify, watchify} -d to write sourcemap
* use exorcist to write the sourcemap to a separate file
* add some uglifyjs incantations to read and write sourcefiles
* simples
2017-01-30 19:00:51 +00:00
Richard van der Hoff 2395d2856b Merge pull request #344 from matrix-org/rav/allow_e2e_options
Record all e2e room settings in localstorage
2017-01-30 16:26:23 +00:00
David Baker 34ae473e6f Merge pull request #340 from matrix-org/rav/allow_olm_with_browserify
Make Olm work with browserified js-sdk
2017-01-30 15:05:22 +00:00
Richard van der Hoff 656c54ead9 Record all e2e room settings in localstorage
I can't quite remember what the logic behind only recording the algorithm in
localstorage was, but the upshot is that if you try to set any e2e config
options (such as the megolm rotation periods) via the room state, then the
state gets rejected and you can't send any events.
2017-01-30 11:40:09 +00:00
Richard van der Hoff c6e21c9c5c Make Olm work with browserified js-sdk
We want to avoid distributing olm as part of the js-sdk, so we exclude it from
the browserified build. Previously this meant that you couldn't use olm this
way.

We can do better though: if the web page includes olm.js via a separate script
tag, that will set global.Olm, which we can get browserify to shim in.
2017-01-28 15:35:55 +00:00
Richard van der Hoff e71e32122c Merge pull request #339 from matrix-org/rav/no_require_browserify
Make browserify a dev dependency
2017-01-26 17:20:47 +00:00
Kegan Dougal 522105a858 First cut 2017-01-26 16:57:59 +00:00
Richard van der Hoff e3b008e19a Make browserify a dev dependency 2017-01-26 16:40:19 +00:00
Kegan Dougal 776cfed2b3 Merge branch 'develop' into kegan/indexeddb 2017-01-26 14:29:30 +00:00
Richard van der Hoff 85cf2a3692 Fix lint 2017-01-26 13:29:56 +00:00
Richard van der Hoff c9b700ef6a Merge branch 'matthew/warn-unknown-devices' into matthew/blacklist-unverified 2017-01-26 13:25:10 +00:00
Richard van der Hoff 34fde7d16d Store device 'known' status in session store 2017-01-26 13:15:50 +00:00
Matthew Hodgson 5911c4d2db don't automatically mark devices as known; require the app to do it 2017-01-25 23:53:51 +01:00
Matthew Hodgson dfae72e9af Merge branch 'matthew/warn-unknown-devices' of git+ssh://github.com/matrix-org/matrix-js-sdk into matthew/warn-unknown-devices 2017-01-25 23:35:23 +01:00
Richard van der Hoff 085493d580 Fix tests 2017-01-25 14:59:14 +00:00
Richard van der Hoff 5245c7f2ab Merge remote-tracking branch 'origin/develop' into matthew/warn-unknown-devices 2017-01-25 11:03:23 +00:00
Richard van der Hoff 4ccd649358 Address my own review comments 2017-01-25 11:02:49 +00:00
Richard van der Hoff 32f42d59b1 Merge pull request #338 from matrix-org/dbkr/brace_style_allow_single_line
Allow single line brace-style
2017-01-23 14:54:18 +00:00
David Baker ca618f2bf2 Allow single line brace-style
As we sometimes use (x) => {foo = x;}, especially for react
components, but probably no reason not to allow it generally.
2017-01-23 14:18:09 +00:00
Richard van der Hoff a0ae0b3922 Merge pull request #333 from matrix-org/dbkr/comma_dangle_function
Turn on comma-dangle for function calls
2017-01-23 10:44:56 +00:00
David Baker a09329949a Make comma dangle an error
& put max warnings back down
2017-01-23 10:18:22 +00:00
David Baker b67e360302 Run the test code through babel
Needs it now we use dangling commas on functio calls because
node doesn't like that
2017-01-23 10:08:59 +00:00
Matthew Hodgson 3d30ad843f make it work 2017-01-22 01:29:33 +01:00
Matthew Hodgson d37935dd78 actually consider _crypto.getGlobalBlacklistUnverifiedDevices 2017-01-21 17:51:48 +00:00
Matthew Hodgson 512d5882c9 track whether we blacklist unverified devices per-room & globally 2017-01-21 17:38:35 +00:00
Matthew Hodgson 247deacbb7 some incoherent jottings on the warning semantics 2017-01-21 17:36:26 +00:00
Matthew Hodgson e79926db6c fix lint 2017-01-21 05:26:01 +00:00
Matthew Hodgson 34a0bd4c38 oops, unbreak it 2017-01-21 05:13:12 +00:00
Matthew Hodgson fb820fa9a7 experimental support for warning users when unknown devices show up in a room.
hopefully a step towards fixing https://github.com/vector-im/riot-web/issues/2143
2017-01-21 05:10:51 +00:00
David Baker 423175f539 eslint --fix for dangley commas on function calls 2017-01-20 16:12:02 +00:00
David Baker 007848c42e Turn on comma-dangle for function calls
Our code style mandates this, but it's not the default.

Also use the babel-eslint parser because the standard one doesn't
support dangling commas on functions.
2017-01-20 12:42:57 +00:00
Richard van der Hoff 49e6fd3c60 Merge pull request #331 from matrix-org/dbkr/prefer_const
Add prefer-const
2017-01-19 18:28:13 +00:00
David Baker 80129e7483 Fix last prefer-const, decrease max warnings
and make prefer-const an error
2017-01-19 18:24:28 +00:00
David Baker dc74a2326f Fix some more consts 2017-01-19 18:11:09 +00:00
David Baker fc0117869d Update max warnings 2017-01-19 17:44:48 +00:00
David Baker 7bca05af64 eslint ---fix for prefer-const 2017-01-19 17:42:10 +00:00
David Baker 9b354ba99e Merge remote-tracking branch 'origin/develop' into dbkr/prefer_const 2017-01-19 17:05:54 +00:00
Richard van der Hoff 194fad7445 Stop linting on npm install
It's annoying to run the linter on npm install, because it slows down the
react-sdk and riot-web builds.

The fact that `npm install` runs `prepublish` is going to be changed in npm 4
(see https://github.com/npm/npm/pull/14290), but for now this is the best bet.
2017-01-19 11:13:51 +00:00
Richard van der Hoff 18001dc539 jenkins.sh: stop if npm i fails
(There is no point going on)
2017-01-19 10:50:28 +00:00
Richard van der Hoff 78031f2c04 Merge pull request #326 from matrix-org/rav/megolm_export
Support for importing and exporting megolm sessions
2017-01-19 03:04:35 +00:00
Richard van der Hoff 07261fa5d9 Bump to Olm 2.2.1 2017-01-19 02:53:49 +00:00
David Baker aa4ffc7bda Add prefer-const
Our code style says we prefer consts, so add it to the linter.
2017-01-18 15:53:08 +00:00
David Baker cee7f7a280 Merge pull request #329 from matrix-org/kegan/eslint2
Fix linting on all tests
2017-01-17 09:55:37 +00:00
David Baker c7f173bbb2 v0.7.4 2017-01-16 13:12:18 +00:00
David Baker 118bd75a68 Prepare changelog for v0.7.4 2017-01-16 13:12:18 +00:00
David Baker 9a593f147f Fix non-screensharing calls 2017-01-16 13:12:18 +00:00
David Baker dfcbdeb002 v0.7.4-rc.1 2017-01-16 13:12:18 +00:00
David Baker df20365a6d Prepare changelog for v0.7.4-rc.1 2017-01-16 13:12:18 +00:00
David Baker 9c6973a46a v0.7.4 2017-01-16 13:03:35 +00:00
David Baker b439be8fb5 Prepare changelog for v0.7.4 2017-01-16 13:03:34 +00:00
David Baker edaf84a78f Fix non-screensharing calls 2017-01-16 12:28:52 +00:00
Kegan Dougal 056ed3b3c4 Reduce max warnings 2017-01-16 10:31:00 +00:00
Kegan Dougal 317898d41c Fix linting on all tests
Manually.
2017-01-16 10:28:51 +00:00
Richard van der Hoff c8b26eeac4 Support for importing megolm session keys 2017-01-14 00:45:03 +00:00
Richard van der Hoff 766d8f0ba4 Support for exporting megolm session data 2017-01-14 00:45:03 +00:00
Kegan Dougal e159e504fa Use ev.target.result for consistency 2017-01-13 17:57:41 +00:00
Kegan Dougal 8bcb048f53 Very rough WIP of an IndexedDBStore 2017-01-13 16:22:59 +00:00
David Baker 0fa9f7c609 Merge pull request #325 from matrix-org/kegan/fix-eslint
Fix ESLint warnings and errors
2017-01-13 15:07:09 +00:00
David Baker c24b580165 Merge pull request #324 from matrix-org/kegan/rm-webstorage
BREAKING CHANGE: Remove WebStorageStore
2017-01-13 14:58:53 +00:00
Kegan Dougal cc39ede920 Reduce max warnings and fix lint errors 2017-01-13 14:36:48 +00:00
Kegan Dougal 7a4ef7b257 Merge branch 'kegan/rm-webstorage' into kegan/fix-eslint 2017-01-13 14:35:01 +00:00
Kegan Dougal 8c52870e07 Dummy commit 2017-01-13 12:03:44 +00:00
Kegan Dougal 922dd6cf9c Merge branch 'kegan/rm-webstorage' into kegan/fix-eslint 2017-01-13 11:57:48 +00:00
Kegan Dougal 9633532dd4 Bump to node 6 to fix 'SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode' 2017-01-13 11:57:33 +00:00
Kegan Dougal 5abf6b9f20 Manually patch up files which were formatted wrong
`eslint --fix` expands `if` statements incorrectly (wrong indentation).
2017-01-13 11:50:00 +00:00
Kegan Dougal 478550ec93 Not sure how this ever worked, but fix it since it wasn't working 2017-01-13 11:03:00 +00:00
David Baker bb7c9b2227 v0.7.4-rc.1 2017-01-13 10:57:51 +00:00
David Baker 8538d7881a Prepare changelog for v0.7.4-rc.1 2017-01-13 10:57:50 +00:00
Kegan Dougal 5f28bc4468 Fix errors (line limits) 2017-01-13 10:55:17 +00:00
Kegan Dougal 7ed65407e6 Pass through eslint --fix 2017-01-13 10:49:32 +00:00
Kegan Dougal 97e421306b Remove UTs 2017-01-13 10:47:20 +00:00
Kegan Dougal 1ce9e7c6bb BREAKING CHANGE: Remove WebStorageStore
This will be replaced with an IndexedDB style solution. Maintaining 2 different
persistent stores is not my idea of fun.
2017-01-13 10:44:20 +00:00
David Baker df4ae597a5 Include eslint config files in distribution
Otherwise linting will fail if you try to run it
2017-01-12 16:54:49 +00:00
David Baker 8c512bce9e Use less ancient version of node
ESLint errors with old node (at least without --harmony) so use
a more sensible version
2017-01-12 16:20:49 +00:00
David Baker 4775010452 Be quiet npm
we don't need the commands you're running in the xml file
2017-01-12 16:07:44 +00:00
David Baker 03f4b15c61 Merge pull request #321 from matrix-org/dbkr/dont_polyfill
Remove babel-polyfill
2017-01-12 15:56:57 +00:00
David Baker d8c23c0dcb Merge pull request #320 from matrix-org/dbkr/build_process_es6
Update build process for ES6
2017-01-12 15:50:35 +00:00
David Baker 4ab261b89f Add eslint:recommends
Turn off / tweak some options from it. Fix a double-definition.
Add an eslint config to the spec directory to tell it about the
jasmine magic globals.
2017-01-12 15:05:42 +00:00
David Baker e057956ede Add google eslint rules as a base
Remove some we don't care about. Set some other ones we do care
about but don't currently adhere to to warn. Set the max warnings
threshold to the current number of warnings, so we don't introduce
more of them. Fix a bunch of legit lint errors and add exceptions
to various places in the test code that does funny things with
'this'.
2017-01-12 14:35:58 +00:00
David Baker 543b9cf0ce Run lint on prepublish, not build
and make everything errors, so now you can do local builds with
lint failures, but CI will fail and you can't release.
2017-01-12 12:57:24 +00:00
David Baker 591b56d794 it's build now, not compile 2017-01-12 12:55:50 +00:00
David Baker 7f8375d864 Lint spec as well as src 2017-01-12 12:51:59 +00:00
David Baker 31af4bbeb5 Fix jsdoc errors in spec/ 2017-01-12 12:51:22 +00:00
David Baker 0a11404be2 Fix legitimate JSDoc errors 2017-01-12 11:46:07 +00:00
David Baker ff723980ac Add exceptions to eslintrc for JSDoc
To allow things we've been OK about previously
2017-01-12 11:26:17 +00:00
David Baker 0dfd60ad5e Merge compile target into build 2017-01-11 19:02:25 +00:00
David Baker 18f57a2100 Remove babel-polyfill
react-sdk's tests are failing because babel-polyfill is being
pulled in twice.

As per https://babeljs.io/docs/usage/polyfill/ babel-polyfill is
"intended to be used in an application rather than a library/tool".
From a library, we're limited to things that don't modify globals,
since the js env is not ours to start screwing around with.
2017-01-11 18:52:49 +00:00
David Baker 9b5cb3a631 Update build process for ES6
* Make npm run build run npm compile (it needs the output)
 * Switch to ESlint so we can actually use ES6 without the linter
   crying.
2017-01-11 18:11:47 +00:00
David Baker 09e4e4709f Merge pull request #319 from matrix-org/dbkr/babel_is_not_a_package
'babel' is not a babel package anymore
2017-01-11 17:35:57 +00:00
David Baker 00895f00e6 Empty commit to force rebuild 2017-01-11 17:30:15 +00:00
David Baker c57be7b966 'babel' is not a babel package anymore
and so is just redundant as you have babel-cli which is what you
actually want.
2017-01-11 17:17:19 +00:00
Kegsay 51d94e63f4 Merge pull request #318 from matrix-org/kegan/es6
Add Babel for ES6 support
2017-01-11 11:47:19 +00:00
Kegan Dougal 6daeec838f Add start script 2017-01-11 11:25:24 +00:00
Kegan Dougal 53f23939c1 Review comments 2017-01-11 10:59:33 +00:00
Kegan Dougal 0bbec9e182 Appease linters 2017-01-11 10:40:20 +00:00
Kegan Dougal 101970dcd9 Merge branch 'develop' into kegan/es6 2017-01-11 10:35:01 +00:00
Kegan Dougal 6644151d19 Add babel-polyfill to include more ES6 goodies 2017-01-11 10:32:52 +00:00
Kegan Dougal 548ffdced1 es2015 not env 2017-01-11 10:27:51 +00:00
Kegan Dougal cba4b24b23 Add /lib to ignore as with react SDK 2017-01-11 10:24:31 +00:00
Kegan Dougal 3e922c2d41 Add babel dep and config 2017-01-11 10:19:46 +00:00
Kegan Dougal ae6a409cc2 Move /lib to /src 2017-01-11 10:09:04 +00:00
David Baker 94d79edbd0 Merge pull request #317 from matrix-org/dbkr/move_screen_sharing_error
Move screen sharing check/error
2017-01-11 10:08:12 +00:00
David Baker 4dc331d629 Move screen sharing check/error
Because the https check only applies in the browser
2017-01-10 18:36:54 +00:00
David Baker 18131735d7 Cheekily fix screen sharing with audio 2017-01-10 14:24:09 +00:00
Richard van der Hoff 2a51e7a665 Merge pull request #316 from matrix-org/kegan/release-script-dies-if-uncommitted-changes
release.sh: Bail early if there are uncommitted changes
2017-01-04 14:05:23 +00:00
Kegan Dougal df7ac77113 Review comments 2017-01-04 13:57:51 +00:00
Kegan Dougal 1b222249c4 Reset ret before reusing it 2017-01-04 11:56:01 +00:00
Kegan Dougal 126967cb90 release.sh: Bail early if there are uncommitted changes 2017-01-04 11:47:33 +00:00
Kegan Dougal 2afa381cae Merge branch 'master' into develop 2017-01-04 11:34:24 +00:00
Kegan Dougal 05cbc217a0 Add more docs to explain how to do releases 2017-01-04 11:27:34 +00:00
Kegan Dougal 54eec40d20 v0.7.3 2017-01-04 11:25:43 +00:00
Kegan Dougal 3ab34f911b Prepare changelog for v0.7.3 2017-01-04 11:25:42 +00:00
Kegan Dougal d6e4d0a417 Remove RELEASING.md
vdh says it is out of date and is misleading and should be removed, so removing it.
2017-01-04 11:16:59 +00:00
Kegan Dougal fac40f5183 Styling and ES5 only 2017-01-04 11:10:24 +00:00
Kegsay ce684a6628 Merge pull request #310 from Deadid/develop
User presence list feature
2017-01-04 10:38:09 +00:00
Richard van der Hoff 14fac241f7 bump to olm 2.1.0 2016-12-23 14:36:22 +00:00
Sergiy Makhov 335579e250 Changed paramater type to string array. Removed bad doc 2016-12-23 08:51:41 +02:00
Kegsay c8565be3a5 Merge pull request #313 from matrix-org/kegan/timeouts-for-everyone
Allow clients the ability to set a default local timeout
2016-12-22 17:17:25 +00:00
Kegan Dougal 76e76269cf Review comments 2016-12-22 17:09:41 +00:00
Kegan Dougal 3c43e2718d More JSDoc 2016-12-22 17:07:52 +00:00
Kegan Dougal f2676772c8 Allow clients the ability to set a default local timeout
Will be used to fix matrix-org/matrix-appservice-irc#328
2016-12-22 16:54:42 +00:00
David Baker c9bf4270fc Merge pull request #312 from matrix-org/dbkr/delete_threepid
Add API to delete threepid
2016-12-22 15:06:54 +00:00
David Baker 41ddb7660b Add doc details 2016-12-22 15:01:20 +00:00
Luke Barnard b3e93ffadf Add getDate function to MatrixEvent (#311)
* Add `getDate` function to MatrixEvent

`getDate` can be used to get the timestamp of the event as a `Date` instance. 

Adds handleRemoteEcho function to be called on change of internal event of MatrixEvent, so that the internal `_date` can be updated when a remote echo replaces a local one.
2016-12-22 14:07:26 +00:00
David Baker 582576b1c3 Add API to delete threepid
Uses https://github.com/matrix-org/synapse/pull/1714
2016-12-21 18:48:42 +00:00
David Baker 456135a6ec get latest changes after updating changelog 2016-12-16 17:39:24 +00:00
Luke Barnard c7357952ec v0.7.2 2016-12-15 17:37:22 +00:00
Luke Barnard b796246d9d Prepare changelog for v0.7.2 2016-12-15 17:37:22 +00:00
Sergiy Makhov 598e48e39b User presence list feature 2016-12-15 13:22:25 +02:00
Richard van der Hoff 2e3c349c5e Merge pull request #309 from matrix-org/rav/fix_olm_import
Bump to Olm 2.0
2016-12-14 18:59:13 +00:00
Richard van der Hoff 7f4ff352e8 Merge pull request #307 from matrix-org/rav/check_payload_length
Sanity check payload length before encrypting
2016-12-14 14:34:02 +00:00
Richard van der Hoff b11bff5a5b Bump to Olm 2.0
... since the code requires it. Also update the tests.
2016-12-14 14:15:31 +00:00
Richard van der Hoff 223bd459f6 Merge pull request #308 from matrix-org/rav/remove_dead_ohai
Remove dead _sendPingToDevice function
2016-12-14 13:56:12 +00:00
Richard van der Hoff ed1673c66c Remove dead _sendPingToDevice function
This is no longer used; remove it.
2016-12-14 12:38:33 +00:00
Richard van der Hoff 0fda43b603 Sanity check payload length before encrypting
... to give better error messages.
2016-12-14 11:54:35 +00:00
David Baker d3b63c592e Set GIT_COMITTER_EMAIL
so the release tag verifies correctly
2016-12-12 15:37:09 +00:00
Luke Barnard a32af7d77c Merge pull request #306 from matrix-org/luke/feature-AS-room-publication
Add setRoomDirectoryVisibilityAppService
2016-12-12 14:20:43 +00:00
Luke Barnard 742d942baa Add setRoomDirectoryVisibilityAppService
This is for setting the publicity of a room that is bridged to a 3rd party network. This change reflects the second bullet point of https://github.com/matrix-org/synapse/pull/1676#issue-193810881.
2016-12-12 13:52:44 +00:00
David Baker 73e86bfc5d Don't depend on having a build_dir
to get latest changes
2016-12-09 20:17:25 +00:00
David Baker 0a4c41c958 Merge branch 'master' into develop 2016-12-09 19:30:15 +00:00
David Baker 79a699f0be v0.7.1 2016-12-09 19:29:13 +00:00
David Baker 4a2e6a826b Prepare changelog for v0.7.1 2016-12-09 19:29:12 +00:00
David Baker 95f56f95ec Merge pull request #305 from matrix-org/dbkr/signed_releases
Update release script to do signed releases
2016-12-09 19:22:01 +00:00
David Baker 67e75fb7af Invoke changelog_head from outside the pushd
so we know the relative path is right
2016-12-09 19:19:40 +00:00
David Baker ebda89d1ae Add changelog_head.py 2016-12-09 17:34:11 +00:00
David Baker 5ce5299651 Update release script to do signed releases
if a signing ID is set in release_config.yaml

Also set the release text to the relevant changelog entry
2016-12-09 17:04:19 +00:00
David Baker 41bd518182 Merge pull request #304 from matrix-org/rav/await_pending_downloads
e2e: Wait for pending device lists
2016-12-08 18:41:53 +00:00
Richard van der Hoff 739e94302d fix test fail 2016-12-08 18:06:22 +00:00
Richard van der Hoff e54541aecf e2e: Wait for pending device lists
When we send a megolm message, wait for any existing key download to complete.
2016-12-08 16:37:29 +00:00
Richard van der Hoff 338c707579 Merge pull request #303 from matrix-org/rav/new_session_after_blacklist
Start a new megolm session when devices are blacklisted
2016-12-08 16:08:27 +00:00
Richard van der Hoff 301ab01911 Start a new megolm session when devices are blacklisted
If we have shared the session with a device which is subsequently blacklisted,
we need to start a new session for the next message.

Rather than doing this proactively (which would be subject to false-positives
and require slightly awkward tracking of who we had shared the session with),
we check the list of who we have shared the session with on each send, and
start a new session if any of them are blocked.

Fixes https://github.com/vector-im/riot-web/issues/2146.
2016-12-08 13:39:58 +00:00
Richard van der Hoff 99089c0f5f Merge pull request #302 from matrix-org/rav/send_to_our_devices
E2E: Download our own devicelist on startup
2016-12-07 11:43:24 +00:00
Richard van der Hoff ec124847d7 Test for self-keyshare
Make sure that we share keys with our own devices.
2016-12-07 11:14:36 +00:00
Richard van der Hoff 89ced19874 E2E: Download our own devicelist on startup
Make sure we get a list of our own devices when starting a new client.

Fixes https://github.com/vector-im/riot-web/issues/2676.
2016-12-06 17:09:21 +00:00
David Baker f997b4a1d5 v0.7.1-rc.1 2016-12-05 17:43:39 +00:00
David Baker def99728e7 Prepare changelog for v0.7.1-rc.1 2016-12-05 17:43:39 +00:00
David Baker fab234dccb Merge pull request #300 from matrix-org/rav/handle_no_session_store
Avoid NPE when no sessionStore is given
2016-11-30 11:06:17 +00:00
Richard van der Hoff 0c0572948e Avoid NPE when no sessionStore is given
Fix the check on opts.sessionStore to check for undefined as well; fixes
"TypeError: Cannot read property 'getEndToEndAccount' of undefined"
2016-11-30 10:48:40 +00:00
David Baker a4e281265f Merge pull request #299 from matrix-org/rav/improve_decryption_errors
Improve decryption error messages
2016-11-24 14:55:48 +00:00
Richard van der Hoff ce71de0d78 Improve decryption error messages
Attempt to make the decryption errors less obscure
2016-11-24 14:51:35 +00:00
Matthew Hodgson c5afcaeaf7 Merge branch 'release-v0.7.0' 2016-11-19 01:56:11 +02:00
Matthew Hodgson 889bfce65d v0.7.0 2016-11-19 01:52:52 +02:00
Matthew Hodgson 1f33d76e87 Prepare changelog for v0.7.0 2016-11-19 01:52:51 +02:00
Matthew Hodgson d8de23228f actually speak valid git in release.sh 2016-11-19 01:51:19 +02:00
Matthew Hodgson 579218aafc correctly crash out if jq or hub are missing 2016-11-19 01:25:36 +02:00
Richard van der Hoff aefdacc566 Merge pull request #297 from matrix-org/rav/better_new_device_handling
Avoid a packetstorm of device queries on startup
2016-11-17 17:42:51 +00:00
Richard van der Hoff d619495136 Merge pull request #295 from matrix-org/rav/handle_errors_on_key_share
E2E: Check devices to share keys with on each send
2016-11-17 17:40:20 +00:00
Richard van der Hoff 036d1da013 Avoid a packetstorm of device queries on startup
Two main changes here:
 * when we get an m.new_device event for a device we know about, ignore it
 * Batch up the m.new_device events received during initialsync and spam out
   all the queries at once.
2016-11-17 16:23:24 +00:00
Richard van der Hoff 4ba8e7e072 Merge pull request #296 from matrix-org/rav/unknown_keyshare_mitigations
Apply unknown-keyshare mitigations
2016-11-17 14:54:41 +00:00
Richard van der Hoff f6830992ea Apply unknown-keyshare mitigations
Now that the mobile clients have been updated to send the right fields, enforce
their correctness on the recipient side.
2016-11-16 22:39:08 +00:00
Richard van der Hoff 769a0cb76f Check devices to share keys with on each send
Instead of trying to maintain a list of devices we need to share with, just
check all the devices for all the users on each send.

This should fix https://github.com/vector-im/vector-web/issues/2568, and
generally mean we're less likely to get out of sync.
2016-11-16 22:24:11 +00:00
Richard van der Hoff 766e837775 Factor out ensureOlmSessionsForDevices
... and move to olmlib
2016-11-16 21:21:57 +00:00
Richard van der Hoff 749f53a22b Merge pull request #294 from matrix-org/rav/distinguish_users_with_no_devices
distinguish unknown users from deviceless users
2016-11-16 20:43:04 +00:00
Richard van der Hoff 851b33aac2 distinguish unknown users from deviceless users
Fixes https://github.com/vector-im/vector-web/issues/2275
2016-11-16 18:05:41 +00:00
David Baker fc958a3922 lint 2016-11-16 16:33:08 +00:00
David Baker 2c31b72c52 Merge pull request #293 from arxcode/develop
Allows to start client with initialSyncLimit = 0
2016-11-16 14:43:20 +00:00
Richard van der Hoff 8decb02027 Merge pull request #289 from matrix-org/luke/api-change-tlw-public-unpagination
Make timeline-window _unpaginate public and rename to unpaginate
2016-11-16 14:29:15 +00:00
Alexander a0fd87c032 Allows to start client with initialSyncLimit = 0 (see http://bit.ly/2f49Kbs) 2016-11-16 09:26:16 -05:00
Luke Barnard c0d862c9f0 Correct jsdoc for unpaginate 2016-11-16 11:06:56 +00:00
David Baker 8143abc9e7 Merge pull request #286 from fred-wang/fix-sync-stop
Send a STOPPED sync updated after call to stopClient
2016-11-16 10:02:28 +00:00
Richard van der Hoff af95dcaef6 Merge pull request #292 from matrix-org/rav/fix_megolm_keys
Fix bug in verifying megolm event senders
2016-11-16 09:47:44 +00:00
Richard van der Hoff 5b4aedd4be Fix bug in verifying megolm event senders
1a03e534bd introduced a bug which mixed up the keys_proved and the
keys_claimed. Switch them around again so that megolm messages are correctly
tied back to the sending device.
2016-11-16 09:22:31 +00:00
Luke Barnard d8c0b16d7e Make timeline-window _unpaginate public and remove _ 2016-11-15 13:27:42 +00:00
Richard van der Hoff 909b56d48e Merge pull request #288 from matrix-org/rav/decrypt_after_keys_arrive
Handle decryption of events after they arrive
2016-11-15 11:11:10 +00:00
Richard van der Hoff a5d857945a Retry decryption after receiving keys
m.room_keys may arrive after the messages themselves, so allow events to be
decrypted after the event (haha).
2016-11-14 15:13:02 +00:00
Richard van der Hoff 1a03e534bd Refactor decryption
Create the MatrixEvent wrapper before decryption, and then pass that into the
decryptors, which should update it.

Also remove the workaround that sends m.new_device messages when we get an
unknown session; it's just a bandaid which is obscuring more meaningful
problems.
2016-11-14 15:13:02 +00:00
Richard van der Hoff e623b539c4 persist DecryptionAlgorithm instances
It's useful to be able to keep state between events in the DecryptionAlgorithm,
so store them in a map.
2016-11-14 15:13:02 +00:00
Kegsay 2ff6f5f958 Merge pull request #287 from fred-wang/fix-example
Fix examples.
2016-11-14 12:49:06 +00:00
Matthew Hodgson 1532188d95 fix typo 2016-11-13 13:24:51 +00:00
Frédéric Wang 04093692c9 Use native Array.isArray when available. 2016-11-13 13:24:35 +00:00
Matthew Hodgson a96389a3e4 Merge pull request #283 from matrix-org/revert-282-is-array
Revert "Use native Array.isArray when available."
2016-11-13 13:23:54 +00:00
Matthew Hodgson 00e7c84a93 Revert "Use native Array.isArray when available." 2016-11-13 13:23:41 +00:00
Matthew Hodgson e0c924870d Merge pull request #282 from fred-wang/is-array
Use native Array.isArray when available.
2016-11-13 13:20:54 +00:00
Frédéric Wang 1a27ad22a7 Use native Array.isArray when available. 2016-11-13 13:47:31 +01:00
Frédéric Wang 7029083266 Send a STOPPED sync updated after call to stopClient 2016-11-12 21:58:58 +01:00
Frédéric Wang a5f0ec7c7d Fix examples. 2016-11-12 17:39:48 +01:00
Mark Haines e7dcc06855 Merge pull request #278 from matrix-org/markjh/travis
Add a travis.yml
2016-11-11 13:58:52 +00:00
Richard van der Hoff 867ac49b50 Merge pull request #277 from matrix-org/markjh/encrypted_voip
Encrypt all events, including 'm.call.*'
2016-11-11 11:06:54 +00:00
Mark Haines bfffbea4a0 Merge remote-tracking branch 'origin/develop' into markjh/encrypted_voip 2016-11-11 10:20:40 +00:00
Mark Haines f8b1c124df Add a travis.yml 2016-11-11 10:04:48 +00:00
Richard van der Hoff bc9e290c11 Merge pull request #276 from matrix-org/rav/ignore_key_reshares
Ignore reshares of known megolm sessions
2016-11-10 19:56:01 +00:00
Mark Haines 777ef83378 Merge remote-tracking branch 'origin/develop' into markjh/encrypted_voip 2016-11-10 19:44:42 +00:00
Mark Haines 24283dcbd5 Encrypt all events, including 'm.call.*' 2016-11-10 19:42:16 +00:00
Richard van der Hoff 2113c83679 Ignore reshares of known megolm sessions
If we get a second key for a known megolm session, ignore it.

Fixes https://github.com/vector-im/vector-web/issues/2326, one hopes.
2016-11-10 19:28:08 +00:00
Richard van der Hoff 77508f38bb event jsdoc
Add a comment on the event event
2016-11-08 16:53:07 +00:00
Richard van der Hoff 6c3eb19b74 Merge pull request #274 from matrix-org/rav/log_on_unknown_session
Log to the console on unknown session
2016-11-07 22:46:37 +00:00
Richard van der Hoff e173d822e8 Log to the console on unknown session
This might help diagnose Erik/Matthew's comms breakdown.
2016-11-07 18:57:09 +00:00
David Baker e2d3ace476 Merge branch 'master' into develop 2016-11-04 10:01:03 +00:00
David Baker 6f79a3107b v0.6.4 2016-11-04 10:00:09 +00:00
David Baker e6a3b2aa28 Change version back so npm can change it back again 2016-11-04 09:59:49 +00:00
David Baker e5dcfdf115 There is now one change 2016-11-04 09:57:37 +00:00
David Baker 47e12fcc3e Use env var for dist version
Don't pass it as an arg because that really confuses npm scripts
that aren;t expecting an arg (npm it just blindly appends it).
2016-11-04 09:42:08 +00:00
David Baker d51b2884da v0.6.4 2016-11-04 09:20:17 +00:00
David Baker 28e9c10ded Prepare changelog for v0.6.4 2016-11-04 09:20:17 +00:00
David Baker e27bf04ced Merge pull request #273 from matrix-org/paul/wrap-request
Make it easier for SDK users to wrap prevailing the 'request' function
2016-11-03 13:57:14 +00:00
Paul "LeoNerd" Evans 65f1b3c976 Document the return type of getRequest() 2016-11-02 18:04:00 +00:00
Paul "LeoNerd" Evans 4529578cd6 Make a handy shortcut for SDK users to provide request wrapping functions in a neat stack 2016-11-02 18:02:02 +00:00
Paul "LeoNerd" Evans 6769c96942 Add a method for querying the js-sdk's current 'request' function in case people want to wrap it 2016-11-02 17:55:23 +00:00
David Baker dcb987732c Add version arg to the dist script
as per comment
2016-11-02 11:36:53 +00:00
David Baker 4f4eba16d6 v0.6.4-rc.2 2016-11-02 11:01:09 +00:00
David Baker 9da913f5a6 Prepare changelog for v0.6.4-rc.2 2016-11-02 11:01:09 +00:00
David Baker a15aa0f7a4 Merge branch 'release-v0.6.4' 2016-11-02 10:30:05 +00:00
David Baker efa1eee6e2 v0.6.4-rc.1 2016-11-02 10:26:53 +00:00
David Baker 55179f0a1a Prepare changelog for v0.6.4-rc.1 2016-11-02 10:26:53 +00:00
David Baker 01593d1a69 Set the release branch variable
when using the current branch, otherwise we'll try to check out
the wrong thing
2016-11-02 10:25:47 +00:00
Richard van der Hoff 40e22cfa86 Merge pull request #272 from matrix-org/dbkr/release_script_fixes
More fixes to the release script
2016-11-02 10:05:41 +00:00
David Baker 97aeaec8d2 More fixes to the release script
* Don't create a new release branch if the current branch starts
   with 'release'
 * This is definitely a bash script at this point
 * Fix update_changelog test
 * Tabs
2016-11-02 09:56:00 +00:00
David Baker 0b9f85d97b Merge pull request #271 from matrix-org/dbkr/build_process
Update the release process to use github releases
2016-11-01 17:43:06 +00:00
David Baker d266486581 Check we have the various scripts
Rather than trying to use them and failing at annoying points
2016-11-01 16:48:16 +00:00
David Baker c47d2fc750 too many ses(es) 2016-11-01 15:38:20 +00:00
David Baker 66c4c8882f Right repo 2016-11-01 15:37:39 +00:00
David Baker 72d7dd7690 /dist not dist 2016-11-01 15:36:37 +00:00
David Baker fff354669c Update README to point to where releases now live 2016-11-01 14:53:26 +00:00
David Baker 07ae4b0be6 Add 'dist' script to js-sdk
(which just runs npm build, but the presence of the dist script
will inform the release process to run it and upload release
assets)
2016-11-01 14:36:56 +00:00
David Baker cc51805c39 Use github release in release.sh
Adds the 'dist' target for building assets for distribution,
which the release script will run, uploading resulting files
as release assets.
2016-11-01 14:34:48 +00:00
David Baker c61ac2a845 Delete aaaaaaall of dist/
and gitignore dist since we now just use it for build output.
2016-10-31 18:31:24 +00:00
David Baker 6d67de06a2 Build bundled files straight to dist
Rather than to version specific directories beneath dist, since
we're now not keeping every built version in the source tree.

Also:

 * build the minified version at the same time,
 * Include rimraf so we can rm -r dist (npm has re-ordered the
   deps)
 * Exclude olm from the bundled file
2016-10-31 18:25:34 +00:00
Richard van der Hoff ec1273893f Merge pull request #270 from matrix-org/dbkr/no_pack_world
Don't package the world when we release
2016-10-31 18:09:29 +00:00
David Baker 1e26077d58 Include browser-index.js 2016-10-31 18:06:37 +00:00
David Baker ad67f002e3 Remove comment about syncing with .npmignore
Given there is no longer a .npmignore
2016-10-31 18:04:26 +00:00
David Baker 572df32dca Don't package the world when we release
Include files explicitly rather than excluding them with .npmignore

As https://github.com/vector-im/vector-web/pull/2516
2016-10-31 17:29:34 +00:00
David Baker 6b8181c06f Merge pull request #269 from matrix-org/luke/feature-initial-sync-filter
Add ability to set a filter prior to the first /sync
2016-10-26 11:37:55 +01:00
lukebarnard 5900542cfb Add ability to set a filter prior to initial sync.
Useful for only syncing with a subset of joined rooms or only retrieving certain relevant types of events.
2016-10-25 20:05:25 +01:00
Richard van der Hoff a28b825c4d Merge pull request #236 from pik/get-room-tags
Add getRoomTags method to client
2016-10-21 14:26:52 +01:00
Richard van der Hoff d105854619 Merge pull request #243 from matrix-org/rav/sign_one_time_keys
Sign one-time keys, and verify their signatures
2016-10-21 14:24:07 +01:00
Richard van der Hoff a4f192bc88 Sign one-time keys, and verify their signatures
We have decided that signing one-time keys is the lesser of two evils;
accordingly, use a new key algorithm type (`signed_curve25519`), sign the
one-time keys that we upload to the server, and verify the signatures on those
we download.

This will mean that develop won't be able to talk to master, but hey, we're in
beta.
2016-10-21 12:24:19 +01:00
Richard van der Hoff db925d7fde Merge branch 'master' into develop 2016-10-21 12:07:16 +01:00
Mark Haines 16b4865035 Merge pull request #241 from matrix-org/markjh/check_for_duplicate_message_ids
Check for duplicate message indexes for group messages
2016-10-21 09:55:38 +01:00
Mark Haines 20b310484b Document the format of the keys 2016-10-21 09:54:57 +01:00
Richard van der Hoff 611a191b0e Merge pull request #240 from matrix-org/rav/rotate_megolm_sessions
Rotate megolm sessions
2016-10-20 21:06:28 +01:00
Mark Haines 8b856b9d15 Wrap the longer lines 2016-10-20 18:02:48 +01:00
Mark Haines 3f7df0d15c Fiddle linebreaks 2016-10-20 17:59:15 +01:00
Mark Haines e0917d3c47 Check for duplicate message indexes for group messages 2016-10-20 17:49:37 +01:00
Richard van der Hoff 19c257703c Rotate megolm sessions
In order to mitigate backward-secrecy concerns, make sure that we rotate the
outbound megolm session at regular intervals (every week/100 msgs by default).
2016-10-20 15:42:06 +01:00
pik 62b6262534 Add getRoomTags method
Signed-off-by: pik <alexander.maznev@gmail.com>
2016-10-19 10:10:33 -05:00
Richard van der Hoff 55bd3ac302 Add CONTRIBUTING.rst 2016-10-19 11:55:09 +01:00
Richard van der Hoff 7a7f345f28 Merge pull request #239 from matrix-org/rav/fix_unknown_key
Check recipient and sender in Olm messages
2016-10-19 11:44:48 +01:00
Richard van der Hoff ff2282a41a Merge pull request #237 from matrix-org/rav/check_userid_in_device_list
Consistency checks for E2E device downloads
2016-10-19 11:30:07 +01:00
Richard van der Hoff b5c7c700d5 Check recipient and sender in Olm messages
Embed the sender, recipient, and recipient keys in the plaintext of Olm
messages, and check those fields on receipt.

Fixes https://github.com/vector-im/vector-web/issues/2483
2016-10-19 11:24:59 +01:00
Richard van der Hoff de6330fb80 Fix up failing test
Update a failing test to include user_id and device_id in the right place.

Remove one of the cases since it's somewhat redundant to
matrix-client-crypto-spec anyway.
2016-10-18 21:09:10 +01:00
Richard van der Hoff aafb1ffdef Consistency checks for E2E device downloads
Check that the user_id and device_id in device query responses match those that
we expect.

This resolves an unknown-key attack whereby Eve can re-sign Bob's keys with her
own key, thus getting Alice to send her messages which she can then forward to
Bob, making Bob think that Alice sent the messages to him.
2016-10-18 13:40:13 +01:00
David Baker c5d738d25c Merge pull request #235 from matrix-org/rav/delete_device_ui_auth
Support User-Interactive auth for delete device
2016-10-12 18:13:49 +01:00
David Baker 15d8252909 Merge pull request #234 from matrix-org/rav/interactive_auth
Utility to help with interactive auth
2016-10-12 18:13:36 +01:00
Richard van der Hoff 8189c58fc3 Use utils.extend instead of Object.assign
... because javascript is awful
2016-10-12 15:21:47 +01:00
David Baker b3e7f4ea21 gjslint wants a space before the '='... 2016-10-12 11:42:10 +01:00
David Baker 9c9ae562ec Merge branch 'master' into develop 2016-10-12 11:38:08 +01:00
David Baker f93eea095e Fail the build if the docs don't generate 2016-10-12 11:32:42 +01:00
David Baker 09255a52f7 Merge branch 'release-v0.6.3' 2016-10-12 11:27:05 +01:00
David Baker 6f9c8c3007 Apparently that jsdoc syntax is not valid 2016-10-12 11:24:14 +01:00
David Baker 2e99d5da64 0.6.3 2016-10-12 11:11:09 +01:00
David Baker 72c8586fad Prepare changelog for v0.6.3 2016-10-12 11:07:51 +01:00
Richard van der Hoff d98867b810 User-Interactive auth for delete device
Allow app to pass in an auth dict on delete device
2016-10-12 08:37:16 +01:00
Richard van der Hoff de7061184b Utility to help with interactive auth 2016-10-12 08:27:53 +01:00
David Baker 4e2483b41a Merge pull request #233 from matrix-org/dbkr/register_dont_replace_params
Fix params getting replaced on register calls
2016-10-11 14:57:56 +01:00
David Baker d3db4ee63d lint bunny 2016-10-11 14:56:21 +01:00
David Baker 5d049cc5e8 Fix params getting replaced on register calls
The react-sdk sets guest access token to null sometimes, but we
previously added anything that was not 'undefined' to the params,
causing us to send parameters which overwrite the previous actual
parameters with the useless, {guest_access_token: null} which
caused registrations from an email link to break.

We should have no reason to send null, at least for these
particular params, so don't.
2016-10-11 14:30:06 +01:00
Richard van der Hoff 6218bad00f Merge pull request #232 from matrix-org/dbkr/retry_immediately_from_reconnecting
Fix potential 30s delay on reconnect
2016-10-10 18:07:29 +01:00
David Baker 2968e9c0c7 Fix potential 30s delay on reconnect
After a connection glitch we would normally sync with zero timeout
so the connection comes back faster, but we didn't if the first
keepalive succeeds since we never marked the connection as failed.
This makes the behaviour more consistent.

Also get rid of the connectionLost flag which was only used in
one place anyway.
2016-10-10 17:08:28 +01:00
David Baker e73051b230 Merge pull request #230 from matrix-org/rav/uploadContent_platform_consistency
uploadContent: Attempt some consistency between browser and node
2016-10-10 10:21:56 +01:00
David Baker acad3e69dd Merge pull request #229 from matrix-org/rav/fix_upload_error_parsing
Fix error handling on uploadContent
2016-10-10 10:15:44 +01:00
Richard van der Hoff 4794dfc17b uploadContent: Attempt some consistency between browser and node
Previously, the API for uploadContent differed wildly depending on whether you
were on a browser with XMLHttpRequest or node.js with the HTTP system
library. This lead to great confusion, as well as making it hard to test the
browser behaviour.

The browser version expected a File, which could be sent straight to
XMLHttpRequest, whereas the node.js version expected an object with a `stream`
property. Now, we no longer recommend the `stream` property (though maintain it
for backwards compatibility) and instead expect the first argument to be the
thing to upload. To support the different ways of passing `type` and `name`,
they can now either be properties of the first argument (which will probably
suit browsers), or passed in as explicit `opts` (which will suit the node.js
users).

Even more crazily, the browser version returned the value of the `content_uri`
property of the result, while the node.js returned the raw JSON. Both flew in
the face of the convention of the js-sdk, which is to return the entire parsed
result object. Hence, add `rawResponse` and `onlyContentUri` options, which
grandfather in those behaviours.
2016-10-10 00:22:22 +01:00
Richard van der Hoff d505ab9eeb Fix error handling on uploadContent
Make sure we parse the json content of errors from uploadContent before trying
to turn them into MatrixErrors.
2016-10-10 00:22:04 +01:00
Richard van der Hoff 631eeb9bc0 Merge pull request #226 from matrix-org/rav/fix_upload
Fix uploadContent for node.js
2016-10-10 00:21:43 +01:00
Richard van der Hoff 892ca56808 Merge pull request #228 from pik/bug-invalid-filter
Fix sync breaking when an invalid filterId is in localStorage
2016-10-09 20:35:09 +01:00
pik 828c7ba451 Fix sync breaking when an invalid filterId is in localStorage
* if getFilter fails for a filterId, null out the localStorage id and
   redirect to the createFilter path
 * add spec
 * fix unit/matrix-client.spec.js http response not matching synapse
2016-10-09 14:17:18 -05:00
Richard van der Hoff a3d86c03b1 Fix uploadContent for node.js
9e89e71e broke uploadContent, making it set 'json=true' on the request, so that
we would try to turn raw content into JSON. It also misguidedly set a
client-side timeout of 30s.

Fix that, and add some tests to check uploadContent works.

In mock-request: distinguish between an expectation (ExpectedRequest)
and an actual request (Request). Add support for checking the headers, and the
request options in general, to Request.
2016-10-08 17:48:10 +01:00
Richard van der Hoff 74d6cb802f Merge pull request #223 from matrix-org/dbkr/sync_dont_error_until_keepalive_fail
Don't emit ERROR until a keepalive poke fails
2016-10-07 16:05:14 +01:00
David Baker 1b83f66536 Merge pull request #224 from matrix-org/rav/auth_fallback_url
Function to get the fallback url for interactive auth
2016-10-07 15:41:12 +01:00
Richard van der Hoff e5d5cd901a Function to get the fallback url for interactive auth 2016-10-07 14:08:28 +01:00
David Baker 92ae4dda72 Add short delay before keepalives + 'RECONNECTING'
Changed my mind - it's a good idea to wait a short time before
sending a keepalive request: this will make sure we never
tightloop.

This also adds a 'RECONNECTING' state for when a sync request has
failed but there is no reason to suspect there is actually a
connectivity problem. This is necessary for the tests to be able
to advance the clock at the appropriate time, but could be nice
for clients. Add a breaking change changelog entry since
technically this is an API change that might break clients if they
were relying on 'SYNCING' always coming before 'ERROR' for some
reason.
2016-10-07 11:29:52 +01:00
David Baker cd5a88c718 Fix tests
* Go back to previous behaviour of continuing to emit ERROR states if it continues to fail
 * Don't set a timer if the timeout is zero
 * Change test to assert the continued-error behaviour, not exactly multiple syncs failing
 * Update other tests to fail the keepalive requests where appropriate
2016-10-06 20:54:57 +01:00
David Baker 1c744a66e6 Don't emit ERROR until a keepalive poke fails
This accomplishes the same as
https://github.com/matrix-org/matrix-js-sdk/pull/216/files, but
without the client waiting 110 seconds for a sync request to time
out. That is, don't display an error message a soon as a sync
request fails, since we should accept that sometimes long lived
HTTP connections will go away and that's fine.

Also:
 * Use request rather than deprecated requestWithPrefix
 * http-api: The empty string may be falsy, but it's a valid prefix
2016-10-06 18:29:05 +01:00
David Baker 57cf7e1f7d Merge pull request #222 from matrix-org/revert-216-erikj/sync_fail_first
Revert "Handle the first /sync failure differently."
2016-10-06 16:28:45 +01:00
David Baker 86ea00cfee Revert "Handle the first /sync failure differently." 2016-10-06 16:27:38 +01:00
David Baker 02f8e7da3d 0.6.2 2016-10-05 16:43:11 +01:00
David Baker a245b735b3 Prepare changelog for v0.6.2 2016-10-05 16:39:48 +01:00
Richard van der Hoff 0f71983cb9 Merge pull request #221 from matrix-org/dbkr/check_dependencies
Check dependencies aren't on develop in release.sh
2016-10-05 16:38:41 +01:00
David Baker 7db6b9e490 Check dependencies aren't on develop in release.sh 2016-10-05 16:34:36 +01:00
David Baker 0021b21170 Merge pull request #220 from matrix-org/rav/fix_turnserver_leak
Fix checkTurnServers leak on logout
2016-10-03 11:24:25 +01:00
David Baker 3080dc018a Merge pull request #219 from matrix-org/rav/refactor_httpapi
Fix leak of file upload objects
2016-10-03 11:18:02 +01:00
David Baker f5d0ec32e5 Merge pull request #218 from matrix-org/rav/deduplicate_storage_update
crypto: remove duplicate code
2016-10-03 11:01:33 +01:00
Richard van der Hoff 6b3a06a8ed Fix checkTurnServers leak on logout
Remember to cancel the checkTurnServers callback when we stop the client.
2016-10-02 21:17:31 +01:00
Richard van der Hoff 9e89e71e0e Fix leak of file upload objects
After an upload completed, we were failing to delete the details of the upload
from the list (because the comparison was bogus), meaning we leaked an object
each time.

While we're in the area:

  - make the request methods take an opts object (instead of a localTimeout
    param), and deprecate the WithPrefix versions.

  - make the non-xhr path for uploadContent use authedRequest instead of
    rolling its own.

  - make cancelUpload use the promise.abort() hack for simplicity
2016-09-30 15:29:45 +01:00
David Baker dcedc78fc2 Merge pull request #217 from matrix-org/dbkr/join_3p_location
Add API for 3rd party location lookup
2016-09-30 14:32:33 +01:00
Richard van der Hoff faff057592 crypto: remove duplicate code
Only call SessionStore.storeEndToEndDevicesForUser once per user, rather than
once per device.

(Probably also fixes a bug where, when a user removes all devices, the store
isn't updated)
2016-09-30 09:17:54 +01:00
David Baker 56be271b0a I actually docced them as well 2016-09-29 17:47:51 +01:00
David Baker fa557eb0cc Add API for 3rd party location lookup 2016-09-29 15:50:00 +01:00
Erik Johnston b784d1a5e7 Merge pull request #216 from matrix-org/erikj/sync_fail_first
Handle the first /sync failure differently.
2016-09-23 11:01:10 +01:00
Erik Johnston f6614ac0e4 Fix tests 2016-09-23 10:08:40 +01:00
Erik Johnston e4aea701ab Comment 2016-09-23 09:57:06 +01:00
Erik Johnston 352f79e9fd Handle the first /sync failure differently.
A /sync request may spuriously fail on occasion, without the
"connection" actually being lost. To avoid spurious "Connection Lost"
warning messages we ignore the first /sync and immediately retry, and
only if that fails do we enter an ERROR state.
2016-09-22 16:24:40 +01:00
David Baker 3a17ef983e 0.6.1 2016-09-21 17:23:15 +01:00
David Baker 91e571fb68 Prepare changelog for v0.6.1 2016-09-21 17:20:10 +01:00
Richard van der Hoff 1a3ee28d01 Log when we get an oh_hai message 2016-09-21 17:07:40 +01:00
Richard van der Hoff 669aecf4e6 E2E: Fix NPE in getEventSenderDeviceInfo 2016-09-21 15:05:27 +01:00
David Baker ea23db6450 Merge branch 'master' into develop 2016-09-21 11:38:59 +01:00
David Baker 69e4bdd421 0.6.0 2016-09-21 11:32:26 +01:00
David Baker 86a4fd687c Prepare changelog for v0.6.0 2016-09-21 11:29:28 +01:00
David Baker 14bc4af90c Merge pull request #215 from matrix-org/rav/key_proofs
Fix the ed25519 key checking
2016-09-21 10:44:46 +01:00
Richard van der Hoff 0cd2b2c0e2 Merge pull request #214 from matrix-org/rav/event_sender_device_info
Add MatrixClient.getEventSenderDeviceInfo()
2016-09-21 10:21:57 +01:00
Richard van der Hoff 832559926f Fix the ed25519 key checking
Finish plumbing in the Ed25519 key checks. Make sure we store the claimed key
correctly in the megolm sessions, and keep them as a separate field in
MatrixEvent rather than stuffing them into _clearEvent
2016-09-20 20:42:08 +01:00
Richard van der Hoff 59411353b1 Add 'keys' to *all* olm messages
(including ones which just carry megolm keys)
2016-09-20 20:39:40 +01:00
Richard van der Hoff 83bd420cd5 Return null from decryptEvent if session is unknown
This just makes the shape of the API a bit saner.
2016-09-20 20:39:40 +01:00
Richard van der Hoff 78a0aa5d47 Add MatrixClient.getEventSenderDeviceInfo()
- a function to get information about the device which sent an event
2016-09-20 20:39:16 +01:00
Richard van der Hoff 6e31319294 Handle lack of one-time keys better
If a device had run out of one-time keys, we would send it an empty to_device
event, which it would then fail to decrypt with a "Not included in
recipients", which is all a bit pointless.
2016-09-18 22:58:35 +01:00
Richard van der Hoff cd0b19f93f Crypto: improve console logs
Attempt to make the console logs more helpful by reducing noise and adding
helpful debug info.
2016-09-18 21:55:38 +01:00
Richard van der Hoff 4f22610499 Megolm: clarify jsdoc
Clarify somewhat misleading jsdoc text
2016-09-18 13:52:48 +01:00
Matthew Hodgson 9e57a9352a Merge pull request #212 from matrix-org/rav/get_devicelist_on_join
Pull user device list on join
2016-09-17 19:21:17 +01:00
Richard van der Hoff 4e0d7b56d8 Merge pull request #213 from matrix-org/rav/fix_oh_hai_ping
Fix sending of oh_hais on bad sessions
2016-09-17 19:08:09 +01:00
Richard van der Hoff f2e10e030d Unknown sessions: send oh_hai to all devices if device_id is unknown 2016-09-17 19:07:03 +01:00
Richard van der Hoff 266b7afc72 Fix sending of oh_hais on bad sessions
Fix a bunch of bugs in the code which tried to send an oh_hai message when we
got a message with an unknown megolm session.
2016-09-17 18:30:12 +01:00
Richard van der Hoff a15dffbb3a Pull user device list on join
When a new user joins a room, make sure we download their device list if we
don't already have it.

This should fix at least one cause of
https://github.com/vector-im/vector-web/issues/2249.
2016-09-17 17:44:15 +01:00
Matthew Hodgson a30c816cb6 typo 2016-09-17 15:44:55 +01:00
Matthew Hodgson e65fe483e1 Merge pull request #211 from matrix-org/dbkr/public_rooms_paginate
Support /publicRooms pagination
2016-09-17 01:24:40 +01:00
David Baker 0d51fad805 Make js-sdk compatible with older synapses
Use GET API if no params given. Revert changelog entry since it now doesn't break older synapses.
2016-09-16 23:23:25 +01:00
David Baker 17ed38ad05 Merge remote-tracking branch 'origin/develop' into dbkr/public_rooms_paginate 2016-09-16 20:14:29 +01:00
David Baker 8259f08882 Add changelog entry
to note we've broken publicRooms on older synapses
2016-09-16 20:11:00 +01:00
David Baker 55d6cf7ab0 Update /publicRooms to use the new pagination API 2016-09-16 20:08:21 +01:00
Matthew Hodgson 425f862cf8 Merge pull request #205 from matrix-org/markjh/megolm
Update the olm library version to 1.3.0
2016-09-16 17:30:26 +01:00
Mark Haines 5d6256bede Merge pull request #209 from matrix-org/markjh/comment_upload_key
Comment what the logic in uploadKeys does
2016-09-16 16:34:37 +01:00
Mark Haines ff5b923e6f Spelling: s/cliamed/claimed/ 2016-09-16 16:31:00 +01:00
Mark Haines af7a9a68b8 Merge pull request #210 from matrix-org/markjh/echo_keys_proved
Include keysProved and keysClaimed in the local echo for events we send.
2016-09-16 15:45:22 +01:00
Mark Haines 905059d6da More comments explaining the keysClaimed/keysProved properties 2016-09-16 15:42:02 +01:00
Mark Haines 3bc56cf3f8 More comments on the local echo 2016-09-16 15:36:56 +01:00
Mark Haines 1feb7fc0ba Fix copy+paste 2016-09-16 15:32:46 +01:00
Mark Haines c2a40572a5 Include keysProved and keysClaimed in the local echo for events we send. 2016-09-16 15:30:22 +01:00
Mark Haines ee7d4d0521 Explain what happens to the old keys in olm 2016-09-16 14:43:22 +01:00
Mark Haines 6ab410ef6a Comment what the logic in uploadKeys does 2016-09-16 14:38:26 +01:00
Mark Haines 8235d966d6 Merge pull request #208 from matrix-org/markjh/upload_keys
Check if we need to upload new one-time keys every 10 minutes
2016-09-16 14:21:03 +01:00
Mark Haines c7b83f6ee6 More semicolons 2016-09-16 11:40:06 +01:00
Mark Haines 460f20a4ce Merge pull request #207 from matrix-org/markjh/variable_scoping
Reset oneTimeKey to null on each loop iteration.
2016-09-16 11:23:58 +01:00
Mark Haines da408f975e Check if we need to upload new one-time keys every 10 minutes 2016-09-16 11:22:36 +01:00
Mark Haines 9a98c3991a Reset onTimeKey to null on each loop iteration.
Otherwise we will use a value from a previous iteration of the loop.
2016-09-16 10:44:25 +01:00
Matthew Hodgson 6e0b2de99f fix lint 2016-09-16 03:19:20 +01:00
Matthew Hodgson 0633d7d3f6 track raw displayname on user objects 2016-09-16 03:18:47 +01:00
Matthew Hodgson 2765720b76 unbreak NPE where megolm's decryptEvent doesn't return a result 2016-09-15 20:09:41 +01:00
Mark Haines 71f23ffce1 Merge branch 'develop' into markjh/megolm
Conflicts:
	lib/crypto/algorithms/megolm.js
2016-09-15 17:10:02 +01:00
Mark Haines 1863af147d Merge pull request #206 from matrix-org/markjh/ed25519
Add getKeysProved and getKeysClaimed methods to MatrixEvent.
2016-09-15 17:07:52 +01:00
Mark Haines 0d5d74674e Remove spurious senderKey argument 2016-09-15 16:46:28 +01:00
Mark Haines 45ed0884df Document return type 2016-09-15 16:42:40 +01:00
Mark Haines 45e9f59fdc Poke jenkins 2016-09-15 16:40:02 +01:00
Mark Haines bde6a171f6 Add getKeysProved and getKeysClaimed methods to MatrixEvent.
These list the keys that sender of the event must have ownership
of and the keys of that the sender claims ownership of.

All olm and megolm messages prove ownership of a curve25519 key.
All new olm and megolm message will now claim ownership of a
ed25519 key.

This allows us to detect if an attacker claims ownership of a curve25519
key they don't own when advertising their device keys, because when we
receive an event from the original user it will have a different ed25519 key
to the attackers.
2016-09-15 16:26:43 +01:00
Mark Haines 49a74755a8 Merge pull request #204 from matrix-org/markjh/oh_hai_reliability
Send a 'm.new_device' when we get a message for an unknown group session
2016-09-15 14:44:06 +01:00
Mark Haines 2fbef8638f Fix grammar 2016-09-15 14:43:23 +01:00
Mark Haines eb4166afe3 Whitespace 2016-09-15 14:36:53 +01:00
Mark Haines b3beaacec7 Remove unnecessary dep 2016-09-15 14:26:05 +01:00
Mark Haines 355b728a57 Remove unnecessary semicolon; 2016-09-15 14:23:30 +01:00
Mark Haines 577b0e8f1b Add a test to check the olm version 2016-09-15 14:08:25 +01:00
Mark Haines 35d99564c1 Rate limit the oh hai pings 2016-09-15 14:07:40 +01:00
Mark Haines 6f9bb38232 Include our device key in megolm messages 2016-09-15 11:56:56 +01:00
Mark Haines d02c205910 Rename the "content" variable to avoid shadowing 2016-09-15 11:46:49 +01:00
Mark Haines 38681202dc Add olm version to client. Add semicolons. 2016-09-14 20:03:31 +01:00
Mark Haines 0d20a0acf0 Add a test to check that we have the right version of Olm 2016-09-14 19:59:32 +01:00
Mark Haines 9277a86403 Add the accidentally deleted sessionId documentation back 2016-09-14 19:35:31 +01:00
Mark Haines 5ec8688cf6 Semicolon 2016-09-14 19:26:44 +01:00
Mark Haines 6ae82a9cb4 Fix syntax error 2016-09-14 19:20:46 +01:00
Mark Haines 72a4b92022 Send a 'm.new_device' when we get a message for an unknown group session
This should reduce the risk of a device getting permenantly stuck unable
to receive encrypted group messages.
2016-09-14 19:16:24 +01:00
Mark Haines 0cc68bc125 Update the olm library version to 1.3.0 2016-09-14 14:24:21 +01:00
Matthew Hodgson 6ca917f4db Merge pull request #196 from matrix-org/matthew/filtered-timelines
Introduce EventTimelineSet, filtered timelines and global notif timeline.
2016-09-12 15:56:55 +01:00
Matthew Hodgson 8a848deddc unbreak mocks in tests 2016-09-12 15:52:10 +01:00
David Baker 2ebd4b15a4 Merge pull request #203 from matrix-org/markjh/try_catch
Wrap the crypto event handlers in try/catch blocks
2016-09-12 14:32:46 +01:00
Mark Haines f0274f3f26 Wrap the crypto event handlers in try/catch blocks 2016-09-12 11:44:31 +01:00
Matthew Hodgson 85b2e5d758 fix refactoring bug; emit timelineReset after updating _liveTimeline 2016-09-11 03:23:43 +01:00
Matthew Hodgson eef03882ad don't forget to emit timelineResets for normal room resets 2016-09-11 03:23:15 +01:00
Matthew Hodgson f7e5d962c0 Merge branch 'develop' into matthew/filtered-timelines 2016-09-11 02:38:50 +01:00
Matthew Hodgson 87c6a40b3f reemit timelineReset correctly from Sync 2016-09-11 02:15:29 +01:00
Matthew Hodgson e614e17a71 correctly notify when timelineSets get reset 2016-09-10 10:44:48 +01:00
Matthew Hodgson b4dc5e620b oops, unbreak notif pagination 2016-09-10 01:36:12 +01:00
Matthew Hodgson 0713e65fc5 fix lint 2016-09-10 00:58:16 +01:00
Matthew Hodgson b69f6cf70a don't double-add events in Room.addEventsToTimeline
also, ignore notif events from initialSync as their time ordering is wrong
2016-09-10 00:56:37 +01:00
Matthew Hodgson 2c6409a67a special case 'end' token 2016-09-09 18:45:15 +01:00
Matthew Hodgson ad7db78829 only consider rooms when paginating EventTimelines with rooms 2016-09-09 18:05:43 +01:00
Matthew Hodgson bd9e3e5794 only call setEventMetadata on unfiltered timelineSets 2016-09-09 17:42:24 +01:00
Matthew Hodgson bd32ed5598 refactr paginateNotifTimeline out of existence 2016-09-09 16:49:39 +01:00
Matthew Hodgson 5a5257a598 fix comment 2016-09-09 16:41:29 +01:00
Matthew Hodgson 75b6ebf287 revert comment position 2016-09-09 16:35:38 +01:00
Matthew Hodgson a9d3ae4ef8 fix tests 2016-09-09 16:34:02 +01:00
Matthew Hodgson d480b6cf3e remove unnecessary getUnfilteredTimelineSet() 2016-09-09 16:06:10 +01:00
Richard van der Hoff fdb640e361 Merge pull request #202 from matrix-org/rav/decryption_warnings
Show warnings on to-device decryption fail
2016-09-09 14:09:05 +01:00
Richard van der Hoff 924a8533f1 Merge pull request #201 from matrix-org/rav/DisplayName
s/Displayname/DisplayName/
2016-09-09 14:08:36 +01:00
Richard van der Hoff 72b4f270ff Show warnings on to-device decryption fail
If we can't decrypt a to-device message, show a warning about it, rather than
swallowing the error.
2016-09-09 12:37:02 +01:00
Richard van der Hoff 946539e32d s/Displayname/DisplayName/ 2016-09-09 11:32:57 +01:00
Matthew Hodgson 9882fed6d7 Merge branch 'develop' into matthew/filtered-timelines 2016-09-09 11:12:42 +01:00
Matthew Hodgson 93f45c0a94 reemit notif timeline events correctly 2016-09-09 02:28:01 +01:00
Matthew Hodgson c6d358a6f3 doc Room.timeline event correctly 2016-09-09 02:27:51 +01:00
Matthew Hodgson 2e4c362ccd make /notification pagination actually work 2016-09-09 02:08:39 +01:00
Matthew Hodgson f959e1a134 incorporate PR feedback 2016-09-08 22:38:39 +01:00
Matthew Hodgson 7dfc4a404c initial PR fixes 2016-09-08 17:51:14 +01:00
Richard van der Hoff 2af349eb72 Merge pull request #200 from matrix-org/rav/oh_hai_new_device
OH HAI
2016-09-08 16:20:59 +01:00
Richard van der Hoff 43f3a1e8b3 Merge pull request #199 from matrix-org/rav/share_megolm_state
Share the current ratchet with new members
2016-09-08 16:18:48 +01:00
Matthew Hodgson 13c186dfbe fix lint 2016-09-08 15:29:53 +01:00
Matthew Hodgson 4d88736d13 add much-needed room.getUnfilteredTimelineSet() helper 2016-09-08 14:37:26 +01:00
Richard van der Hoff 1da633e28a Handle new device announcements
When we see a new device, download its keys, and then add it to the list of
things waiting for a keyshare.
2016-09-08 14:35:13 +01:00
Richard van der Hoff 879da47f0e Send an "oh hai" message to other e2e users
When we first complete an initial sync on a new device, send out an
m.new_device message for each user we share an e2e room with
2016-09-08 14:34:08 +01:00
Richard van der Hoff cacafb461d Share the current ratchet with new members
When a new member joins the room, we don't need to reset the megolm session;
instead we can just share the current state with the new user.
2016-09-08 14:20:54 +01:00
Richard van der Hoff 15e285c6b4 Merge pull request #198 from matrix-org/rav/refactor_crypto
Move crypto bits into a subdirectory
2016-09-08 13:35:38 +01:00
Richard van der Hoff 71c33420f6 Move crypto bits into a subdirectory
It was getting a bit sprawly; this should help keep things together.
2016-09-08 09:50:31 +01:00
Richard van der Hoff e7f70bba5c Merge pull request #197 from matrix-org/rav/refactor_crypto_event_handler
Refactor event handling in Crypto
2016-09-08 09:44:31 +01:00
Matthew Hodgson e4ec2aa55f maintain the global notification timeline set.
* track notifTimelineSet on MatrixClient
* stop Rooms from tracking notifTimelineSet as they don't need to
* Implement client.paginateNotifTimelineSet
* make Events store their pushActions properly
* insert live notifs directly into the notifTimelineSet in /sync, ordering by origin_server_ts.
2016-09-08 02:57:49 +01:00
Matthew Hodgson fc495a5f1e fix lint 2016-09-08 00:18:17 +01:00
Richard van der Hoff 6fe4dfcad0 Refactor event handling in Crypto
Move the event-handler registration from client.js into crypto.js
2016-09-07 23:13:22 +01:00
Matthew Hodgson dac820f957 actually filter /messages 2016-09-07 22:04:12 +01:00
Matthew Hodgson 91f8df8d19 make EventTimeline take an EventTimelineSet 2016-09-07 21:17:06 +01:00
Matthew Hodgson 9b507f6c6c Merge branch 'develop' into matthew/filtered-timelines 2016-09-07 20:34:57 +01:00
Matthew Hodgson 5e583d3c50 populate up filtered timelineSets vaguely correctly 2016-09-07 19:45:30 +01:00
Richard van der Hoff d706b57fe9 Merge pull request #195 from matrix-org/rav/lazy_olm
Don't create Olm sessions proactively
2016-09-07 19:19:07 +01:00
Richard van der Hoff 1063a16013 Don't create Olm sessions proactively
In what I hoped would be a five-minute refactor to help clean up an annoying
not-really-used codepath, but turned into a bit of a hackathon on the tests,
create Olm sessions lazily in Olm rooms, just as we do in megolm rooms, which
allows us to avoid having to get the member list before configuring e2e in a
room.
2016-09-07 18:44:02 +01:00
Richard van der Hoff 46a2073427 Merge pull request #194 from matrix-org/rav/use_todevice_events
Use to-device events for key sharing
2016-09-07 14:01:57 +01:00
Richard van der Hoff d7bb9574e7 Merge pull request #193 from matrix-org/rav/update_readme
README: callbacks deprecated
2016-09-07 14:00:24 +01:00
Richard van der Hoff 9c18893ae5 Use to-device events for key sharing
Synapse now supports out-of-band messages, so use them instead of sending the
key-sharing messages in-band.
2016-09-07 13:56:54 +01:00
Richard van der Hoff 4503c320e5 README: callbacks deprecated
We are no longer adding callback arguments as a matter of course.
2016-09-07 11:39:29 +01:00
Matthew Hodgson c4995bd153 fix filtering 2016-09-07 02:17:03 +01:00
Richard van der Hoff af0f5b37d8 Bump to olm 1.2.0 2016-09-06 22:29:33 +01:00
Richard van der Hoff b9ba4671b4 Merge pull request #192 from matrix-org/rav/verified_megolm_senders
Fix sender verification for megolm messages
2016-09-06 22:22:20 +01:00
Richard van der Hoff 50b8f13037 Fix sender verification for megolm messages
Turns out all we need to do is to make sure we use the Olm device table when we
look up megolm senders.
2016-09-06 16:46:57 +01:00
Richard van der Hoff 4aa9dca608 Merge pull request #191 from matrix-org/rav/ciphertext_field
Use `ciphertext` instead of `body` in megolm events
2016-09-06 16:25:35 +01:00
Richard van der Hoff 98dc5328a0 Use ciphertext instead of body in megolm events
Apparently `body` is going to be special.
2016-09-06 16:23:23 +01:00
Richard van der Hoff 408671b58a Megolm: Remove check for signature field
We're putting the signature in the body, so we don't want a separate field.
2016-09-06 15:59:57 +01:00
Matthew Hodgson 1bda527e3d export EventTimelineSet 2016-09-06 01:04:23 +01:00
Richard van der Hoff e0f1b9ebf0 Merge pull request #189 from matrix-org/rav/get_olm_sessions_for_user
Add debug methods to get the state of OlmSessions
2016-09-05 10:38:57 +01:00
Richard van der Hoff 55127aa43f Merge pull request #190 from matrix-org/rav/get_stored_devices
MatrixClient.getStoredDevicesForUser
2016-09-05 10:38:12 +01:00
Matthew Hodgson 888fbe3549 fix some lint 2016-09-05 02:44:46 +01:00
Matthew Hodgson ed5c061566 move getOrCreateClient from sync.js to client.js 2016-09-05 02:44:24 +01:00
Richard van der Hoff df6b1d1471 Add debug methods to get the state of OlmSessions
I've been trying to track down issues with the OlmSessions getting out of sync
between two devices. To help with this, add a method which can be used from the
JS console to inspect the state of OlmSessions.
2016-09-05 00:03:21 +01:00
Richard van der Hoff 5e0f09075d MatrixClient.getStoredDevicesForUser
Implement MatrixClient.getStoredDevicesForUser which uses
Crypto.getStoredDevicesForUser, which is more powerful than listDeviceKeys, and
isn't deprecated.

Also a couple of accessors for DeviceInfo.
2016-09-04 23:31:38 +01:00
Matthew Hodgson 2daa1b6007 change TimelineWindow to take a timelineSet rather than a Room 2016-09-04 13:57:56 +01:00
Matthew Hodgson 4ff2ad9fac s/EventTimelineList/EventTimelineSet/g at vdh's req 2016-09-03 22:27:29 +01:00
Matthew Hodgson ba06e8091f Merge branch 'develop' into matthew/filtered-timelines 2016-09-03 13:33:15 +01:00
Richard van der Hoff 692b3107ac Merge pull request #188 from matrix-org/rav/cleanups
Olm-related cleanups
2016-09-02 15:39:15 +01:00
Richard van der Hoff 6baf9e1c37 Olm-related cleanups
A couple of small refactors which fell out of the aborted stuff for upgrading
to secure Ed25519 keys, but are useful in their own right.

The main functional change here is to calculate the "algorithms" list from
DECRYPTION_CLASSES (and hence include megolm in the list).
2016-09-02 11:33:50 +01:00
Richard van der Hoff c07e662b90 Update to olm 1.1.0
Use fixed olm library
2016-09-02 11:18:58 +01:00
Matthew Hodgson aca8b32e5b Merge pull request #186 from matrix-org/matthew/uninterrupted-audio
always play audio out of the remoteAudioElement if it exists.
2016-09-01 10:54:20 +01:00
Matthew Hodgson 0ec8a6e0af always play audio out of the remoteAudioElement if it exists.\n\nfixes https://github.com/vector-im/vector-web/issues/1271 and https://github.com/vector-im/vector-web/issues/621 2016-08-31 21:52:24 +01:00
Matthew Hodgson 41af7c8883 Merge pull request #185 from matrix-org/matthew/webrtc-promises
Fix exceptions where HTMLMediaElement loads and plays race
2016-08-31 21:00:24 +01:00
Matthew Hodgson 7f2070f7b7 fix lint 2016-08-31 20:59:02 +01:00
Matthew Hodgson 1cc74ec116 don't break the promise chain if a play() fails 2016-08-31 20:57:08 +01:00
Matthew Hodgson 627f662384 PR review 2016-08-31 17:39:06 +01:00
Matthew Hodgson c791881c87 fix lint 2016-08-31 16:31:49 +01:00
Matthew Hodgson b0782885d5 kill unhandled exceptions where loads and plays race by queuing them as promises 2016-08-31 16:29:14 +01:00
Matthew Hodgson d25d60f0f0 make the tests pass again 2016-08-30 23:34:11 +01:00
Kegsay d356e722da Make example actually work 2016-08-30 15:11:06 +01:00
Richard van der Hoff 84e2fc91ae Merge pull request #183 from matrix-org/rav/reset_megolm_on_member_change
Reset megolm session when people join/leave the room
2016-08-30 14:58:18 +01:00
Richard van der Hoff 9768fb020c Merge pull request #184 from matrix-org/rav/fix_redactions
Fix exceptions when dealing with redactions
2016-08-30 14:53:48 +01:00
Richard van der Hoff e25112ad35 Fix exceptions when dealing with redactions
When we got a redaction event, we were adding the entire (circular) MatrixEvent
object for the redaction to the redacted event, which would then cause
exceptions down the line (particularly when dealing with gappy timelines).

We should only be adding the raw event.

Fixes (hopefully) https://github.com/vector-im/vector-web/issues/1389.
2016-08-30 14:30:12 +01:00
Matthew Hodgson e18b446190 unbreak filter text 2016-08-30 01:30:23 +01:00
Matthew Hodgson 0848d4ed10 reemit Room.timeline events correctly 2016-08-30 01:13:32 +01:00
Matthew Hodgson 7514aea813 make most things work other than Room.timeline firing 2016-08-30 01:11:47 +01:00
Matthew Hodgson 58031ab21d fix things until they almost work again... 2016-08-30 00:36:52 +01:00
Matthew Hodgson c1c2ca3ec1 tweak doc; make it build 2016-08-29 23:18:19 +01:00
Matthew Hodgson b863a363da WIP refactor 2016-08-29 21:08:35 +01:00
Matthew Hodgson b42db46abd WIP refactor 2016-08-29 21:06:53 +01:00
Matthew Hodgson d46863e199 fix syntax 2016-08-28 23:44:10 +01:00
Matthew Hodgson 751ce421cd Merge branch 'develop' into matthew/filtered-timelines 2016-08-28 18:49:54 +01:00
Richard van der Hoff b4f22310ea Reset megolm session when people join/leave the room 2016-08-26 11:24:59 +01:00
Matthew Hodgson dd5878015a WIP filtered timelines 2016-08-23 14:31:47 +01:00
145 changed files with 21661 additions and 226264 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"presets": ["es2015"],
"plugins": [
// this transforms async functions into generator functions, which
// are then made to use the regenerator module by babel's
// transform-regnerator plugin (which is enabled by es2015).
"transform-async-to-bluebird",
// This makes sure that the regenerator runtime is available to
// the transpiled code.
"transform-runtime",
],
}
+23
View File
@@ -0,0 +1,23 @@
# Copyright 2017 Aviral Dasgupta
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
root = true
[*]
charset=utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
+71
View File
@@ -0,0 +1,71 @@
module.exports = {
parser: "babel-eslint",
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
ecmaFeatures: {
}
},
env: {
browser: true,
node: true,
// babel's transform-runtime converts references to ES6 globals such as
// Promise and Map to core-js polyfills, so we can use ES6 globals.
es6: true,
},
extends: ["eslint:recommended", "google"],
rules: {
// rules we've always adhered to or now do
"max-len": ["error", {
code: 90,
ignoreComments: true,
}],
curly: ["error", "multi-line"],
"prefer-const": ["error"],
"comma-dangle": ["error", {
arrays: "always-multiline",
objects: "always-multiline",
imports: "always-multiline",
exports: "always-multiline",
functions: "always-multiline",
}],
// loosen jsdoc requirements a little
"require-jsdoc": ["error", {
require: {
FunctionDeclaration: false,
}
}],
"valid-jsdoc": ["error", {
requireParamDescription: false,
requireReturn: false,
requireReturnDescription: false,
}],
// rules we do not want from eslint-recommended
"no-console": ["off"],
"no-constant-condition": ["off"],
"no-empty": ["error", { "allowEmptyCatch": true }],
// rules we do not want from the google styleguide
"object-curly-spacing": ["off"],
"spaced-comment": ["off"],
// in principle we prefer single quotes, but life is too short
quotes: ["off"],
// rules we'd ideally like to adhere to, but the current
// code does not (in most cases because it's still ES5)
// we set these to warnings, and assert that the number
// of warnings doesn't exceed a given threshold
"no-var": ["warn"],
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
"prefer-rest-params": ["warn"],
"prefer-spread": ["warn"],
"one-var": ["warn"],
"padded-blocks": ["warn"],
"no-extend-native": ["warn"],
"camelcase": ["warn"],
}
}
+5 -3
View File
@@ -1,6 +1,6 @@
# Keep this file in sync with .npmignore.
/.jsdocbuild
/.jsdoc
.jsdoc
node_modules
.lock-wscript
build/Release
@@ -8,7 +8,9 @@ coverage
lib-cov
out
reports
dist/browser-matrix-dev.js
/dist
/lib
/specbuild
# version file and tarball created by 'npm pack'
/git-revision.txt
-11
View File
@@ -1,11 +0,0 @@
{
"node": true,
"jasmine": true,
"nonew": true,
"curly": true,
"forin": true,
"freeze": false,
"undef": true,
"unused": "vars"
}
-15
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
script:
- ./travis.sh
+760
View File
@@ -1,3 +1,763 @@
Changes in [0.8.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.3) (2017-09-20)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.3-rc.1...v0.8.3)
* No changes
Changes in [0.8.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.3-rc.1) (2017-09-19)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.2...v0.8.3-rc.1)
* consume trailing slash when creating Matrix Client in HS and IS urls
[\#526](https://github.com/matrix-org/matrix-js-sdk/pull/526)
* Add ignore users API
[\#539](https://github.com/matrix-org/matrix-js-sdk/pull/539)
* Upgrade to jsdoc 3.5.5
[\#540](https://github.com/matrix-org/matrix-js-sdk/pull/540)
* Make re-emitting events much more memory efficient
[\#538](https://github.com/matrix-org/matrix-js-sdk/pull/538)
* Only re-emit events from Event objects if needed
[\#536](https://github.com/matrix-org/matrix-js-sdk/pull/536)
* Handle 'left' users in the deviceList mananagement
[\#535](https://github.com/matrix-org/matrix-js-sdk/pull/535)
* Factor out devicelist integration tests to a separate file
[\#534](https://github.com/matrix-org/matrix-js-sdk/pull/534)
* Refactor sync._sync as an async function
[\#533](https://github.com/matrix-org/matrix-js-sdk/pull/533)
* Add es6 to eslint environments
[\#532](https://github.com/matrix-org/matrix-js-sdk/pull/532)
Changes in [0.8.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.2) (2017-08-24)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.1...v0.8.2)
* Handle m.call.* events which are decrypted asynchronously
[\#530](https://github.com/matrix-org/matrix-js-sdk/pull/530)
* Re-emit events from, er, Event objects
[\#529](https://github.com/matrix-org/matrix-js-sdk/pull/529)
Changes in [0.8.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.1) (2017-08-23)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.1-rc.1...v0.8.1)
* [No changes]
Changes in [0.8.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.1-rc.1) (2017-08-22)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.0...v0.8.1-rc.1)
* Fix error handling in interactive-auth
[\#527](https://github.com/matrix-org/matrix-js-sdk/pull/527)
* Make lots of OlmDevice asynchronous
[\#524](https://github.com/matrix-org/matrix-js-sdk/pull/524)
* Make crypto.decryptMessage return decryption results
[\#523](https://github.com/matrix-org/matrix-js-sdk/pull/523)
Changes in [0.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.0) (2017-08-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.13...v0.8.0)
BREAKING CHANGE
---------------
In order to support a move to a more scalable storage backend, we need to make
a number of the APIs related end-to-end encryption asynchronous.
This release of the JS-SDK includes the following changes which will affect
applications which support end-to-end encryption:
1. `MatrixClient` now provides a new (asynchronous) method,
`initCrypto`. Applications which support end-to-end encryption must call
this method (and wait for it to complete) before calling `startClient`, to
give the crypto layer a chance to initialise.
2. The following APIs have been changed to return promises:
* `MatrixClient.getStoredDevicesForUser`
* `MatrixClient.getStoredDevice`
* `MatrixClient.setDeviceVerified`
* `MatrixClient.setDeviceBlocked`
* `MatrixClient.setDeviceKnown`
* `MatrixClient.getEventSenderDeviceInfo`
* `MatrixClient.isEventSenderVerified`
* `MatrixClient.importRoomKeys`
Applications using the results of any of the above methods will need to be
updated to wait for the result of the promise.
3. `MatrixClient.listDeviceKeys` has been removed altogether. It's been
deprecated for some time. Applications using it should instead be changed to
use `MatrixClient.getStoredDevices`, which is similar but returns its results
in a slightly different format.
* Make bits of `olmlib` asynchronous
[\#521](https://github.com/matrix-org/matrix-js-sdk/pull/521)
* Make some of DeviceList asynchronous
[\#520](https://github.com/matrix-org/matrix-js-sdk/pull/520)
* Make methods in crypto/algorithms async
[\#519](https://github.com/matrix-org/matrix-js-sdk/pull/519)
* Avoid sending unencrypted messages in e2e room
[\#518](https://github.com/matrix-org/matrix-js-sdk/pull/518)
* Make tests wait for syncs to happen
[\#517](https://github.com/matrix-org/matrix-js-sdk/pull/517)
* Make a load of methods in the 'Crypto' module asynchronous
[\#510](https://github.com/matrix-org/matrix-js-sdk/pull/510)
* Set `rawDisplayName` to `userId` if membership has `displayname=null`
[\#515](https://github.com/matrix-org/matrix-js-sdk/pull/515)
* Refactor handling of crypto events for async
[\#508](https://github.com/matrix-org/matrix-js-sdk/pull/508)
* Let event decryption be asynchronous
[\#509](https://github.com/matrix-org/matrix-js-sdk/pull/509)
* Transform `async` functions to bluebird promises
[\#511](https://github.com/matrix-org/matrix-js-sdk/pull/511)
* Add more group APIs
[\#512](https://github.com/matrix-org/matrix-js-sdk/pull/512)
* Retrying test: wait for localEchoUpdated event
[\#507](https://github.com/matrix-org/matrix-js-sdk/pull/507)
* Fix member events breaking on timeline reset, 2
[\#504](https://github.com/matrix-org/matrix-js-sdk/pull/504)
* Make bits of the js-sdk api asynchronous
[\#503](https://github.com/matrix-org/matrix-js-sdk/pull/503)
* Yet more js-sdk test deflakification
[\#499](https://github.com/matrix-org/matrix-js-sdk/pull/499)
* Fix racy 'matrixclient retrying' test
[\#497](https://github.com/matrix-org/matrix-js-sdk/pull/497)
* Fix spamming of key-share-requests
[\#495](https://github.com/matrix-org/matrix-js-sdk/pull/495)
* Add progress handler to `uploadContent`
[\#500](https://github.com/matrix-org/matrix-js-sdk/pull/500)
* Switch matrix-js-sdk to bluebird
[\#490](https://github.com/matrix-org/matrix-js-sdk/pull/490)
* Fix some more flakey tests
[\#492](https://github.com/matrix-org/matrix-js-sdk/pull/492)
* make the npm test script windows-friendly
[\#489](https://github.com/matrix-org/matrix-js-sdk/pull/489)
* Fix a bunch of races in the tests
[\#488](https://github.com/matrix-org/matrix-js-sdk/pull/488)
* Fix early return in MatrixClient.setGuestAccess
[\#487](https://github.com/matrix-org/matrix-js-sdk/pull/487)
* Remove testUtils.failTest
[\#486](https://github.com/matrix-org/matrix-js-sdk/pull/486)
* Add test:watch script
[\#485](https://github.com/matrix-org/matrix-js-sdk/pull/485)
* Make it possible to use async/await
[\#484](https://github.com/matrix-org/matrix-js-sdk/pull/484)
* Remove m.new_device support
[\#483](https://github.com/matrix-org/matrix-js-sdk/pull/483)
* Use access-token in header
[\#478](https://github.com/matrix-org/matrix-js-sdk/pull/478)
* Sanity-check response from /thirdparty/protocols
[\#482](https://github.com/matrix-org/matrix-js-sdk/pull/482)
* Avoid parsing plain-text errors as JSON
[\#479](https://github.com/matrix-org/matrix-js-sdk/pull/479)
* Use external mock-request
[\#481](https://github.com/matrix-org/matrix-js-sdk/pull/481)
* Fix some races in the tests
[\#480](https://github.com/matrix-org/matrix-js-sdk/pull/480)
* Fall back to MemoryCryptoStore if indexeddb fails
[\#475](https://github.com/matrix-org/matrix-js-sdk/pull/475)
* Fix load failure in firefox when indexedDB is disabled
[\#474](https://github.com/matrix-org/matrix-js-sdk/pull/474)
* Fix a race in a test
[\#471](https://github.com/matrix-org/matrix-js-sdk/pull/471)
* Avoid throwing an unhandled error when the indexeddb is deleted
[\#470](https://github.com/matrix-org/matrix-js-sdk/pull/470)
* fix jsdoc
[\#469](https://github.com/matrix-org/matrix-js-sdk/pull/469)
* Handle m.forwarded_room_key events
[\#468](https://github.com/matrix-org/matrix-js-sdk/pull/468)
* Improve error reporting from indexeddbstore.clearDatabase
[\#466](https://github.com/matrix-org/matrix-js-sdk/pull/466)
* Implement sharing of megolm keys
[\#454](https://github.com/matrix-org/matrix-js-sdk/pull/454)
* Process received room key requests
[\#449](https://github.com/matrix-org/matrix-js-sdk/pull/449)
* Send m.room_key_request events when we fail to decrypt an event
[\#448](https://github.com/matrix-org/matrix-js-sdk/pull/448)
Changes in [0.7.13](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.13) (2017-06-22)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.12...v0.7.13)
* Fix failure on Tor browser
[\#473](https://github.com/matrix-org/matrix-js-sdk/pull/473)
* Fix issues with firefox private browsing
[\#472](https://github.com/matrix-org/matrix-js-sdk/pull/472)
Changes in [0.7.12](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.12) (2017-06-19)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.12-rc.1...v0.7.12)
* No changes
Changes in [0.7.12-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.12-rc.1) (2017-06-15)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.11...v0.7.12-rc.1)
* allow setting iceTransportPolicy to relay through forceTURN option
[\#462](https://github.com/matrix-org/matrix-js-sdk/pull/462)
Changes in [0.7.11](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.11) (2017-06-12)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.11-rc.1...v0.7.11)
* Add a bunch of logging around sending messages
[\#460](https://github.com/matrix-org/matrix-js-sdk/pull/460)
Changes in [0.7.11-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.11-rc.1) (2017-06-09)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.10...v0.7.11-rc.1)
* Make TimelineWindow.load resolve quicker if we have the events
[\#458](https://github.com/matrix-org/matrix-js-sdk/pull/458)
* Stop peeking when a matrix client is stopped
[\#451](https://github.com/matrix-org/matrix-js-sdk/pull/451)
* Update README: Clarify how to install libolm
[\#450](https://github.com/matrix-org/matrix-js-sdk/pull/450)
Changes in [0.7.10](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.10) (2017-06-02)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.9...v0.7.10)
* BREAKING CHANGE: The SDK no longer ``require``s ``olm`` - instead it expects
libolm to be provided as an ``Olm`` global. This will only affect
applications which use end-to-end encryption. See the
[README](README.md#end-to-end-encryption-support) for details.
* indexeddb-crypto-store: fix db deletion
[\#447](https://github.com/matrix-org/matrix-js-sdk/pull/447)
* Load Olm from the global rather than requiring it.
[\#446](https://github.com/matrix-org/matrix-js-sdk/pull/446)
Changes in [0.7.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.9) (2017-06-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8...v0.7.9)
* Initial framework for indexeddb-backed crypto store
[\#445](https://github.com/matrix-org/matrix-js-sdk/pull/445)
* Factor out reEmit to a common module
[\#444](https://github.com/matrix-org/matrix-js-sdk/pull/444)
* crypto/algorithms/base.js: Convert to es6
[\#443](https://github.com/matrix-org/matrix-js-sdk/pull/443)
* maySendRedactionForEvent for userId
[\#435](https://github.com/matrix-org/matrix-js-sdk/pull/435)
* MatrixClient: add getUserId()
[\#441](https://github.com/matrix-org/matrix-js-sdk/pull/441)
* Run jsdoc on a custom babeling of the source
[\#442](https://github.com/matrix-org/matrix-js-sdk/pull/442)
* Add in a public api getStoredDevice allowing clients to get a specific
device
[\#439](https://github.com/matrix-org/matrix-js-sdk/pull/439)
Changes in [0.7.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8) (2017-05-22)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8-rc.1...v0.7.8)
* No changes
Changes in [0.7.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8-rc.1) (2017-05-19)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7...v0.7.8-rc.1)
* Attempt to rework the release-tarball-signing stuff
[\#438](https://github.com/matrix-org/matrix-js-sdk/pull/438)
* ability to specify webrtc audio/video inputs for the lib to request
[\#427](https://github.com/matrix-org/matrix-js-sdk/pull/427)
* make screen sharing call FF friendly :D
[\#434](https://github.com/matrix-org/matrix-js-sdk/pull/434)
* Fix race in device list updates
[\#431](https://github.com/matrix-org/matrix-js-sdk/pull/431)
* WebRTC: Support recvonly for video for those without a webcam
[\#424](https://github.com/matrix-org/matrix-js-sdk/pull/424)
* Update istanbul to remove minimatch DoS Warning
[\#422](https://github.com/matrix-org/matrix-js-sdk/pull/422)
* webrtc/call: Make it much less likely that callIds collide locally
[\#423](https://github.com/matrix-org/matrix-js-sdk/pull/423)
* Automatically complete dummy auth
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
* Don't leave the gh-pages branch checked out
[\#418](https://github.com/matrix-org/matrix-js-sdk/pull/418)
Changes in [0.7.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7) (2017-04-25)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7-rc.1...v0.7.7)
* No changes
Changes in [0.7.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7-rc.1) (2017-04-21)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6...v0.7.7-rc.1)
* Automatically complete dummy auth
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
Changes in [0.7.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6) (2017-04-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.2...v0.7.6)
* No changes
Changes in [0.7.6-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.2) (2017-04-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.1...v0.7.6-rc.2)
* Add feature detection for webworkers
[\#416](https://github.com/matrix-org/matrix-js-sdk/pull/416)
* Fix release script
[\#415](https://github.com/matrix-org/matrix-js-sdk/pull/415)
Changes in [0.7.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.1) (2017-04-07)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5...v0.7.6-rc.1)
* Make indexeddb save after the first sync
[\#414](https://github.com/matrix-org/matrix-js-sdk/pull/414)
* Make indexeddb startup faster
[\#413](https://github.com/matrix-org/matrix-js-sdk/pull/413)
* Add ability to do indexeddb sync work in webworker
[\#412](https://github.com/matrix-org/matrix-js-sdk/pull/412)
* Move more functionality to the indexeddb backend
[\#409](https://github.com/matrix-org/matrix-js-sdk/pull/409)
* Indicate syncState ERROR after many failed /syncs
[\#410](https://github.com/matrix-org/matrix-js-sdk/pull/410)
* Further reorganising of indexeddb sync code
[\#407](https://github.com/matrix-org/matrix-js-sdk/pull/407)
* Change interface of IndexedDBStore: hide internals
[\#406](https://github.com/matrix-org/matrix-js-sdk/pull/406)
* Don't be SYNCING until updating from the server
[\#405](https://github.com/matrix-org/matrix-js-sdk/pull/405)
* Don't log the entire /sync response
[\#403](https://github.com/matrix-org/matrix-js-sdk/pull/403)
* webrtc/call: Assign MediaStream to video element srcObject
[\#402](https://github.com/matrix-org/matrix-js-sdk/pull/402)
* Fix undefined reference in http-api
[\#400](https://github.com/matrix-org/matrix-js-sdk/pull/400)
* Add copyright header to event-timeline.js
[\#382](https://github.com/matrix-org/matrix-js-sdk/pull/382)
* client: fix docs for user-scoped account_data events
[\#397](https://github.com/matrix-org/matrix-js-sdk/pull/397)
* Add a CONTRIBUTING for js-sdk
[\#399](https://github.com/matrix-org/matrix-js-sdk/pull/399)
* Fix leaking room state objects on limited sync responses
[\#395](https://github.com/matrix-org/matrix-js-sdk/pull/395)
* Extend 'ignoreFailure' to be 'background'
[\#396](https://github.com/matrix-org/matrix-js-sdk/pull/396)
* Add x_show_msisdn parameter to register calls
[\#388](https://github.com/matrix-org/matrix-js-sdk/pull/388)
* Update event redaction to keep sender and origin_server_ts
[\#394](https://github.com/matrix-org/matrix-js-sdk/pull/394)
* Handle 'limited' timeline responses in the SyncAccumulator
[\#393](https://github.com/matrix-org/matrix-js-sdk/pull/393)
* Give a better error message if the HS doesn't support msisdn registeration
[\#391](https://github.com/matrix-org/matrix-js-sdk/pull/391)
* Add getEmailSid
[\#383](https://github.com/matrix-org/matrix-js-sdk/pull/383)
* Add m.login.email.identity support to UI auth
[\#380](https://github.com/matrix-org/matrix-js-sdk/pull/380)
* src/client.js: Fix incorrect roomId reference in VoIP glare code
[\#381](https://github.com/matrix-org/matrix-js-sdk/pull/381)
* add .editorconfig
[\#379](https://github.com/matrix-org/matrix-js-sdk/pull/379)
* Store account data in the same way as room data
[\#377](https://github.com/matrix-org/matrix-js-sdk/pull/377)
* Upload one-time keys on /sync rather than a timer
[\#376](https://github.com/matrix-org/matrix-js-sdk/pull/376)
* Increase the WRITE_DELAY on database syncing
[\#374](https://github.com/matrix-org/matrix-js-sdk/pull/374)
* Make deleteAllData() return a Promise
[\#373](https://github.com/matrix-org/matrix-js-sdk/pull/373)
* Don't include banned users in the room name
[\#372](https://github.com/matrix-org/matrix-js-sdk/pull/372)
* Support IndexedDB as a backing store
[\#363](https://github.com/matrix-org/matrix-js-sdk/pull/363)
* Poll /sync with a short timeout while catching up
[\#370](https://github.com/matrix-org/matrix-js-sdk/pull/370)
* Make test coverage work again
[\#368](https://github.com/matrix-org/matrix-js-sdk/pull/368)
* Add docs to event
[\#367](https://github.com/matrix-org/matrix-js-sdk/pull/367)
* Keep the device-sync token more up-to-date
[\#366](https://github.com/matrix-org/matrix-js-sdk/pull/366)
* Fix race conditions in device list download
[\#365](https://github.com/matrix-org/matrix-js-sdk/pull/365)
* Fix the unban method
[\#364](https://github.com/matrix-org/matrix-js-sdk/pull/364)
* Spread out device verification work
[\#362](https://github.com/matrix-org/matrix-js-sdk/pull/362)
* Clean up/improve e2e logging
[\#361](https://github.com/matrix-org/matrix-js-sdk/pull/361)
* Fix decryption of events whose key arrives later
[\#360](https://github.com/matrix-org/matrix-js-sdk/pull/360)
* Invalidate device lists when encryption is enabled in a room
[\#359](https://github.com/matrix-org/matrix-js-sdk/pull/359)
* Switch from jasmine to mocha + expect + lolex
[\#358](https://github.com/matrix-org/matrix-js-sdk/pull/358)
* Install source-map-support in each test
[\#356](https://github.com/matrix-org/matrix-js-sdk/pull/356)
* searchMessageText: avoid setting keys=undefined
[\#357](https://github.com/matrix-org/matrix-js-sdk/pull/357)
* realtime-callbacks: pass `global` as `this`
[\#355](https://github.com/matrix-org/matrix-js-sdk/pull/355)
* Make the tests work without olm
[\#354](https://github.com/matrix-org/matrix-js-sdk/pull/354)
* Tests: Factor out TestClient and use it in crypto tests
[\#353](https://github.com/matrix-org/matrix-js-sdk/pull/353)
* Fix some lint
[\#352](https://github.com/matrix-org/matrix-js-sdk/pull/352)
* Make a sig for source tarballs when releasing
[\#351](https://github.com/matrix-org/matrix-js-sdk/pull/351)
* When doing a pre-release, don't bother merging to master and develop.
[\#350](https://github.com/matrix-org/matrix-js-sdk/pull/350)
Changes in [0.7.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5) (2017-02-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.3...v0.7.5)
No changes from 0.7.5-rc.3
Changes in [0.7.5-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.3) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.2...v0.7.5-rc.3)
* Include DeviceInfo in deviceVerificationChanged events
[a3cc8eb](https://github.com/matrix-org/matrix-js-sdk/commit/a3cc8eb1f6d165576a342596f638316721cb26b6)
* Fix device list update
[5fd7410](https://github.com/matrix-org/matrix-js-sdk/commit/5fd74109ffc56b73deb40c2604d84c38b8032c40)
Changes in [0.7.5-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.2) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.1...v0.7.5-rc.2)
* Use the device change notifications interface
[\#348](https://github.com/matrix-org/matrix-js-sdk/pull/348)
* Rewrite the device key query logic
[\#347](https://github.com/matrix-org/matrix-js-sdk/pull/347)
Changes in [0.7.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.1) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.4...v0.7.5-rc.1)
* Support for blacklisting unverified devices, both per-room and globally
[\#336](https://github.com/matrix-org/matrix-js-sdk/pull/336)
* track errors when events can't be sent
[\#349](https://github.com/matrix-org/matrix-js-sdk/pull/349)
* Factor out device list management
[\#346](https://github.com/matrix-org/matrix-js-sdk/pull/346)
* Support for warning users when unknown devices show up
[\#335](https://github.com/matrix-org/matrix-js-sdk/pull/335)
* Enable sourcemaps in browserified distro
[\#345](https://github.com/matrix-org/matrix-js-sdk/pull/345)
* Record all e2e room settings in localstorage
[\#344](https://github.com/matrix-org/matrix-js-sdk/pull/344)
* Make Olm work with browserified js-sdk
[\#340](https://github.com/matrix-org/matrix-js-sdk/pull/340)
* Make browserify a dev dependency
[\#339](https://github.com/matrix-org/matrix-js-sdk/pull/339)
* Allow single line brace-style
[\#338](https://github.com/matrix-org/matrix-js-sdk/pull/338)
* Turn on comma-dangle for function calls
[\#333](https://github.com/matrix-org/matrix-js-sdk/pull/333)
* Add prefer-const
[\#331](https://github.com/matrix-org/matrix-js-sdk/pull/331)
* Support for importing and exporting megolm sessions
[\#326](https://github.com/matrix-org/matrix-js-sdk/pull/326)
* Fix linting on all tests
[\#329](https://github.com/matrix-org/matrix-js-sdk/pull/329)
* Fix ESLint warnings and errors
[\#325](https://github.com/matrix-org/matrix-js-sdk/pull/325)
* BREAKING CHANGE: Remove WebStorageStore
[\#324](https://github.com/matrix-org/matrix-js-sdk/pull/324)
Changes in [0.7.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.4) (2017-01-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.4-rc.1...v0.7.4)
* Fix non-conference calling
Changes in [0.7.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.4-rc.1) (2017-01-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.3...v0.7.4-rc.1)
* Remove babel-polyfill
[\#321](https://github.com/matrix-org/matrix-js-sdk/pull/321)
* Update build process for ES6
[\#320](https://github.com/matrix-org/matrix-js-sdk/pull/320)
* 'babel' is not a babel package anymore
[\#319](https://github.com/matrix-org/matrix-js-sdk/pull/319)
* Add Babel for ES6 support
[\#318](https://github.com/matrix-org/matrix-js-sdk/pull/318)
* Move screen sharing check/error
[\#317](https://github.com/matrix-org/matrix-js-sdk/pull/317)
* release.sh: Bail early if there are uncommitted changes
[\#316](https://github.com/matrix-org/matrix-js-sdk/pull/316)
Changes in [0.7.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.3) (2017-01-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.2...v0.7.3)
* User presence list feature
[\#310](https://github.com/matrix-org/matrix-js-sdk/pull/310)
* Allow clients the ability to set a default local timeout
[\#313](https://github.com/matrix-org/matrix-js-sdk/pull/313)
* Add API to delete threepid
[\#312](https://github.com/matrix-org/matrix-js-sdk/pull/312)
Changes in [0.7.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.2) (2016-12-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1...v0.7.2)
* Bump to Olm 2.0
[\#309](https://github.com/matrix-org/matrix-js-sdk/pull/309)
* Sanity check payload length before encrypting
[\#307](https://github.com/matrix-org/matrix-js-sdk/pull/307)
* Remove dead _sendPingToDevice function
[\#308](https://github.com/matrix-org/matrix-js-sdk/pull/308)
* Add setRoomDirectoryVisibilityAppService
[\#306](https://github.com/matrix-org/matrix-js-sdk/pull/306)
* Update release script to do signed releases
[\#305](https://github.com/matrix-org/matrix-js-sdk/pull/305)
* e2e: Wait for pending device lists
[\#304](https://github.com/matrix-org/matrix-js-sdk/pull/304)
* Start a new megolm session when devices are blacklisted
[\#303](https://github.com/matrix-org/matrix-js-sdk/pull/303)
* E2E: Download our own devicelist on startup
[\#302](https://github.com/matrix-org/matrix-js-sdk/pull/302)
Changes in [0.7.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1) (2016-12-09)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1-rc.1...v0.7.1)
No changes
Changes in [0.7.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1-rc.1) (2016-12-05)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.0...v0.7.1-rc.1)
* Avoid NPE when no sessionStore is given
[\#300](https://github.com/matrix-org/matrix-js-sdk/pull/300)
* Improve decryption error messages
[\#299](https://github.com/matrix-org/matrix-js-sdk/pull/299)
* Revert "Use native Array.isArray when available."
[\#283](https://github.com/matrix-org/matrix-js-sdk/pull/283)
* Use native Array.isArray when available.
[\#282](https://github.com/matrix-org/matrix-js-sdk/pull/282)
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
* Avoid a packetstorm of device queries on startup
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
* E2E: Check devices to share keys with on each send
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
* Apply unknown-keyshare mitigations
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
* distinguish unknown users from deviceless users
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
* Allow starting client with initialSyncLimit = 0
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
* Make timeline-window _unpaginate public and rename to unpaginate
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
* Send a STOPPED sync updated after call to stopClient
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
* Fix bug in verifying megolm event senders
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
* Handle decryption of events after they arrive
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
* Fix examples.
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
* Add a travis.yml
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
* Encrypt all events, including 'm.call.*'
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
* Ignore reshares of known megolm sessions
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
* Log to the console on unknown session
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
* Make it easier for SDK users to wrap prevailing the 'request' function
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
* Change release script to pass version by environment variable
Changes in [0.6.4-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.2) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.1...v0.6.4-rc.2)
* Add getRoomTags method to client
[\#236](https://github.com/matrix-org/matrix-js-sdk/pull/236)
Changes in [0.6.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.1) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.3...v0.6.4-rc.1)
Breaking Changes
----------------
* Bundled version of the JS SDK are no longer versioned along with
source files in the dist/ directory. As of this release, they
will be included in the release tarball, but not the source
repository.
Other Changes
-------------
* More fixes to the release script
[\#272](https://github.com/matrix-org/matrix-js-sdk/pull/272)
* Update the release process to use github releases
[\#271](https://github.com/matrix-org/matrix-js-sdk/pull/271)
* Don't package the world when we release
[\#270](https://github.com/matrix-org/matrix-js-sdk/pull/270)
* Add ability to set a filter prior to the first /sync
[\#269](https://github.com/matrix-org/matrix-js-sdk/pull/269)
* Sign one-time keys, and verify their signatures
[\#243](https://github.com/matrix-org/matrix-js-sdk/pull/243)
* Check for duplicate message indexes for group messages
[\#241](https://github.com/matrix-org/matrix-js-sdk/pull/241)
* Rotate megolm sessions
[\#240](https://github.com/matrix-org/matrix-js-sdk/pull/240)
* Check recipient and sender in Olm messages
[\#239](https://github.com/matrix-org/matrix-js-sdk/pull/239)
* Consistency checks for E2E device downloads
[\#237](https://github.com/matrix-org/matrix-js-sdk/pull/237)
* Support User-Interactive auth for delete device
[\#235](https://github.com/matrix-org/matrix-js-sdk/pull/235)
* Utility to help with interactive auth
[\#234](https://github.com/matrix-org/matrix-js-sdk/pull/234)
* Fix sync breaking when an invalid filterId is in localStorage
[\#228](https://github.com/matrix-org/matrix-js-sdk/pull/228)
Changes in [0.6.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.3) (2016-10-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.2...v0.6.3)
Breaking Changes
----------------
* Add a 'RECONNECTING' state to the sync states. This is an additional state
between 'SYNCING' and 'ERROR', so most clients should not notice.
Other Changes
----------------
* Fix params getting replaced on register calls
[\#233](https://github.com/matrix-org/matrix-js-sdk/pull/233)
* Fix potential 30s delay on reconnect
[\#232](https://github.com/matrix-org/matrix-js-sdk/pull/232)
* uploadContent: Attempt some consistency between browser and node
[\#230](https://github.com/matrix-org/matrix-js-sdk/pull/230)
* Fix error handling on uploadContent
[\#229](https://github.com/matrix-org/matrix-js-sdk/pull/229)
* Fix uploadContent for node.js
[\#226](https://github.com/matrix-org/matrix-js-sdk/pull/226)
* Don't emit ERROR until a keepalive poke fails
[\#223](https://github.com/matrix-org/matrix-js-sdk/pull/223)
* Function to get the fallback url for interactive auth
[\#224](https://github.com/matrix-org/matrix-js-sdk/pull/224)
* Revert "Handle the first /sync failure differently."
[\#222](https://github.com/matrix-org/matrix-js-sdk/pull/222)
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
* Check dependencies aren't on develop in release.sh
[\#221](https://github.com/matrix-org/matrix-js-sdk/pull/221)
* Fix checkTurnServers leak on logout
[\#220](https://github.com/matrix-org/matrix-js-sdk/pull/220)
* Fix leak of file upload objects
[\#219](https://github.com/matrix-org/matrix-js-sdk/pull/219)
* crypto: remove duplicate code
[\#218](https://github.com/matrix-org/matrix-js-sdk/pull/218)
* Add API for 3rd party location lookup
[\#217](https://github.com/matrix-org/matrix-js-sdk/pull/217)
* Handle the first /sync failure differently.
[\#216](https://github.com/matrix-org/matrix-js-sdk/pull/216)
Changes in [0.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.1) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.0...v0.6.1)
* Fix the ed25519 key checking
[\#215](https://github.com/matrix-org/matrix-js-sdk/pull/215)
* Add MatrixClient.getEventSenderDeviceInfo()
[\#214](https://github.com/matrix-org/matrix-js-sdk/pull/214)
Changes in [0.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.0) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.6...v0.6.0)
* Pull user device list on join
[\#212](https://github.com/matrix-org/matrix-js-sdk/pull/212)
* Fix sending of oh_hais on bad sessions
[\#213](https://github.com/matrix-org/matrix-js-sdk/pull/213)
* Support /publicRooms pagination
[\#211](https://github.com/matrix-org/matrix-js-sdk/pull/211)
* Update the olm library version to 1.3.0
[\#205](https://github.com/matrix-org/matrix-js-sdk/pull/205)
* Comment what the logic in uploadKeys does
[\#209](https://github.com/matrix-org/matrix-js-sdk/pull/209)
* Include keysProved and keysClaimed in the local echo for events we send.
[\#210](https://github.com/matrix-org/matrix-js-sdk/pull/210)
* Check if we need to upload new one-time keys every 10 minutes
[\#208](https://github.com/matrix-org/matrix-js-sdk/pull/208)
* Reset oneTimeKey to null on each loop iteration.
[\#207](https://github.com/matrix-org/matrix-js-sdk/pull/207)
* Add getKeysProved and getKeysClaimed methods to MatrixEvent.
[\#206](https://github.com/matrix-org/matrix-js-sdk/pull/206)
* Send a 'm.new_device' when we get a message for an unknown group session
[\#204](https://github.com/matrix-org/matrix-js-sdk/pull/204)
* Introduce EventTimelineSet, filtered timelines and global notif timeline.
[\#196](https://github.com/matrix-org/matrix-js-sdk/pull/196)
* Wrap the crypto event handlers in try/catch blocks
[\#203](https://github.com/matrix-org/matrix-js-sdk/pull/203)
* Show warnings on to-device decryption fail
[\#202](https://github.com/matrix-org/matrix-js-sdk/pull/202)
* s/Displayname/DisplayName/
[\#201](https://github.com/matrix-org/matrix-js-sdk/pull/201)
* OH HAI
[\#200](https://github.com/matrix-org/matrix-js-sdk/pull/200)
* Share the current ratchet with new members
[\#199](https://github.com/matrix-org/matrix-js-sdk/pull/199)
* Move crypto bits into a subdirectory
[\#198](https://github.com/matrix-org/matrix-js-sdk/pull/198)
* Refactor event handling in Crypto
[\#197](https://github.com/matrix-org/matrix-js-sdk/pull/197)
* Don't create Olm sessions proactively
[\#195](https://github.com/matrix-org/matrix-js-sdk/pull/195)
* Use to-device events for key sharing
[\#194](https://github.com/matrix-org/matrix-js-sdk/pull/194)
* README: callbacks deprecated
[\#193](https://github.com/matrix-org/matrix-js-sdk/pull/193)
* Fix sender verification for megolm messages
[\#192](https://github.com/matrix-org/matrix-js-sdk/pull/192)
* Use `ciphertext` instead of `body` in megolm events
[\#191](https://github.com/matrix-org/matrix-js-sdk/pull/191)
* Add debug methods to get the state of OlmSessions
[\#189](https://github.com/matrix-org/matrix-js-sdk/pull/189)
* MatrixClient.getStoredDevicesForUser
[\#190](https://github.com/matrix-org/matrix-js-sdk/pull/190)
* Olm-related cleanups
[\#188](https://github.com/matrix-org/matrix-js-sdk/pull/188)
* Update to fixed olmlib
[\#187](https://github.com/matrix-org/matrix-js-sdk/pull/187)
* always play audio out of the remoteAudioElement if it exists.
[\#186](https://github.com/matrix-org/matrix-js-sdk/pull/186)
* Fix exceptions where HTMLMediaElement loads and plays race
[\#185](https://github.com/matrix-org/matrix-js-sdk/pull/185)
* Reset megolm session when people join/leave the room
[\#183](https://github.com/matrix-org/matrix-js-sdk/pull/183)
* Fix exceptions when dealing with redactions
[\#184](https://github.com/matrix-org/matrix-js-sdk/pull/184)
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
+115
View File
@@ -0,0 +1,115 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
How to contribute
~~~~~~~~~~~~~~~~~
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
**The single biggest thing you need to know is: please base your changes on
the develop branch - /not/ master.**
We use the master branch to track the most recent release, so that folks who
blindly clone the repo and automatically check out master get something that
works. Develop is the unstable branch where all the development actually
happens: the workflow is that contributors should fork the develop branch to
make a 'feature' branch for a particular contribution, and then make a pull
request to merge this back into the matrix.org 'official' develop branch. We
use github's pull request workflow to review the contribution, and either ask
you to make any refinements needed or merge it and make them ourselves. The
changes will then land on master when we next do a release.
We use Travis for continuous integration, and all pull requests get
automatically tested by Travis: if your change breaks the build, then the PR
will show that there are failed checks, so please check back after a few
minutes.
Code style
~~~~~~~~~~
The code-style for matrix-js-sdk is not formally documented, but contributors
are encouraged to read the code style document for matrix-react-sdk
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and **never** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
~~~~~~~~~~~
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
~~~~~~~~
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've adopted the
same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix::
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment::
Signed-off-by: Your Name <your@email.example.org>
...using your real name; unfortunately pseudonyms and anonymous contributions
can't be accepted. Git makes this trivial - just use the -s flag when you do
``git commit``, having first set ``user.name`` and ``user.email`` git configs
(which you should have done anyway :)
+81 -32
View File
@@ -10,11 +10,13 @@ Quickstart
In a browser
------------
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
your page. There will be a global variable ``matrixcs`` attached to
``window`` through which you can access the SDK.
Download either the full or minified version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
Please check [the working browser example](examples/browser) for more information.
Please check [the working browser example](examples/browser) for more information.
In Node.js
----------
@@ -28,8 +30,9 @@ In Node.js
console.log("Public Rooms: %s", JSON.stringify(data));
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
Please check [the Node.js terminal app](examples/node) for a more complex example.
What does this SDK do?
----------------------
@@ -64,6 +67,7 @@ Later versions of the SDK will:
Usage
=====
Conventions
-----------
@@ -78,7 +82,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 +92,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 +156,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 +175,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 +207,10 @@ Output:
);
}
});
matrixClient.startClient();
```
Output:
```
My Room
@@ -211,7 +220,7 @@ Output:
(join) Bob
(invite) @charlie:localhost
```
API Reference
=============
@@ -226,9 +235,49 @@ 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.
End-to-end encryption support
=============================
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](http://matrix.org/git/olm). It is left up to the application to make
libolm available, via the ``Olm`` global.
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
initialise the crypto layer.
If the ``Olm`` global is not available, the SDK will show a warning, as shown
below; ``initCrypto()`` will also fail.
```
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
```
If the crypto layer is not (successfully) initialised, the SDK will continue to
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
specification.
To provide the Olm library in a browser application:
* download the transpiled libolm (from https://matrix.org/packages/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
To provide the Olm library in a node.js application:
* ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz``
(replace the URL with the latest version you want to use from
https://matrix.org/packages/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you
can use ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
--save-optional`` (if your application also works without e2e crypto enabled)
or ``--save`` (if it doesn't) to do so.
Contributing
============
*This section is for people who want to modify the SDK. If you just
@@ -256,7 +305,7 @@ To run tests (Jasmine)::
```
$ npm test
```
To run linting:
```
$ npm run lint
-14
View File
@@ -1,14 +0,0 @@
There is a script `release.sh` which does the following, but if you need to do
a release manually, here are the steps:
- `git checkout -b release-v0.x.x`
- Update `CHANGELOG.md`
- `npm version 0.x.x`
- Merge `release-v0.x.x` onto `master`.
- Push `master`.
- Push the tag: `git push --tags`
- `npm publish`
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
- Merge `master` onto `develop`.
- Push `develop`.
+19
View File
@@ -1,4 +1,23 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("browser-request"));
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
var indexedDB;
try {
indexedDB = global.indexedDB;
} catch(e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto"
);
}
);
}
module.exports = matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
-5826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-6490
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-9900
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10023
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10864
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15420
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15577
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15785
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15832
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-16305
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-16327
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-37345
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-41222
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-1
View File
@@ -1 +0,0 @@
Release builds and development builds will reside here.
+31
View File
@@ -0,0 +1,31 @@
Random notes from Matthew on the two possible approaches for warning users about unexpected
unverified devices popping up in their rooms....
Original idea...
================
Warn when an existing user adds an unknown device to a room.
Warn when a user joins the room with unverified or unknown devices.
Warn when you initial sync if the room has any unverified devices in it.
^ this is good enough if we're doing local storage.
OR, better:
Warn when you initial sync if the room has any new undefined devices since you were last there.
=> This means persisting the rooms that devices are in, across initial syncs.
Updated idea...
===============
Warn when the user tries to send a message:
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
+1 -1
View File
@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js
+9 -5
View File
@@ -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() {
+10 -2
View File
@@ -44,7 +44,15 @@ window.onload = function() {
disableButtons(true, true, true);
};
client.on("syncComplete", function () {
client.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
break;
}
});
function syncComplete() {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true);
@@ -85,5 +93,5 @@ client.on("syncComplete", function () {
call = c;
addListeners(call);
});
});
}
client.startClient();
+1 -1
View File
@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js
+13 -10
View File
@@ -1,9 +1,12 @@
#!/bin/bash -l
export NVM_DIR="/home/jenkins/.nvm"
set -x
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 0.10
npm install
nvm use 6 || exit $?
npm install || exit $?
RC=0
@@ -12,15 +15,13 @@ function fail {
RC=1
}
# don't use last time's test reports
rm -rf reports coverage || exit $?
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 $?"
npm run -s lint -- -f checkstyle > eslint.xml ||
fail "eslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
@@ -28,4 +29,6 @@ rm -f matrix-js-sdk-*.tgz
npm pack ||
fail "npm pack finished with return code $?"
npm run gendoc || fail "JSDoc failed with code $?"
exit $RC
-614
View File
@@ -1,614 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module 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: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 {
var e2eAccount = this._sessionStore.getEndToEndAccount();
if (e2eAccount === null) {
account.create();
var pickled = account.pickle(this._pickleKey);
this._sessionStore.storeEndToEndAccount(pickled);
} else {
account.unpickle(this._pickleKey, e2eAccount);
}
e2eKeys = JSON.parse(account.identity_keys());
} finally {
account.free();
}
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
}
/**
* extract our OlmAccount from the session store and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getAccount = function(func) {
var account = new Olm.Account();
try {
var pickledAccount = this._sessionStore.getEndToEndAccount();
account.unpickle(this._pickleKey, pickledAccount);
return func(account);
} finally {
account.free();
}
};
/**
* store our OlmAccount in the session store
*
* @param {OlmAccount} account
* @private
*/
OlmDevice.prototype._saveAccount = function(account) {
var pickledAccount = account.pickle(this._pickleKey);
this._sessionStore.storeEndToEndAccount(pickledAccount);
};
/**
* extract an OlmSession from the session store and call the given function
*
* @param {string} deviceKey
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
var sessions = this._sessionStore.getEndToEndSessions(deviceKey);
var pickledSession = sessions[sessionId];
var session = new Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
return func(session);
} finally {
session.free();
}
};
/**
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {OlmSession} session
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session) {
var pickledSession = session.pickle(this._pickleKey);
this._sessionStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession
);
};
/**
* get an OlmUtility and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
var utility = new Olm.Utility();
try {
return func(utility);
} finally {
utility.free();
}
};
/**
* Signs a message with the ed25519 key for this account.
*
* @param {string} message message to be signed
* @return {string} base64-encoded signature
*/
OlmDevice.prototype.sign = function(message) {
return this._getAccount(function(account) {
return account.sign(message);
});
};
/**
* Get the current (unused, unpublished) one-time keys for this account.
*
* @return {object} one time keys; an object with the single property
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key.
*/
OlmDevice.prototype.getOneTimeKeys = function() {
return this._getAccount(function(account) {
return JSON.parse(account.one_time_keys());
});
};
/**
* Get the maximum number of one-time keys we can store.
*
* @return {number} number of keys
*/
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
return this._getAccount(function(account) {
return account.max_number_of_one_time_keys();
});
};
/**
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = function() {
var self = this;
this._getAccount(function(account) {
account.mark_keys_as_published();
self._saveAccount(account);
});
};
/**
* Generate some new one-time keys
*
* @param {number} numKeys number of keys to generate
*/
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
var self = this;
this._getAccount(function(account) {
account.generate_one_time_keys(numKeys);
self._saveAccount(account);
});
};
/**
* Generate a new outbound session
*
* The new session will be stored in the sessionStore.
*
* @param {string} theirIdentityKey remote user's Curve25519 identity key
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundSession = function(
theirIdentityKey, theirOneTimeKey
) {
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
self._saveSession(theirIdentityKey, session);
return session.session_id();
} finally {
session.free();
}
});
};
/**
* Generate a new inbound session, given an incoming message
*
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
* @param {number} message_type message_type field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {string} decrypted payload
*
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = function(
theirDeviceIdentityKey, message_type, ciphertext
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
}
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
self._saveAccount(account);
var payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return payloadString;
} finally {
session.free();
}
});
};
/**
* Get a list of known session IDs for the given device
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string[]} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
var sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey
);
return utils.keys(sessions);
};
/**
* 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];
};
/**
* Encrypt an outgoing message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptMessage = function(
theirDeviceIdentityKey, sessionId, payloadString
) {
var self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveSession(theirDeviceIdentityKey, session);
return res;
});
};
/**
* Decrypt an incoming message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {object} Result, with keys <ul>
* <li><tt>matchesInbound</tt>: (boolean): true if the message was a pre-key
* message which matched an existing inbound session.</li>
* <li><tt>payload</tt>: (string): decrypted payload; null if the received
* message was not valid (for instance, it did not match this session).</li>
* </ul>
*/
OlmDevice.prototype.decryptMessage = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
) {
var self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var matchesInbound = (message_type === 0 &&
session.matches_inbound(ciphertext));
var payloadString = null;
try {
payloadString = session.decrypt(message_type, ciphertext);
} catch (e) {
console.log(
"Failed to decrypt with an existing session: " + e.message
);
// return null as the payload
}
self._saveSession(theirDeviceIdentityKey, session);
return {
matchesInbound: matchesInbound,
payload: payloadString,
};
});
};
// 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} senderKey
* @param {string} sessionId
* @param {Olm.InboundGroupSession} session
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderKey, sessionId, session
) {
var r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderKey, 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} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func
) {
var r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId
);
if (r === null) {
throw new Error("Unknown inbound group session id");
}
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);
} 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 at index chainIndex
* @param {number} chainIndex index at which sessionKey applies
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, chainIndex
) {
var self = this;
var session = new Olm.InboundGroupSession();
try {
session.create(chainIndex, sessionKey);
self._saveInboundGroupSession(roomId, senderKey, sessionId, session);
} 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 {string} plaintext
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body
) {
var self = this;
function decrypt(session) {
var res = session.decrypt(body);
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session
);
return res;
}
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;
-162
View File
@@ -1,162 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module crypto-algorithms/base
*/
var q = require("q");
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.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module: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
*/
var EncryptionAlgorithm = function(params) {
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
/**
* Initialise this EncryptionAlgorithm instance for a particular room.
*
* <p>This will be called once per EncryptionAlgorithm, just after the
* constructor is called.
*
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
EncryptionAlgorithm.prototype.initRoomEncryption = function(roomMembers) {
return q();
};
/**
* 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
*/
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto-algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @param {module:OlmDevice} params.olmDevice olm.js wrapper
*/
var DecryptionAlgorithm = function(params) {
this._olmDevice = params.olmDevice;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
/**
* Decrypt an event
*
* @method module:crypto-algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @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:modules/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;
};
-257
View File
@@ -1,257 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* 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");
/**
* 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);
this._prepPromise = null;
this._outboundSessionId = null;
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves when setup is
* complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
if (this._outboundSessionId) {
// prep already done
return q();
}
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, key.chain_index
);
// send the keys to each (unblocked) device in the room.
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session_id,
session_key: key.key,
chain_index: key.chain_index,
}
};
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var self = this;
// TODO: we need to give the user a chance to block any devices or users
// before we send them the keys; it's too late to download them here.
this._prepPromise = this._crypto.downloadKeys(
roomMembers, false
).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function(devicemap) {
// TODO: send OOB messages. for now, send an in-band message. Each
// encrypted copy of the key takes up about 1K, so we'll only manage
// about 60 copies before we hit the event size limit; but ultimately the
// OOB messaging API will solve that problem for us.
var participantKeys = [];
for (var userId in devicemap) {
if (!devicemap.hasOwnProperty(userId)) {
continue;
}
var devices = devicemap[userId];
for (var deviceId in devices) {
if (!devices.hasOwnProperty(deviceId)) {
continue;
}
var deviceInfo = devices[deviceId].device;
participantKeys.push(deviceInfo.getIdentityKey());
}
}
var encryptedContent = olmlib.encryptMessageForDevices(
self._deviceId,
self._olmDevice,
participantKeys,
payload
);
var txnId = '' + (new Date().getTime());
var path = utils.encodeUri(
"/rooms/$roomId/send/m.room.encrypted/$txnId", {
$roomId: self._roomId,
$txnId: txnId,
}
);
// TODO: retries
return self._baseApis._http.authedRequest(
undefined, "PUT", path, undefined, encryptedContent
);
}).then(function() {
// don't set this until the keys are sent successfully; if we get an
// error, the user can restart by resending the message.
self._outboundSessionId = session_id;
}).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
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
var self = this;
return this._ensureOutboundSession(room).then(function() {
var payloadJson = {
room_id: self._roomId,
type: eventType,
content: content
};
var ciphertext = self._olmDevice.encryptGroupMessage(
self._outboundSessionId, JSON.stringify(payloadJson)
);
var encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
body: ciphertext,
session_id: self._outboundSessionId,
signature: "FIXME",
};
return encryptedContent;
});
};
/**
* 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);
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @throws {module:crypto-algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
console.log("decrypting " + event.event_id + " with sid " +
content.session_id);
if (!content.sender_key || !content.session_id ||
!content.body || !content.signature
) {
throw new base.DecryptionError("Missing fields in input");
}
try {
var res = this._olmDevice.decryptGroupMessage(
event.room_id, content.sender_key, content.session_id, content.body
);
return JSON.parse(res);
} catch (e) {
throw new base.DecryptionError(e);
}
};
/**
* @inheritdoc
*
* @param {module:modules/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 ||
content.chain_index === undefined
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, content.chain_index
);
};
base.registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
);
-188
View File
@@ -1,188 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto-algorithms/olm
*/
var q = require('q');
var utils = require("../utils");
var olmlib = require("../olmlib");
var DeviceInfo = require("../crypto-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);
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
/**
* @inheritdoc
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype.initRoomEncryption = function(roomMembers) {
var crypto = this._crypto;
return crypto.downloadKeys(roomMembers, true).then(function(res) {
return crypto.ensureOlmSessionsForUsers(roomMembers);
});
};
/**
* @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 participantKeys = [];
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
var devices = this._crypto.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var key = deviceInfo.getIdentityKey();
if (key == this._olmDevice.deviceCurve25519Key) {
// don't bother setting up session to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
participantKeys.push(key);
}
}
return q(
olmlib.encryptMessageForDevices(
this._deviceId, this._olmDevice, participantKeys, {
room_id: room.roomId,
type: eventType,
content: content,
}
)
);
};
/**
* 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 {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @throws {module:crypto-algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
}
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
var payloadString = null;
var foundSession = false;
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._olmDevice.decryptMessage(
deviceKey, sessionId, message.type, message.body
);
payloadString = res.payload;
if (payloadString) {
console.log("decrypted with sessionId " + sessionId);
break;
}
if (res.matchesInbound) {
// this was a prekey message which matched this session; don't
// create a new session.
foundSession = true;
break;
}
}
if (message.type === 0 && !foundSession && payloadString === null) {
try {
payloadString = this._olmDevice.createInboundSession(
deviceKey, message.type, message.body
);
console.log("created new inbound sesion");
} catch (e) {
// Failed to decrypt with a new session.
}
}
// TODO: Check the sender user id matches the sender key.
// TODO: check the room_id and fingerprint
if (payloadString !== null) {
return JSON.parse(payloadString);
} else {
throw new base.DecryptionError("Bad Encrypted Message");
}
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
-758
View File
@@ -1,758 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto
*/
var anotherjson = require('another-json');
var q = require("q");
var OlmDevice = require("./OlmDevice");
var olmlib = require("./olmlib");
var algorithms = require("./crypto-algorithms");
var DeviceInfo = require("./crypto-deviceinfo");
var DeviceVerification = DeviceInfo.DeviceVerification;
/**
* Cryptography bits
*
* @alias module:crypto.Crypto
* @constructor
* @alias module:crypto
*
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
*
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
* Store to be used for end-to-end crypto session data
*
* @param {string} userId The user ID for the local user
*
* @param {string} deviceId The identifier for this device.
*/
function Crypto(baseApis, sessionStore, userId, deviceId) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._userId = userId;
this._deviceId = deviceId;
this._olmDevice = new OlmDevice(sessionStore);
// EncryptionAlgorithm instance for each room
this._roomAlgorithms = {};
// build our device keys: these will later be uploaded
this._deviceKeys = {};
this._deviceKeys["ed25519:" + this._deviceId] =
this._olmDevice.deviceEd25519Key;
this._deviceKeys["curve25519:" + this._deviceId] =
this._olmDevice.deviceCurve25519Key;
// add our own deviceinfo to the sessionstore
var deviceInfo = {
keys: this._deviceKeys,
algorithms: [olmlib.OLM_ALGORITHM],
verified: DeviceVerification.VERIFIED,
};
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
this._userId
) || {};
myDevices[this._deviceId] = deviceInfo;
this._sessionStore.storeEndToEndDevicesForUser(
this._userId, myDevices
);
}
/**
* Get the Ed25519 key for this device
*
* @return {string} base64-encoded ed25519 key.
*/
Crypto.prototype.getDeviceEd25519Key = function() {
return this._olmDevice.deviceEd25519Key;
};
/**
* Upload the device keys to the homeserver and ensure that the
* homeserver has enough one-time keys.
* @param {number} maxKeys The maximum number of keys to generate
* @return {object} A promise that will resolve when the keys are uploaded.
*/
Crypto.prototype.uploadKeys = function(maxKeys) {
var self = this;
return _uploadDeviceKeys(this).then(function(res) {
var keyCount = res.one_time_key_counts.curve25519 || 0;
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
var keyLimit = Math.floor(maxOneTimeKeys / 2);
var numberToGenerate = Math.max(keyLimit - keyCount, 0);
if (maxKeys !== undefined) {
numberToGenerate = Math.min(numberToGenerate, maxKeys);
}
if (numberToGenerate <= 0) {
return;
}
self._olmDevice.generateOneTimeKeys(numberToGenerate);
return _uploadOneTimeKeys(self);
});
};
// returns a promise which resolves to the response
function _uploadDeviceKeys(crypto) {
var userId = crypto._userId;
var deviceId = crypto._deviceId;
var deviceKeys = {
algorithms: [olmlib.OLM_ALGORITHM],
device_id: deviceId,
keys: crypto._deviceKeys,
user_id: userId,
};
var sig = crypto._olmDevice.sign(anotherjson.stringify(deviceKeys));
deviceKeys.signatures = {};
deviceKeys.signatures[userId] = {};
deviceKeys.signatures[userId]["ed25519:" + deviceId] = sig;
return crypto._baseApis.uploadKeysRequest({
device_keys: deviceKeys,
}, {
// for now, we set the device id explicitly, as we may not be using the
// same one as used in login.
device_id: deviceId,
});
}
// returns a promise which resolves to the response
function _uploadOneTimeKeys(crypto) {
var oneTimeKeys = crypto._olmDevice.getOneTimeKeys();
var oneTimeJson = {};
for (var keyId in oneTimeKeys.curve25519) {
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
}
}
return crypto._baseApis.uploadKeysRequest({
one_time_keys: oneTimeJson
}, {
// for now, we set the device id explicitly, as we may not be using the
// same one as used in login.
device_id: crypto._deviceId,
}).then(function(res) {
crypto._olmDevice.markKeysAsPublished();
return res;
});
}
/**
* Download the keys for a list of users and stores the keys in the session
* store.
* @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached.
*
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto-deviceinfo|DeviceInfo}.
*/
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
var self = this;
// map from userid -> deviceid -> DeviceInfo
var stored = {};
// list of userids we need to download keys for
var downloadUsers = [];
for (var i = 0; i < userIds.length; ++i) {
var userId = userIds[i];
stored[userId] = {};
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var dev = devices[j];
stored[userId][dev.deviceId] = dev;
}
if (devices.length === 0 || forceDownload) {
downloadUsers.push(userId);
}
}
if (downloadUsers.length === 0) {
return q(stored);
}
return this._baseApis.downloadKeysForUsers(
downloadUsers
).then(function(res) {
for (var userId in res.device_keys) {
if (!stored.hasOwnProperty(userId)) {
// spurious result
continue;
}
// map from deviceid -> deviceinfo for this user
var userStore = stored[userId];
var updated = _updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, res.device_keys[userId]
);
if (!updated) {
continue;
}
// update the session store
var storage = {};
for (var deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
storage[deviceId] = userStore[deviceId].toStorage();
self._sessionStore.storeEndToEndDevicesForUser(
userId, storage
);
}
}
return stored;
});
};
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) {
var updated = false;
// remove any devices in the store which aren't in the response
for (var deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
if (!(deviceId in userResult)) {
console.log("Device " + userId + ":" + deviceId +
" has been removed");
delete userStore[deviceId];
updated = true;
}
}
for (deviceId in userResult) {
if (!userResult.hasOwnProperty(deviceId)) {
continue;
}
if (_storeDeviceKeys(
_olmDevice, userId, deviceId, userStore, userResult[deviceId]
)) {
updated = true;
}
}
return updated;
}
/*
* Process a device in a /query response, and add it to the userStore
*
* returns true if a change was made, else false
*/
function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult) {
if (!deviceResult.keys) {
// no keys?
return false;
}
var signKeyId = "ed25519:" + deviceId;
var signKey = deviceResult.keys[signKeyId];
if (!signKey) {
console.log("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
var unsigned = deviceResult.unsigned;
var signatures = deviceResult.signatures || {};
var userSigs = signatures[userId] || {};
var signature = userSigs[signKeyId];
if (!signature) {
console.log("Device " + userId + ":" + deviceId +
" is not signed");
return false;
}
// prepare the canonical json: remove 'unsigned' and signatures, and
// stringify with anotherjson
delete deviceResult.unsigned;
delete deviceResult.signatures;
var json = anotherjson.stringify(deviceResult);
try {
_olmDevice.verifySignature(signKey, json, signature);
} catch (e) {
console.log("Unable to verify signature on device " +
userId + ":" + deviceId + ":", e);
return false;
}
// DeviceInfo
var deviceStore;
if (deviceId in userStore) {
// already have this device.
deviceStore = userStore[deviceId];
if (deviceStore.getFingerprint() != signKey) {
// this should only happen if the list has been MITMed; we are
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
console.warn("Ed25519 key for device" + userId + ": " +
deviceId + " has changed");
return false;
}
} else {
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
}
deviceStore.keys = deviceResult.keys;
deviceStore.algorithms = deviceResult.algorithms;
deviceStore.unsigned = unsigned;
return true;
}
/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto-deviceinfo[]} list of devices
*/
Crypto.prototype.getStoredDevicesForUser = function(userId) {
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs) {
return [];
}
var res = [];
for (var deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
}
}
return res;
};
/**
* List the stored device keys for a user id
*
* @deprecated prefer {@link module:crypto#getStoredDevicesForUser}
*
* @param {string} userId the user to list keys for.
*
* @return {object[]} list of devices with "id", "verified", "blocked",
* "key", and "display_name" parameters.
*/
Crypto.prototype.listDeviceKeys = function(userId) {
var devices = this.getStoredDevicesForUser(userId);
var result = [];
for (var i = 0; i < devices.length; ++i) {
var device = devices[i];
var ed25519Key = device.getFingerprint();
if (ed25519Key) {
result.push({
id: device.deviceId,
key: ed25519Key,
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
display_name: device.getDisplayname(),
});
}
}
// sort by deviceid
result.sort(function(a, b) {
if (a.deviceId < b.deviceId) { return -1; }
if (a.deviceId > b.deviceId) { return 1; }
return 0;
});
return result;
};
/**
* Find a device by curve25519 identity key
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} sender_key curve25519 key to match
*
* @return {module:crypto-deviceinfo?}
*/
Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key) {
if (algorithm !== olmlib.OLM_ALGORITHM) {
// we only deal in olm keys
return null;
}
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devices) {
return null;
}
for (var deviceId in devices) {
if (!devices.hasOwnProperty(deviceId)) {
continue;
}
var device = devices[deviceId];
for (var keyId in device.keys) {
if (!device.keys.hasOwnProperty(keyId)) {
continue;
}
if (keyId.indexOf("curve25519:") !== 0) {
continue;
}
var deviceKey = device.keys[keyId];
if (deviceKey == sender_key) {
return DeviceInfo.fromStorage(device, deviceId);
}
}
}
// doesn't match a known device
return null;
};
/**
* Update the blocked/verified state of the given device
*
* @param {string} userId owner of the device
* @param {string} deviceId unique identifier for the device
*
* @param {?boolean} verified whether to mark the device as verified. Null to
* leave unchanged.
*
* @param {?boolean} blocked whether to mark the device as blocked. Null to
* leave unchanged.
*/
Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, blocked) {
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devices || !devices[deviceId]) {
throw new Error("Unknown device " + userId + ":" + deviceId);
}
var dev = devices[deviceId];
var verificationStatus = dev.verified;
if (verified) {
verificationStatus = DeviceVerification.VERIFIED;
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
verificationStatus = DeviceVerification.UNVERIFIED;
}
if (blocked) {
verificationStatus = DeviceVerification.BLOCKED;
} else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) {
verificationStatus = DeviceVerification.UNVERIFIED;
}
if (dev.verified === verificationStatus) {
return;
}
dev.verified = verificationStatus;
this._sessionStore.storeEndToEndDevicesForUser(userId, devices);
};
/**
* Identify a device by curve25519 identity key and determine its verification state
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} sender_key curve25519 key to match
*
* @return {boolean} true if the device is verified
*/
Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
var device = this.getDeviceByIdentityKey(userId, algorithm, sender_key);
if (!device) {
return false;
}
return device.verified == DeviceVerification.VERIFIED;
};
/**
* Configure a room to use encryption (ie, save a flag in the sessionstore).
*
* @param {string} roomId The room ID to enable encryption in.
* @param {object} config The encryption config for the room.
* @param {string[]} roomMembers userIds of room members to start sessions with
*
* @return {module:client.Promise} A promise that will resolve when encryption is setup.
*/
Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
// if we already have encryption in this room, we should ignore this event
// (for now at least. maybe we should alert the user somehow?)
var existingConfig = this._sessionStore.getEndToEndRoom(roomId);
if (existingConfig) {
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
console.error("Ignoring m.room.encryption event which requests " +
"a change of config in " + roomId);
return q();
}
}
var AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
if (!AlgClass) {
throw new Error("Unable to encrypt with " + config.algorithm);
}
// remove spurious keys
config = {
algorithm: config.algorithm,
};
this._sessionStore.storeEndToEndRoom(roomId, config);
var alg = new AlgClass({
deviceId: this._deviceId,
crypto: this,
olmDevice: this._olmDevice,
baseApis: this._baseApis,
roomId: roomId,
});
this._roomAlgorithms[roomId] = alg;
return alg.initRoomEncryption(roomMembers);
};
/**
* @typedef {Object} module:crypto~OlmSessionResult
* @property {module:crypto-deviceinfo} device device info
* @property {string?} sessionId base64 olm session id; null if no session
* could be established
*/
/**
* Try to make sure we have established olm sessions for the given users.
*
* @param {string[]} users list of user ids
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
var devicesWithoutSession = [
// [userId, deviceId, deviceInfo], ...
];
var result = {};
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
result[userId] = {};
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
var key = deviceInfo.getIdentityKey();
if (key == this._olmDevice.deviceCurve25519Key) {
// don't bother setting up session to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
var sessionId = this._olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId, deviceInfo]);
}
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 self = this;
return this._baseApis.claimOneTimeKeys(
devicesWithoutSession
).then(function(res) {
for (var i = 0; i < devicesWithoutSession.length; ++i) {
var device = devicesWithoutSession[i];
var userId = device[0];
var deviceId = device[1];
var deviceInfo = device[2];
var userRes = res.one_time_keys[userId] || {};
var deviceRes = userRes[deviceId];
var oneTimeKey;
for (var keyId in deviceRes) {
if (keyId.indexOf("curve25519:") === 0) {
oneTimeKey = deviceRes[keyId];
}
}
if (oneTimeKey) {
var sid = self._olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey
);
console.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
result[userId][deviceId].sessionId = sid;
} else {
console.warn("No one-time keys for device " +
userId + ":" + deviceId);
}
}
return result;
});
};
/**
* Whether encryption is enabled for a room.
* @param {string} roomId the room id to query.
* @return {bool} whether encryption is enabled.
*/
Crypto.prototype.isRoomEncrypted = function(roomId) {
return Boolean(this._roomAlgorithms[roomId]);
};
/**
* Encrypt an event according to the configuration of the room, if necessary.
*
* @param {module:models/event.MatrixEvent} event event to be sent
*
* @param {module:models/room?} room destination room. Null if the destination
* is not a room we have seen over the sync pipe.
*
* @return {module:client.Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
Crypto.prototype.encryptEventIfNeeded = function(event, room) {
if (event.isEncrypted()) {
// this event has already been encrypted; this happens if the
// encryption step succeeded, but the send step failed on the first
// attempt.
return null;
}
if (event.getType() !== "m.room.message") {
// we only encrypt m.room.message
return null;
}
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
}
var roomId = event.getRoomId();
var alg = this._roomAlgorithms[roomId];
if (!alg) {
// not encrypting messages in this room
// check that the HS hasn't hidden the crypto event
if (this._sessionStore.getEndToEndRoom(roomId)) {
throw new Error(
"Room was previously configured to use encryption, but is " +
"no longer. Perhaps the homeserver is hiding the " +
"configuration event."
);
}
return null;
}
return alg.encryptMessage(
room, event.getType(), event.getContent()
).then(function(encryptedContent) {
event.makeEncrypted("m.room.encrypted", encryptedContent);
});
};
/**
* Decrypt a received event
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @raises {algorithms.DecryptionError} if there is a problem decrypting the event
*/
Crypto.prototype.decryptEvent = function(event) {
var content = event.content;
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError("Unable to decrypt " + content.algorithm);
}
var alg = new AlgClass({
olmDevice: this._olmDevice,
});
return alg.decryptEvent(event);
};
/**
* Handle a key event
*
* @param {module:modules/event~MatrixEvent} event key event
*/
Crypto.prototype.onRoomKeyEvent = function(event) {
var content = event.getContent();
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError(
"Unable to handle keys for " + content.algorithm
);
}
var alg = new AlgClass({
olmDevice: this._olmDevice,
});
alg.onRoomKeyEvent(event);
};
/**
* @see module:crypto-algorithms/base.DecryptionError
*/
Crypto.DecryptionError = algorithms.DecryptionError;
/** */
module.exports = Crypto;
-100
View File
@@ -1,100 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter
*/
/**
* @param {Object} obj
* @param {string} keyNesting
* @param {*} val
*/
function setProp(obj, keyNesting, val) {
var nestedKeys = keyNesting.split(".");
var currentObj = obj;
for (var i = 0; i < (nestedKeys.length - 1); i++) {
if (!currentObj[nestedKeys[i]]) {
currentObj[nestedKeys[i]] = {};
}
currentObj = currentObj[nestedKeys[i]];
}
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
}
/**
* Construct a new Filter.
* @constructor
* @param {string} userId The user ID for this filter.
* @param {string=} filterId The filter ID if known.
* @prop {string} userId The user ID of the filter
* @prop {?string} filterId The filter ID
*/
function Filter(userId, filterId) {
this.userId = userId;
this.filterId = filterId;
this.definition = {};
}
/**
* Get the JSON body of the filter.
* @return {Object} The filter definition
*/
Filter.prototype.getDefinition = function() {
return this.definition;
};
/**
* Set the JSON body of the filter
* @param {Object} definition The filter definition
*/
Filter.prototype.setDefinition = function(definition) {
this.definition = definition;
};
/**
* Set the max number of events to return for each room's timeline.
* @param {Number} limit The max number of events to return for each room.
*/
Filter.prototype.setTimelineLimit = function(limit) {
setProp(this.definition, "room.timeline.limit", limit);
};
/**
* Control whether left rooms should be included in responses.
* @param {boolean} includeLeave True to make rooms the user has left appear
* in responses.
*/
Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
setProp(this.definition, "room.include_leave", includeLeave);
};
/**
* Create a filter from existing data.
* @static
* @param {string} userId
* @param {string} filterId
* @param {Object} jsonObj
* @return {Filter}
*/
Filter.fromJson = function(userId, filterId, jsonObj) {
var filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;
};
/** The Filter class */
module.exports = Filter;
-573
View File
@@ -1,573 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
var q = require("q");
var utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
var callbacks = require("./realtime-callbacks");
/*
TODO:
- CS: complete register function (doing stages)
- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
*/
/**
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
module.exports.PREFIX_R0 = "/_matrix/client/r0";
/**
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the media repo API
*/
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
/**
* Construct a MatrixHttpApi.
* @constructor
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
* @param {Object} opts The options to use for this HTTP API.
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
* 'http://localhost:8008'.
* @param {Function} opts.request Required. The function to call for HTTP
* requests. This function must look like function(opts, callback){ ... }.
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
* @param {bool} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return status
* codes and headers in addition to data. Default: false.
* @param {string} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
* requests.
*/
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = opts.onlyData || false;
this.event_emitter = event_emitter;
this.opts = opts;
this.uploads = [];
};
module.exports.MatrixHttpApi.prototype = {
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
* path and query parameters respectively.
*/
getContentUri: function() {
var params = {
access_token: this.opts.accessToken
};
return {
base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload",
params: params
};
},
/**
* Upload content to the Home Server
* @param {File} file A File object (in a browser) or in Node,
an object with properties:
name: The file's name
stream: A read stream
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
*/
uploadContent: function(file, callback) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var defer = q.defer();
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
// browser-request doesn't support File objects because it deep-copies
// the options using JSON.parse(JSON.stringify(options)). Instead of
// loading the whole file into memory as a string and letting
// browser-request base64 encode and then decode it again, we just
// use XMLHttpRequest directly.
// (browser-request doesn't support progress either, which is also kind
// of important here)
var upload = { loaded: 0, total: 0 };
if (global.XMLHttpRequest) {
var xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
var cb = requestCallback(defer, callback, this.opts.onlyData);
var timeout_fn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(xhr.timeout_timer);
var err;
if (!xhr.responseText) {
err = new Error('No response body.');
err.http_status = xhr.status;
cb(err);
return;
}
var resp = JSON.parse(xhr.responseText);
if (resp.content_uri === undefined) {
err = Error('Bad response');
err.http_status = xhr.status;
cb(err);
return;
}
cb(undefined, xhr, resp.content_uri);
break;
}
};
xhr.upload.addEventListener("progress", function(ev) {
callbacks.clearTimeout(xhr.timeout_timer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(file.name);
xhr.open("POST", url);
if (file.type) {
xhr.setRequestHeader("Content-Type", file.type);
} else {
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
}
xhr.send(file);
} else {
var queryParams = {
filename: file.name,
access_token: this.opts.accessToken
};
upload.request = this.opts.request({
uri: url,
qs: queryParams,
method: "POST",
headers: {"Content-Type": file.type},
body: file.stream
}, requestCallback(defer, callback, this.opts.onlyData));
}
this.uploads.push(upload);
var self = this;
upload.promise = defer.promise.finally(function() {
var uploadsKeys = Object.keys(self.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
if (self.uploads[uploadsKeys[i]].promise === defer.promise) {
self.uploads.splice(uploadsKeys[i], 1);
}
}
});
return upload.promise;
},
cancelUpload: function(promise) {
var uploadsKeys = Object.keys(this.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
var upload = this.uploads[uploadsKeys[i]];
if (upload.promise === promise) {
if (upload.xhr !== undefined) {
upload.xhr.abort();
return true;
} else if (upload.request !== undefined) {
upload.request.abort();
return true;
}
}
}
return false;
},
getCurrentUploads: function() {
return this.uploads;
},
idServerRequest: function(callback, method, path, params, prefix) {
var fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var opts = {
uri: fullUri,
method: method,
withCredentials: false,
json: false,
_matrix_opts: this.opts
};
if (method == 'GET') {
opts.qs = params;
} else {
opts.form = params;
}
var defer = q.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData)
);
// ID server does not always take JSON, so we can't use requests' 'json'
// option as we do with the home server, but it does return JSON, so
// parse it manually
return defer.promise.then(function(response) {
return JSON.parse(response);
});
},
/**
* Perform an authorised request to the homeserver.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) {
if (!queryParams) { queryParams = {}; }
queryParams.access_token = this.opts.accessToken;
var self = this;
var request_promise = this.request(
callback, method, path, queryParams, data, localTimeoutMs
);
request_promise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return request_promise;
},
/**
* Perform a request to the homeserver without any credentials.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data, localTimeoutMs) {
return this.requestWithPrefix(
callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
);
},
/**
* Perform an authorised request to the homeserver with a specific path
* prefix which overrides the default for this call only. Useful for hitting
* different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix, localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
queryParams.access_token = this.opts.accessToken;
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
);
},
/**
* Perform a request to the homeserver without any credentials but with a
* specific path prefix which overrides the default for this call only.
* Useful for hitting different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
);
},
/**
* Perform a request to an arbitrary URL.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} uri The HTTP URI
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestOtherUrl: function(callback, method, uri, queryParams, data,
localTimeoutMs) {
if (!queryParams) {
queryParams = {};
}
return this._request(
callback, method, uri, queryParams, data, localTimeoutMs
);
},
/**
* Form and return a homeserver request URL based on the given path
* params and prefix.
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
var queryString = "";
if (queryParams) {
queryString = "?" + utils.encodeParams(queryParams);
}
return this.opts.baseUrl + prefix + path + queryString;
},
_request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var self = this;
if (!queryParams) {
queryParams = {};
}
if (this.opts.extraParams) {
for (var key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
queryParams[key] = this.opts.extraParams[key];
}
}
var defer = q.defer();
var timeoutId;
var timedOut = false;
var req;
if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs
}));
}, localTimeoutMs);
}
var reqPromise = defer.promise;
try {
req = this.opts.request(
{
uri: uri,
method: method,
withCredentials: false,
qs: queryParams,
body: data,
json: true,
timeout: localTimeoutMs,
_matrix_opts: this.opts
},
function(err, response, body) {
if (localTimeoutMs) {
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
}
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
handlerFn(err, response, body);
}
);
if (req && req.abort) {
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
reqPromise.abort = req.abort.bind(req);
}
}
catch (ex) {
defer.reject(ex);
if (callback) {
callback(ex);
}
}
return reqPromise;
}
};
/*
* Returns a callback that can be invoked by an HTTP request on completion,
* that will either resolve or reject the given defer as well as invoke the
* given userDefinedCallback (if any).
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result code.
*/
var requestCallback = function(defer, userDefinedCallback, onlyData) {
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
if (!err && response.statusCode >= 400) {
err = new module.exports.MatrixError(body);
err.httpStatus = response.statusCode;
}
if (err) {
defer.reject(err);
userDefinedCallback(err);
}
else {
var res = {
code: response.statusCode,
headers: response.headers,
data: body
};
defer.resolve(onlyData ? body : res);
userDefinedCallback(null, onlyData ? body : res);
}
};
};
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
* @constructor
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {integer} httpStatus The numeric HTTP status code given
*/
module.exports.MatrixError = function MatrixError(errorJson) {
errorJson = errorJson || {};
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
};
module.exports.MatrixError.prototype = Object.create(Error.prototype);
/** */
module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
-325
View File
@@ -1,325 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
* the public classes.
* @module models/event
*/
/**
* Enum for event statuses.
* @readonly
* @enum {string}
*/
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The message is being encrypted */
ENCRYPTING: "encrypting",
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
QUEUED: "queued",
/** The event has been sent to the server, but we have not yet received the
* echo. */
SENT: "sent",
/** The event was cancelled before it was successfully sent. */
CANCELLED: "cancelled",
};
/**
* Construct a Matrix Event object
* @constructor
*
* @param {Object} event The raw event to be wrapped in this DAO
*
* @param {Object=} clearEvent For encrypted events, the plaintext payload for
* the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
* this property</b> directly unless you absolutely have to. Prefer the getter
* methods defined on this class. Using the getter methods shields your app
* from changes to event JSON between Matrix versions.
*
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
* this is a presence event.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
* the invitee, the person being banned, etc.
* @prop {EventStatus} status The sending status of the event.
* @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.forwardLooking = true;
this._clearEvent = clearEvent || {};
};
module.exports.MatrixEvent.prototype = {
/**
* Get the event_id for this event.
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
* </code>
*/
getId: function() {
return this.event.event_id;
},
/**
* Get the user_id for this event.
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
*/
getSender: function() {
return this.event.sender || this.event.user_id; // v2 / v1
},
/**
* Get the (decrypted, if necessary) type of event.
*
* @return {string} The event type, e.g. <code>m.room.message</code>
*/
getType: function() {
return this._clearEvent.type || this.event.type;
},
/**
* Get the (possibly encrypted) type of the event that will be sent to the
* homeserver.
*
* @return {string} The event type.
*/
getWireType: function() {
return this.event.type;
},
/**
* Get the room_id for this event. This will return <code>undefined</code>
* for <code>m.presence</code> events.
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
* </code>
*/
getRoomId: function() {
return this.event.room_id;
},
/**
* Get the timestamp of this event.
* @return {Number} The event timestamp, e.g. <code>1433502692297</code>
*/
getTs: function() {
return this.event.origin_server_ts;
},
/**
* Get the (decrypted, if necessary) event content JSON.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the (possibly encrypted) event content JSON that will be sent to the
* homeserver.
*
* @return {Object} The event content JSON, or an empty object.
*/
getWireContent: function() {
return this.event.content || {};
},
/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.
* @return {Object} The previous event content JSON, or an empty object.
*/
getPrevContent: function() {
// v2 then v1 then default
return this.getUnsigned().prev_content || this.event.prev_content || {};
},
/**
* Get either 'content' or 'prev_content' depending on if this event is
* 'forward-looking' or not. This can be modified via event.forwardLooking.
* In practice, this means we get the chronologically earlier content value
* for this event (this method should surely be called getEarlierContent)
* <strong>This method is experimental and may change.</strong>
* @return {Object} event.content if this event is forward-looking, else
* event.prev_content.
*/
getDirectionalContent: function() {
return this.forwardLooking ? this.getContent() : this.getPrevContent();
},
/**
* Get the age of this event. This represents the age of the event when the
* event arrived at the device, and not the age of the event when this
* function was called.
* @return {Number} The age of this event in milliseconds.
*/
getAge: function() {
return this.getUnsigned().age || this.event.age; // v2 / v1
},
/**
* Get the event state_key if it has one. This will return <code>undefined
* </code> for message events.
* @return {string} The event's <code>state_key</code>.
*/
getStateKey: function() {
return this.event.state_key;
},
/**
* Check if this event is a state event.
* @return {boolean} True if this is a state event.
*/
isState: function() {
return this.event.state_key !== undefined;
},
/**
* Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications).
*
* @internal
*
* @param {string} crypto_type type of the encrypted event - typically
* <tt>"m.room.encrypted"</tt>
*
* @param {object} crypto_content raw 'content' for the encrypted event.
*/
makeEncrypted: function(crypto_type, crypto_content) {
// keep the plain-text data for 'view source'
this._clearEvent = {
type: this.event.type,
content: this.event.content,
};
this.event.type = crypto_type;
this.event.content = crypto_content;
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return Boolean(this._clearEvent.type);
},
getSenderKey: function() {
if (!this.isEncrypted()) {
return null;
}
var c = this.getWireContent();
return c.sender_key;
},
getUnsigned: function() {
return this.event.unsigned || {};
},
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
*
* @param {Object} the raw event causing the redaction
*/
makeRedacted: function(redaction_event) {
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redaction_event;
var key;
for (key in this.event) {
if (!this.event.hasOwnProperty(key)) { continue; }
if (!_REDACT_KEEP_KEY_MAP[key]) {
delete this.event[key];
}
}
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
var content = this.getContent();
for (key in content) {
if (!content.hasOwnProperty(key)) { continue; }
if (!keeps[key]) {
delete content[key];
}
}
},
/**
* Check if this event has been redacted
*
* @return {boolean} True if this event has been redacted
*/
isRedacted: function() {
return Boolean(this.getUnsigned().redacted_because);
},
};
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
*
* the server should strip off any keys not in the following list:
* event_id
* type
* room_id
* user_id
* state_key
* prev_state
* content
* [we keep 'unsigned' as well, since that is created by the local server]
*
* The content object should also be stripped of all keys, unless it is one of
* one of the following event types:
* m.room.member allows key membership
* m.room.create allows key creator
* m.room.join_rules allows key join_rule
* m.room.power_levels allows keys ban, events, events_default, kick,
* redact, state_default, users, users_default.
* m.room.aliases allows key aliases
*/
// a map giving the keys we keep when an event is redacted
var _REDACT_KEEP_KEY_MAP = [
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
'content', 'unsigned',
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});
// a map from event type to the .content keys we keep when an event is redacted
var _REDACT_KEEP_CONTENT_MAP = {
'm.room.member': {'membership': 1},
'm.room.create': {'creator': 1},
'm.room.join_rules': {'join_rule': 1},
'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
'kick': 1, 'redact': 1, 'state_default': 1,
'users': 1, 'users_default': 1,
},
'm.room.aliases': {'aliases': 1},
};
-78
View File
@@ -1,78 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
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 a list of devices
*
* @param {string} ourDeviceId
* @param {module:OlmDevice} olmDevice olm.js wrapper
* @param {string[]} participantKeys list of curve25519 keys to encrypt for
* @param {object} payloadFields fields to include in the encrypted payload
*
* @return {object} content for an m.room.encrypted event
*/
module.exports.encryptMessageForDevices = function(
ourDeviceId, olmDevice, participantKeys, payloadFields
) {
participantKeys.sort();
var participantHash = ""; // Olm.sha256(participantKeys.join());
var payloadJson = {
fingerprint: participantHash,
sender_device: ourDeviceId,
};
utils.extend(payloadJson, payloadFields);
var ciphertext = {};
var payloadString = JSON.stringify(payloadJson);
for (var i = 0; i < participantKeys.length; ++i) {
var deviceKey = participantKeys[i];
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.
continue;
}
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
ciphertext[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, payloadString
);
}
var encryptedContent = {
algorithm: module.exports.OLM_ALGORITHM,
sender_key: olmDevice.deviceCurve25519Key,
ciphertext: ciphertext
};
return encryptedContent;
};
-686
View File
@@ -1,686 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. Implementation details:
* <pre>
* Room data is stored as follows:
* room_$ROOMID_timeline_$INDEX : [ Event, Event, Event ]
* room_$ROOMID_state : {
* pagination_token: <oldState.paginationToken>,
* events: {
* <event_type>: { <state_key> : {JSON} }
* }
* }
* User data is stored as follows:
* user_$USERID : User
* Sync token:
* sync_token : $TOKEN
*
* Room Retrieval
* --------------
* Retrieving a room requires the $ROOMID which then pulls out the current state
* from room_$ROOMID_state. A defined starting batch of timeline events are then
* extracted from the highest numbered $INDEX for room_$ROOMID_timeline_$INDEX
* (more indices as required). The $INDEX may be negative. These are
* added to the timeline in the same way as /initialSync (old state will diverge).
* If there exists a room_$ROOMID_timeline_live key, then a timeline sync should
* be performed before retrieving.
*
* Retrieval of earlier messages
* -----------------------------
* The earliest event the Room instance knows about is E. Retrieving earlier
* messages requires a Room which has a storageToken defined.
* This token maps to the index I where the Room is at. Events are then retrieved from
* room_$ROOMID_timeline_{I} and elements before E are extracted. If the limit
* demands more events, I-1 is retrieved, up until I=min $INDEX where it gives
* less than the limit. Index may go negative if you have paginated in the past.
*
* Full Insertion
* --------------
* Storing a room requires the timeline and state keys for $ROOMID to
* be blown away and completely replaced, which is computationally expensive.
* Room.timeline is batched according to the given batch size B. These batches
* are then inserted into storage as room_$ROOMID_timeline_$INDEX. Finally,
* the current room state is persisted to room_$ROOMID_state.
*
* Incremental Insertion
* ---------------------
* As events arrive, the store can quickly persist these new events. This
* involves pushing the events to room_$ROOMID_timeline_live. If the
* current room state has been modified by the new event, then
* room_$ROOMID_state should be updated in addition to the timeline.
*
* Timeline sync
* -------------
* Retrieval of events from the timeline depends on the proper batching of
* events. This is computationally expensive to perform on every new event, so
* is deferred by inserting live events to room_$ROOMID_timeline_live. A
* timeline sync reconciles timeline_live and timeline_$INDEX. This involves
* retrieving _live and the highest numbered $INDEX batch. If the batch is < B,
* the earliest entries from _live are inserted into the $INDEX until the
* batch == B. Then, the remaining entries in _live are batched to $INDEX+1,
* $INDEX+2, and so on. The easiest way to visualise this is that the timeline
* goes from old to new, left to right:
* -2 -1 0 1
* <--OLD---------------------------------------NEW-->
* [a,b,c] [d,e,f] [g,h,i] [j,k,l]
*
* Purging
* -------
* Events from the timeline can be purged by removing the lowest
* timeline_$INDEX in the store.
*
* Example
* -------
* A room with room_id !foo:bar has 9 messages (M1->9 where 9=newest) with a
* batch size of 4. The very first time, there is no entry for !foo:bar until
* storeRoom() is called, which results in the keys: [Full Insert]
* room_!foo:bar_timeline_0 : [M1, M2, M3, M4]
* room_!foo:bar_timeline_1 : [M5, M6, M7, M8]
* room_!foo:bar_timeline_2 : [M9]
* room_!foo:bar_state: { ... }
*
* 5 new messages (N1-5, 5=newest) arrive and are then added: [Incremental Insert]
* room_!foo:bar_timeline_live: [N1]
* room_!foo:bar_timeline_live: [N1, N2]
* room_!foo:bar_timeline_live: [N1, N2, N3]
* room_!foo:bar_timeline_live: [N1, N2, N3, N4]
* room_!foo:bar_timeline_live: [N1, N2, N3, N4, N5]
*
* App is shutdown. Restarts. The timeline is synced [Timeline Sync]
* room_!foo:bar_timeline_2 : [M9, N1, N2, N3]
* room_!foo:bar_timeline_3 : [N4, N5]
* room_!foo:bar_timeline_live: []
*
* And the room is retrieved with 8 messages: [Room Retrieval]
* Room.timeline: [M7, M8, M9, N1, N2, N3, N4, N5]
* Room.storageToken: => early_index = 1 because that's where M7 is.
*
* 3 earlier messages are requested: [Earlier retrieval]
* Use storageToken to find batch index 1. Scan batch for earliest event ID.
* earliest event = M7
* events = room_!foo:bar_timeline_1 where event < M7 = [M5, M6]
* Too few events, use next index (0) and get 1 more:
* events = room_!foo:bar_timeline_0 = [M1, M2, M3, M4] => [M4]
* Return concatentation:
* [M4, M5, M6]
*
* Purge oldest events: [Purge]
* del room_!foo:bar_timeline_0
* </pre>
* @module store/webstorage
*/
var DEBUG = false; // set true to enable console logging.
var utils = require("../utils");
var Room = require("../models/room");
var User = require("../models/user");
var MatrixEvent = require("../models/event").MatrixEvent;
/**
* Construct a web storage store, capable of storing rooms and users.
* @constructor
* @param {WebStorage} webStore A web storage implementation, e.g.
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
* @param {integer} batchSize The number of events to store per key/value (room
* scoped). Use -1 to store all events for a room under one key/value.
* @throws if the supplied 'store' does not meet the Storage interface of the
* WebStorage API.
*/
function WebStorageStore(webStore, batchSize) {
this.store = webStore;
this.batchSize = batchSize;
if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key)) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface"
);
}
if (!parseInt(webStore.length) && webStore.length !== 0) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface (length)"
);
}
// cached list of room_ids this is storing.
this._roomIds = [];
this._syncedWithStore = false;
// tokens used to remember which index the room instance is at.
this._tokens = [
// { earliestIndex: -4 }
];
}
/**
* Retrieve the token to stream from.
* @return {string} The token or null.
*/
WebStorageStore.prototype.getSyncToken = function() {
return this.store.getItem("sync_token");
};
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
*/
WebStorageStore.prototype.setSyncToken = function(token) {
this.store.setItem("sync_token", token);
};
/**
* Store a room in web storage.
* @param {Room} room
*/
WebStorageStore.prototype.storeRoom = function(room) {
var serRoom = SerialisedRoom.fromRoom(room, this.batchSize);
persist(this.store, serRoom);
if (this._roomIds.indexOf(room.roomId) === -1) {
this._roomIds.push(room.roomId);
}
};
/**
* Retrieve a room from web storage.
* @param {string} roomId
* @return {?Room}
*/
WebStorageStore.prototype.getRoom = function(roomId) {
// probe if room exists; break early if not. Every room should have state.
if (!getItem(this.store, keyName(roomId, "state"))) {
debuglog("getRoom: No room with id %s found.", roomId);
return null;
}
var timelineKeys = getTimelineIndices(this.store, roomId);
if (timelineKeys.indexOf("live") !== -1) {
debuglog("getRoom: Live events found. Syncing timeline for %s", roomId);
this._syncTimeline(roomId, timelineKeys);
}
return loadRoom(this.store, roomId, this.batchSize, this._tokens);
};
/**
* Get a list of all rooms from web storage.
* @return {Array} An empty array.
*/
WebStorageStore.prototype.getRooms = function() {
var rooms = [];
var i;
if (!this._syncedWithStore) {
// sync with the store to set this._roomIds correctly. We know there is
// exactly one 'state' key for each room, so we grab them.
this._roomIds = [];
for (i = 0; i < this.store.length; i++) {
if (this.store.key(i).indexOf("room_") === 0 &&
this.store.key(i).indexOf("_state") !== -1) {
// grab the middle bit which is the room ID
var k = this.store.key(i);
this._roomIds.push(
k.substring("room_".length, k.length - "_state".length)
);
}
}
this._syncedWithStore = true;
}
// call getRoom on each room_id
for (i = 0; i < this._roomIds.length; i++) {
var rm = this.getRoom(this._roomIds[i]);
if (rm) {
rooms.push(rm);
}
}
return rooms;
};
/**
* Get a list of summaries from web storage.
* @return {Array} An empty array.
*/
WebStorageStore.prototype.getRoomSummaries = function() {
return [];
};
/**
* Store a user in web storage.
* @param {User} user
*/
WebStorageStore.prototype.storeUser = function(user) {
// persist the events used to make the user, we can reconstruct on demand.
setItem(this.store, "user_" + user.userId, {
presence: user.events.presence ? user.events.presence.event : null
});
};
/**
* Get a user from web storage.
* @param {string} userId
* @return {User}
*/
WebStorageStore.prototype.getUser = function(userId) {
var userData = getItem(this.store, "user_" + userId);
if (!userData) {
return null;
}
var user = new User(userId);
if (userData.presence) {
user.setPresenceEvent(new MatrixEvent(userData.presence));
}
return user;
};
/**
* Retrieve scrollback for this room. Automatically adds events to the timeline.
* @param {Room} room The matrix room to add the events to the start of the timeline.
* @param {integer} limit The max number of old events to retrieve.
* @return {Array<Object>} An array of objects which will be at most 'limit'
* length and at least 0. The objects are the raw event JSON. The last element
* is the 'oldest' (for parity with homeserver scrollback APIs).
*/
WebStorageStore.prototype.scrollback = function(room, limit) {
if (room.storageToken === undefined || room.storageToken >= this._tokens.length) {
return [];
}
// find the index of the earliest event in this room's timeline
var storeData = this._tokens[room.storageToken] || {};
var i;
var earliestIndex = storeData.earliestIndex;
var earliestEventId = room.timeline[0] ? room.timeline[0].getId() : null;
debuglog(
"scrollback in %s (timeline=%s msgs) i=%s, timeline[0].id=%s - req %s events",
room.roomId, room.timeline.length, earliestIndex, earliestEventId, limit
);
var batch = getItem(
this.store, keyName(room.roomId, "timeline", earliestIndex)
);
if (!batch) {
// bad room or already at start, either way we have nothing to give.
debuglog("No batch with index %s found.", earliestIndex);
return [];
}
// populate from this batch first
var scrollback = [];
var foundEventId = false;
for (i = batch.length - 1; i >= 0; i--) {
// go back and find the earliest event ID, THEN start adding entries.
// Make a MatrixEvent so we don't assume .event_id exists
// (e.g v2/v3 JSON may be different)
var matrixEvent = new MatrixEvent(batch[i]);
if (matrixEvent.getId() === earliestEventId) {
foundEventId = true;
debuglog(
"Found timeline[0] event at position %s in batch %s",
i, earliestIndex
);
continue;
}
if (!foundEventId) {
continue;
}
// add entry
debuglog("Add event at position %s in batch %s", i, earliestIndex);
scrollback.push(batch[i]);
if (scrollback.length === limit) {
break;
}
}
if (scrollback.length === limit) {
debuglog("Batch has enough events to satisfy request.");
return scrollback;
}
if (!foundEventId) {
// the earliest index batch didn't contain the event. In other words,
// this timeline is at a state we don't know, so bail.
debuglog(
"Failed to find event ID %s in batch %s", earliestEventId, earliestIndex
);
return [];
}
// get the requested earlier events from earlier batches
while (scrollback.length < limit) {
earliestIndex--;
batch = getItem(
this.store, keyName(room.roomId, "timeline", earliestIndex)
);
if (!batch) {
// no more events
debuglog("No batch found at index %s", earliestIndex);
break;
}
for (i = batch.length - 1; i >= 0; i--) {
debuglog("Add event at position %s in batch %s", i, earliestIndex);
scrollback.push(batch[i]);
if (scrollback.length === limit) {
break;
}
}
}
debuglog(
"Out of %s requested events, returning %s. New index=%s",
limit, scrollback.length, earliestIndex
);
room.addEventsToTimeline(utils.map(scrollback, function(e) {
return new MatrixEvent(e);
}), true, room.getLiveTimeline());
this._tokens[room.storageToken] = {
earliestIndex: earliestIndex
};
return scrollback;
};
/**
* Store events for a room. The events have already been added to the timeline.
* @param {Room} room The room to store events for.
* @param {Array<MatrixEvent>} events The events to store.
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results. The last element
* is the 'oldest' (for parity with homeserver scrollback APIs).
*/
WebStorageStore.prototype.storeEvents = function(room, events, token, toStart) {
if (toStart) {
// add paginated events to lowest batch indexes (can go -ve)
var lowIndex = getIndexExtremity(
getTimelineIndices(this.store, room.roomId), true
);
var i, key, batch;
for (i = 0; i < events.length; i++) { // loop events to be stored
key = keyName(room.roomId, "timeline", lowIndex);
batch = getItem(this.store, key) || [];
while (batch.length < this.batchSize && i < events.length) {
batch.unshift(events[i].event);
i++; // increment to insert next event into this batch
}
i--; // decrement to avoid skipping one (for loop ++s)
setItem(this.store, key, batch);
lowIndex--; // decrement index to get a new batch.
}
}
else {
// dump as live events
var liveEvents = getItem(
this.store, keyName(room.roomId, "timeline", "live")
) || [];
debuglog(
"Adding %s events to %s live list (which has %s already)",
events.length, room.roomId, liveEvents.length
);
var updateState = false;
liveEvents = liveEvents.concat(utils.map(events, function(me) {
// cheeky check to avoid looping twice
if (me.isState()) {
updateState = true;
}
return me.event;
}));
setItem(
this.store, keyName(room.roomId, "timeline", "live"), liveEvents
);
if (updateState) {
debuglog("Storing state for %s as new events updated state", room.roomId);
// use 0 batch size; we don't care about batching right now.
var serRoom = SerialisedRoom.fromRoom(room, 0);
setItem(this.store, keyName(serRoom.roomId, "state"), serRoom.state);
}
}
};
/**
* Sync the 'live' timeline, batching live events according to 'batchSize'.
* @param {string} roomId The room to sync the timeline.
* @param {Array<String>} timelineIndices Optional. The indices in the timeline
* if known already.
*/
WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
timelineIndices = timelineIndices || getTimelineIndices(this.store, roomId);
var liveEvents = getItem(this.store, keyName(roomId, "timeline", "live")) || [];
// get the highest numbered $INDEX batch
var highestIndex = getIndexExtremity(timelineIndices);
var hiKey = keyName(roomId, "timeline", highestIndex);
var hiBatch = getItem(this.store, hiKey) || [];
// fill up the existing batch first.
while (hiBatch.length < this.batchSize && liveEvents.length > 0) {
hiBatch.push(liveEvents.shift());
}
setItem(this.store, hiKey, hiBatch);
// start adding new batches as required
var batch = [];
while (liveEvents.length > 0) {
batch.push(liveEvents.shift());
if (batch.length === this.batchSize || liveEvents.length === 0) {
// persist the full batch and make another
highestIndex++;
hiKey = keyName(roomId, "timeline", highestIndex);
setItem(this.store, hiKey, batch);
batch = [];
}
}
// reset live array
setItem(this.store, keyName(roomId, "timeline", "live"), []);
};
/**
* Store a filter.
* @param {Filter} filter
*/
WebStorageStore.prototype.storeFilter = function(filter) {
};
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
WebStorageStore.prototype.getFilter = function(userId, filterId) {
return null;
};
function SerialisedRoom(roomId) {
this.state = {
events: {}
};
this.timeline = {
// $INDEX: []
};
this.roomId = roomId;
}
/**
* Convert a Room instance into a SerialisedRoom instance which can be stored
* in the key value store.
* @param {Room} room The matrix room to convert
* @param {integer} batchSize The number of events per timeline batch
* @return {SerialisedRoom} A serialised room representation of 'room'.
*/
SerialisedRoom.fromRoom = function(room, batchSize) {
var self = new SerialisedRoom(room.roomId);
var index;
self.state.pagination_token = room.oldState.paginationToken;
// [room_$ROOMID_state] downcast to POJO from MatrixEvent
utils.forEach(utils.keys(room.currentState.events), function(eventType) {
utils.forEach(utils.keys(room.currentState.events[eventType]), function(skey) {
if (!self.state.events[eventType]) {
self.state.events[eventType] = {};
}
self.state.events[eventType][skey] = (
room.currentState.events[eventType][skey].event
);
});
});
// [room_$ROOMID_timeline_$INDEX]
if (batchSize > 0) {
index = 0;
while (index * batchSize < room.timeline.length) {
self.timeline[index] = room.timeline.slice(
index * batchSize, (index + 1) * batchSize
);
self.timeline[index] = utils.map(self.timeline[index], function(me) {
// use POJO not MatrixEvent
return me.event;
});
index++;
}
}
else { // don't batch
self.timeline[0] = utils.map(room.timeline, function(matrixEvent) {
return matrixEvent.event;
});
}
return self;
};
function loadRoom(store, roomId, numEvents, tokenArray) {
var room = new Room(roomId, {
storageToken: tokenArray.length
});
// populate state (flatten nested struct to event array)
var currentStateMap = getItem(store, keyName(roomId, "state"));
var stateEvents = [];
utils.forEach(utils.keys(currentStateMap.events), function(eventType) {
utils.forEach(utils.keys(currentStateMap.events[eventType]), function(skey) {
stateEvents.push(currentStateMap.events[eventType][skey]);
});
});
// TODO: Fix logic dupe with MatrixClient._processRoomEvents
var oldStateEvents = utils.map(
utils.deepCopy(stateEvents), function(e) {
return new MatrixEvent(e);
}
);
var currentStateEvents = utils.map(stateEvents, function(e) {
return new MatrixEvent(e);
}
);
room.oldState.setStateEvents(oldStateEvents);
room.currentState.setStateEvents(currentStateEvents);
// add most recent numEvents
var recentEvents = [];
var index = getIndexExtremity(getTimelineIndices(store, roomId));
var eventIndex = index;
var i, key, batch;
while (recentEvents.length < numEvents) {
key = keyName(roomId, "timeline", index);
batch = getItem(store, key) || [];
if (batch.length === 0) {
// nothing left in the store.
break;
}
for (i = batch.length - 1; i >= 0; i--) {
recentEvents.unshift(new MatrixEvent(batch[i]));
if (recentEvents.length === numEvents) {
eventIndex = index;
break;
}
}
index--;
}
// add events backwards to diverge old state correctly.
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
room.oldState.paginationToken = currentStateMap.pagination_token;
// set the token data to let us know which index this room instance is at
// for scrollback.
tokenArray.push({
earliestIndex: eventIndex
});
return room;
}
function persist(store, serRoom) {
setItem(store, keyName(serRoom.roomId, "state"), serRoom.state);
utils.forEach(utils.keys(serRoom.timeline), function(index) {
setItem(store,
keyName(serRoom.roomId, "timeline", index),
serRoom.timeline[index]
);
});
}
function getTimelineIndices(store, roomId) {
var keys = [];
for (var i = 0; i < store.length; i++) {
if (store.key(i).indexOf(keyName(roomId, "timeline_")) !== -1) {
// e.g. room_$ROOMID_timeline_0 => 0
keys.push(
store.key(i).replace(keyName(roomId, "timeline_"), "")
);
}
}
return keys;
}
function getIndexExtremity(timelineIndices, getLowest) {
var extremity, index;
for (var i = 0; i < timelineIndices.length; i++) {
index = parseInt(timelineIndices[i]);
if (!isNaN(index) && (
extremity === undefined ||
!getLowest && index > extremity ||
getLowest && index < extremity)) {
extremity = index;
}
}
return extremity;
}
function keyName(roomId, key, index) {
return "room_" + roomId + "_" + key + (
index === undefined ? "" : ("_" + index)
);
}
function getItem(store, key) {
try {
return JSON.parse(store.getItem(key));
}
catch (e) {
debuglog("Failed to get key %s: %s", key, e);
debuglog(e.stack);
}
return null;
}
function setItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
}
}
/*
function delRoomStruct(store, roomId) {
var prefix = "room_" + roomId;
var keysToRemove = [];
for (var i = 0; i < store.length; i++) {
if (store.key(i).indexOf(prefix) !== -1) {
keysToRemove.push(store.key(i));
}
}
utils.forEach(keysToRemove, function(key) {
store.removeItem(key);
});
} */
/** Web Storage Store class. */
module.exports = WebStorageStore;
+62 -20
View File
@@ -1,18 +1,21 @@
{
"name": "matrix-js-sdk",
"version": "0.5.6",
"version": "0.8.3",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
"prepublish": "git rev-parse HEAD > git-revision.txt",
"version": "npm run release && git add dist/$npm_package_version"
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "babel -s -w -d lib src",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 109 src spec",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -23,22 +26,61 @@
"browser": "browser-index.js",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
".babelrc",
".eslintrc.js",
"spec/.eslintrc.js",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"RELEASING.md",
"examples",
"git-hooks",
"git-revision.txt",
"index.js",
"browser-index.js",
"jenkins.sh",
"lib",
"package.json",
"release.sh",
"spec",
"src"
],
"dependencies": {
"another-json": "^0.2.0",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"browserify": "^10.2.3",
"q": "^1.4.1",
"content-type": "^1.0.2",
"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"
"babel-cli": "^6.18.0",
"babel-eslint": "^7.1.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"browserify": "^14.0.0",
"browserify-shim": "^3.8.13",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.0",
"mocha": "^3.2.0",
"mocha-jenkins-reporter": "^0.3.6",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"uglify-js": "^2.8.26",
"watchify": "^3.2.1"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-1.0.0.tgz"
"browserify": {
"transform": [
"sourceify"
]
}
}
+189 -13
View File
@@ -1,13 +1,18 @@
#!/bin/sh
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk. Performs the steps documented
# in RELEASING.md
# Script to perform a release of matrix-js-sdk.
#
# Requires github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# Requires:
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (OSX) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
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,23 @@ $USAGE
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit
fi
if ! git diff-files --quiet; then
echo "this git checkout has uncommitted changes. Refusing to release."
exit
fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
@@ -47,11 +69,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 +99,16 @@ if [ -z "$skip_changelog" ]; then
fi
# we might already be on the release branch, in which case, yay
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
if [ -z "$skip_changelog" ]; then
@@ -76,12 +121,140 @@ if [ -z "$skip_changelog" ]; then
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=`mktemp`
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
set -x
# Bump package.json, build the dist, and tag
# Bump package.json and build the dist
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 it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
fi
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from npm link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
npm install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
if [ -n "$signing_id" ]
then
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
assets="$assets -a $i.asc"
fi
done
fi
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag and the release branch
git push origin "$rel_branch" "$tag"
if [ -n "$signing_id" ]; then
# make a signature for the source tarball.
#
# github will make us a tarball from the tag - we want to create a
# signature for it, which means that first of all we need to check that
# it's correct.
#
# we can't deterministically build exactly the same tarball, due to
# differences in gzip implementation - but we *can* build the same tar - so
# the easiest way to check the validity of the tarball from git is to unzip
# it and compare it with our own idea of what the tar should look like.
# the name of the sig file we want to create
source_sigfile="${tag}-src.tar.gz.asc"
tarfile="$tag.tar.gz"
gh_project_url=$(git remote get-url origin |
sed -e 's#^git@github\.com:#https://github.com/#' \
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
-e 's/\.git$//')
project_name="${gh_project_url##*/}"
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
# unzip it and compare it with the tar we would generate
if ! cmp --silent <(gunzip -c $tarfile) \
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
# we don't bail out here, because really it's more likely that our comparison
# screwed up and it's super annoying to abort the script at this point.
cat >&2 <<EOF
!!!!!!!!!!!!!!!!!
!!!! WARNING !!!!
Mismatch between our own tarfile and that generated by github: not signing
source tarball.
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
attach it to the release as $source_sigfile.
!!!!!!!!!!!!!!!!!
EOF
else
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
assets="$assets -a $source_sigfile"
fi
fi
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
rm "${release_text}"
rm "${latest_changes}"
# publish to npmjs
npm publish
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
@@ -98,21 +271,24 @@ if [ -z "$skip_jsdoc" ]; then
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
fi
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
exit 0
fi
# merge release branch to master
echo "updating master branch"
git checkout master
git pull
git merge --ff-only "$rel_branch"
# push everything to github
git push origin master "$rel_branch" "$tag"
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# publish to npmjs
npm publish
# finally, merge master back onto develop
git checkout develop
git pull
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python
"""
Outputs the body of the first entry of changelog file on stdin
"""
import re
import sys
found_first_header = False
for line in sys.stdin:
line = line.strip()
if re.match(r"^Changes in \[.*\]", line):
if found_first_header:
break
found_first_header = True
elif not re.match(r"^=+$", line) and len(line) > 0:
print line
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
env: {
mocha: true,
},
}
+6 -4
View File
@@ -40,14 +40,16 @@ MockStorageApi.prototype = {
return this.keys[index];
},
_recalc: function() {
var keys = [];
for (var k in this.data) {
if (!this.data.hasOwnProperty(k)) { continue; }
const keys = [];
for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) {
continue;
}
keys.push(k);
}
this.keys = keys;
this.length = keys.length;
}
},
};
/** */
+216
View File
@@ -0,0 +1,216 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
// load olm before the sdk if possible
import './olm-loader';
import sdk from '..';
import testUtils from './test-utils';
import MockHttpBackend from 'matrix-mock-request';
import expect from 'expect';
import Promise from 'bluebird';
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
*/
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend,
) {
this.userId = userId;
this.deviceId = deviceId;
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new testUtils.MockStorageApi();
}
this.storage = new sdk.WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://" + userId + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: this.storage,
request: this.httpBackend.requestFn,
});
this.deviceKeys = null;
this.oneTimeKeys = {};
}
TestClient.prototype.toString = function() {
return 'TestClient[' + this.userId + ']';
};
/**
* start the client, and wait for it to initialise.
*
* @return {Promise}
*/
TestClient.prototype.start = function() {
console.log(this + ': starting');
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: 'detached',
});
return Promise.all([
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(this + ': started');
});
};
/**
* stop the client
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
};
/**
* Set up expectations that the client will upload device keys.
*/
TestClient.prototype.expectDeviceKeyUpload = function() {
const self = this;
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
console.log(self + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
};
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
*/
TestClient.prototype.awaitOneTimeKeyUpload = function() {
if (Object.keys(this.oneTimeKeys).length != 0) {
// already got one-time keys
return Promise.resolve(this.oneTimeKeys);
}
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({});
console.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
});
};
/**
* Set up expectations that the client will query device keys.
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
*/
TestClient.prototype.expectKeyQuery = function(response) {
this.httpBackend.when('POST', '/keys/query').respond(
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual({});
});
return response;
});
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* flush a single /sync request, and wait for the syncing event
*
* @returns {Promise} promise which completes once the sync has been flushed
*/
TestClient.prototype.flushSync = function() {
console.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(`${this}: flushSync completed`);
});
};
+369
View File
@@ -0,0 +1,369 @@
import expect from 'expect';
import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
const ROOM_ID = "!room:id";
/**
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @param {string[]} roomMembers
*
* @return {object} sync response
*/
function getSyncResponse(roomMembers) {
const stateEvents = [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
];
Array.prototype.push.apply(
stateEvents,
roomMembers.map(
(m) => testUtils.mkMembership({
mship: 'join',
sender: m,
}),
),
);
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
state: {
events: stateEvents,
},
},
},
},
};
return syncResponse;
}
describe("DeviceList management:", function() {
if (!global.Olm) {
console.warn('not running deviceList tests: Olm not present');
return;
}
let sessionStoreBackend;
let aliceTestClient;
async function createTestClient() {
const testClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
);
await testClient.client.initCrypto();
return testClient;
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi();
aliceTestClient = await createTestClient();
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, {event_id: '$event1'});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
]);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: {'@chris:abc': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
chrisStat);
}
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: {'@bob:xyz': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
bobStat);
}
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
});
});
// https://github.com/vector-im/riot-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function() {
await aliceTestClient.start();
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz']));
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
},
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
);
});
it("when Bob leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
it("when Alice leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
it("when Bob leaves whilst Alice is offline", async function() {
aliceTestClient.stop();
const anotherTestClient = await createTestClient();
try {
anotherTestClient.httpBackend.when('GET', '/keys/changes').respond(
200, {
changed: [],
left: ['@bob:xyz'],
},
);
await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
const bobStat = anotherTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
} finally {
anotherTestClient.stop();
}
});
});
});
File diff suppressed because it is too large Load Diff
+109 -92
View File
@@ -1,22 +1,27 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient events", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
const baseUrl = "http://localhost.or.something";
let client;
let httpBackend;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken
accessToken: selfAccessToken,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
@@ -28,14 +33,14 @@ describe("MatrixClient events", function() {
});
describe("emissions", function() {
var SYNC_DATA = {
const SYNC_DATA = {
next_batch: "s_5_3",
presence: {
events: [
utils.mkPresence({
user: "@foo:bar", name: "Foo Bar", presence: "online"
})
]
user: "@foo:bar", name: "Foo Bar", presence: "online",
}),
],
},
rooms: {
join: {
@@ -43,30 +48,30 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
}),
],
prev_batch: "s"
prev_batch: "s",
},
state: {
events: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
room: "!erufh:bar", mship: "join", user: "@foo:bar",
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar"
}
})
]
}
}
}
}
creator: "@foo:bar",
},
}),
],
},
},
},
},
};
var NEXT_SYNC_DATA = {
const NEXT_SYNC_DATA = {
next_batch: "e_6_7",
rooms: {
join: {
@@ -74,44 +79,45 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
room: "!erufh:bar", user: "@foo:bar",
msg: "ello ello",
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
}),
]
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
}
})
]
}
}
}
}
user_ids: ["@foo:bar"],
},
}),
],
},
},
},
},
};
it("should emit events from both the first and subsequent /sync calls",
function(done) {
function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var expectedEvents = [];
let expectedEvents = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
client.on("event", function(event) {
var found = false;
for (var i = 0; i < expectedEvents.length; i++) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
@@ -119,49 +125,56 @@ describe("MatrixClient events", function() {
}
}
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType()
true, "Unexpected 'event' emitted: " + event.getType(),
);
});
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client).then(() => {
return utils.syncPromise(client);
}),
httpBackend.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls"
0, "Failed to see all events from /sync calls",
);
done();
});
});
it("should emit User events", function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var fired = false;
let fired = false;
client.on("User.presence", function(event, user) {
fired = true;
expect(user).toBeDefined();
expect(event).toBeDefined();
if (!user || !event) { return; }
expect(user).toBeTruthy();
expect(event).toBeTruthy();
if (!user || !event) {
return;
}
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence
SYNC_DATA.presence.events[0].content.presence,
);
});
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
expect(fired).toBe(true, "User.presence didn't fire.");
done();
});
});
it("should emit Room events", function(done) {
it("should emit Room events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomInvokeCount = 0;
var roomNameInvokeCount = 0;
var timelineFireCount = 0;
let roomInvokeCount = 0;
let roomNameInvokeCount = 0;
let timelineFireCount = 0;
client.on("Room", function(room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
@@ -176,35 +189,37 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]).then(function() {
expect(roomInvokeCount).toEqual(
1, "Room fired wrong number of times."
1, "Room fired wrong number of times.",
);
expect(roomNameInvokeCount).toEqual(
1, "Room.name fired wrong number of times."
1, "Room.name fired wrong number of times.",
);
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times"
3, "Room.timeline fired the wrong number of times",
);
done();
});
});
it("should emit RoomState events", function(done) {
it("should emit RoomState events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomStateEventTypes = [
"m.room.member", "m.room.create"
const roomStateEventTypes = [
"m.room.member", "m.room.create",
];
var eventsInvokeCount = 0;
var membersInvokeCount = 0;
var newMemberInvokeCount = 0;
let eventsInvokeCount = 0;
let membersInvokeCount = 0;
let newMemberInvokeCount = 0;
client.on("RoomState.events", function(event, state) {
eventsInvokeCount++;
var index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected room state event type: " + event.getType()
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).toNotEqual(
-1, "Unexpected room state event type: " + event.getType(),
);
if (index >= 0) {
roomStateEventTypes.splice(index, 1);
@@ -225,28 +240,30 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]).then(function() {
expect(membersInvokeCount).toEqual(
1, "RoomState.members fired wrong number of times"
1, "RoomState.members fired wrong number of times",
);
expect(newMemberInvokeCount).toEqual(
1, "RoomState.newMember fired wrong number of times"
1, "RoomState.newMember fired wrong number of times",
);
expect(eventsInvokeCount).toEqual(
2, "RoomState.events fired wrong number of times"
2, "RoomState.events fired wrong number of times",
);
done();
});
});
it("should emit RoomMember events", function(done) {
it("should emit RoomMember events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var typingInvokeCount = 0;
var powerLevelInvokeCount = 0;
var nameInvokeCount = 0;
var membershipInvokeCount = 0;
let typingInvokeCount = 0;
let powerLevelInvokeCount = 0;
let nameInvokeCount = 0;
let membershipInvokeCount = 0;
client.on("RoomMember.name", function(event, member) {
nameInvokeCount++;
});
@@ -264,40 +281,40 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]).then(function() {
expect(typingInvokeCount).toEqual(
1, "RoomMember.typing fired wrong number of times"
1, "RoomMember.typing fired wrong number of times",
);
expect(powerLevelInvokeCount).toEqual(
0, "RoomMember.powerLevel fired wrong number of times"
0, "RoomMember.powerLevel fired wrong number of times",
);
expect(nameInvokeCount).toEqual(
0, "RoomMember.name fired wrong number of times"
0, "RoomMember.name fired wrong number of times",
);
expect(membershipInvokeCount).toEqual(
1, "RoomMember.membership fired wrong number of times"
1, "RoomMember.membership fired wrong number of times",
);
done();
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
var sessionLoggedOutCount = 0;
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(event, member) {
sessionLoggedOutCount++;
});
client.startClient();
httpBackend.flush().done(function() {
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times"
1, "Session.logged_out fired wrong number of times",
);
done();
});
});
});
});
+339 -229
View File
@@ -1,29 +1,30 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventTimeline = sdk.EventTimeline;
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
var baseUrl = "http://localhost.or.something";
var userId = "@alice:localhost";
var userName = "Alice";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
const baseUrl = "http://localhost.or.something";
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
var ROOM_NAME_EVENT = utils.mkEvent({
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
});
var INITIAL_SYNC_DATA = {
const INITIAL_SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
@@ -31,33 +32,33 @@ var INITIAL_SYNC_DATA = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1"
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
creator: userId,
},
}),
],
},
},
},
},
};
var EVENTS = [
const EVENTS = [
utils.mkMessage({
room: roomId, user: userId, msg: "we",
}),
@@ -81,7 +82,7 @@ function startClient(httpBackend, client) {
client.startClient();
// set up a promise which will resolve once the client is initialised
var deferred = q.defer();
const deferred = Promise.defer();
client.on("sync", function(state) {
console.log("sync", state);
if (state != "SYNCING") {
@@ -90,17 +91,18 @@ function startClient(httpBackend, client) {
deferred.resolve();
});
httpBackend.flush();
return deferred.promise;
return Promise.all([
httpBackend.flushAllExpected(),
deferred.promise,
]);
}
describe("getEventTimeline support", function() {
var httpBackend;
var client;
let httpBackend;
let client;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
@@ -118,15 +120,17 @@ describe("getEventTimeline support", function() {
accessToken: accessToken,
});
startClient(httpBackend, client
startClient(httpBackend, client,
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
.toThrow();
}).catch(utils.failTest).done(done);
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
}).nodeify(done);
});
it("timeline support works when enabled", function(done) {
it("timeline support works when enabled", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
@@ -134,14 +138,13 @@ describe("getEventTimeline support", function() {
timelineSupport: true,
});
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
.not.toThrow();
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toNotThrow();
});
});
@@ -153,9 +156,9 @@ describe("getEventTimeline support", function() {
userId: userId,
accessToken: accessToken,
});
var room;
let room;
startClient(httpBackend, client
startClient(httpBackend, client,
).then(function() {
room = client.getRoom(roomId);
@@ -192,18 +195,19 @@ describe("getEventTimeline support", function() {
},
});
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.when("GET", "/messages").respond(200, {
chunk: [EVENTS[0]],
start: "pagin_start",
end: "pagin_end",
});
return httpBackend.flush("/sync", 2);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.flush("/messages", 1);
return client.scrollback(room);
}).then(function() {
@@ -211,15 +215,18 @@ describe("getEventTimeline support", function() {
expect(room.timeline[0].event).toEqual(EVENTS[0]);
expect(room.timeline[1].event).toEqual(EVENTS[1]);
expect(room.oldState.paginationToken).toEqual("pagin_end");
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
describe("MatrixClient event timelines", function() {
var client, httpBackend;
import expect from 'expect';
beforeEach(function(done) {
utils.beforeEach(this);
describe("MatrixClient event timelines", function() {
let client = null;
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
@@ -230,8 +237,7 @@ describe("MatrixClient event timelines", function() {
timelineSupport: true,
});
startClient(httpBackend, client)
.catch(utils.failTest).done(done);
return startClient(httpBackend, client);
});
afterEach(function() {
@@ -240,8 +246,9 @@ describe("MatrixClient event timelines", function() {
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
var room = client.getRoom(roomId);
it("should create a new timeline for new events", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
@@ -257,23 +264,25 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(room, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (var i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return Promise.all([
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (let i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}),
httpBackend.flushAllExpected(),
]);
});
it("should return existing timeline for known events", function(done) {
var room = client.getRoom(roomId);
it("should return existing timeline for known events", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -290,21 +299,25 @@ describe("MatrixClient event timelines", function() {
},
});
httpBackend.flush("/sync").then(function() {
return client.getEventTimeline(room, EVENTS[0].event_id);
return Promise.all([
httpBackend.flush("/sync"),
utils.syncPromise(client),
]).then(function() {
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].sender.name).toEqual(userName);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("s_5_4");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
});
});
it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId);
it("should update timelines where they overlap a previous /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -334,8 +347,9 @@ describe("MatrixClient event timelines", function() {
};
});
const deferred = Promise.defer();
client.on("sync", function() {
client.getEventTimeline(room, EVENTS[2].event_id
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
@@ -345,15 +359,20 @@ describe("MatrixClient event timelines", function() {
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).catch(utils.failTest).done(done);
}).done(() => deferred.resolve(),
(e) => deferred.reject(e));
});
httpBackend.flush().catch(utils.failTest);
return Promise.all([
httpBackend.flushAllExpected(),
deferred.promise,
]);
});
it("should join timelines where they overlap a previous /context",
function(done) {
var room = client.getRoom(roomId);
function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
@@ -409,44 +428,46 @@ describe("MatrixClient event timelines", function() {
};
});
var tl0, tl2, tl3;
client.getEventTimeline(room, EVENTS[0].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(room, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl2 = tl;
return client.getEventTimeline(room, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(room, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
let tl0;
let tl3;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}),
httpBackend.flushAllExpected(),
]);
});
it("should fail gracefully if there is no event field", function(done) {
var room = client.getRoom(roomId);
it("should fail gracefully if there is no event field", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
@@ -460,21 +481,23 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(room, "event1"
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}).catch(function(e) {
expect(String(e)).toMatch(/'event'/);
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return Promise.all([
client.getEventTimeline(timelineSet, "event1",
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}, function(e) {
expect(String(e)).toMatch(/'event'/);
}),
httpBackend.flushAllExpected(),
]);
});
});
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) {
var room = client.getRoom(roomId);
it("should allow you to paginate backwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -491,7 +514,7 @@ describe("MatrixClient event timelines", function() {
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
const params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual(30);
@@ -502,29 +525,31 @@ describe("MatrixClient event timelines", function() {
};
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
let tl;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}),
httpBackend.flushAllExpected(),
]);
});
it("should allow you to paginate forwards", function(done) {
var room = client.getRoom(roomId);
it("should allow you to paginate forwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -541,7 +566,7 @@ describe("MatrixClient event timelines", function() {
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
const params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual(20);
@@ -552,31 +577,32 @@ describe("MatrixClient event timelines", function() {
};
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
let tl;
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}),
httpBackend.flushAllExpected(),
]);
});
});
describe("event timeline for sent events", function() {
var TXN_ID = "txn1";
var event = utils.mkMessage({
const TXN_ID = "txn1";
const event = utils.mkMessage({
room: roomId, user: userId, msg: "a body",
});
event.unsigned = {transaction_id: TXN_ID};
@@ -595,7 +621,7 @@ describe("MatrixClient event timelines", function() {
"!foo:bar": {
timeline: {
events: [
event
event,
],
prev_batch: "f_1_1",
},
@@ -605,54 +631,138 @@ describe("MatrixClient event timelines", function() {
});
});
it("should work when /send returns before /sync", function(done) {
var room = client.getRoom(roomId);
it("should work when /send returns before /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
return Promise.all([
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}).catch(utils.failTest).done(done);
// now let the sync complete, and check it again
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}),
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest);
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1),
]);
});
it("should work when /send returns after /sync", function(done) {
var room = client.getRoom(roomId);
it("should work when /send returns after /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}).catch(utils.failTest).done(done);
return Promise.all([
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}),
httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(room, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}).catch(utils.failTest);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}),
]);
});
});
it("should handle gappy syncs after redactions", function(done) {
// https://github.com/vector-im/vector-web/issues/1389
// a state event, followed by a redaction thereof
const event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId,
});
const redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {},
});
redaction.redacts = event.event_id;
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
event,
redaction,
],
limited: false,
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client),
]).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[1].isRedacted()).toBe(true);
const sync2 = {
next_batch: "batch2",
rooms: {
join: {},
},
};
sync2.rooms.join[roomId] = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client),
]);
}).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
}).nodeify(done);
});
});
+231 -96
View File
@@ -1,25 +1,31 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var publicGlobals = require("../../lib/matrix");
var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var Filter = publicGlobals.Filter;
var utils = require("../test-utils");
var MockStorageApi = require("../MockStorageApi");
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
const Filter = publicGlobals.Filter;
const utils = require("../test-utils");
const MockStorageApi = require("../MockStorageApi");
import expect from 'expect';
describe("MatrixClient", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend, store, sessionStore;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let store = null;
let sessionStore = null;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
store = new MatrixInMemoryStore();
var mockStorage = new MockStorageApi();
const mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
@@ -37,14 +43,127 @@ describe("MatrixClient", function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("uploadContent", function() {
const buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
if (!(req.queryParams.access_token == accessToken ||
req.headers["Authorization"] == "Bearer " + accessToken)) {
expect(true).toBe(false);
}
expect(req.headers["Content-Type"]).toEqual("text/plain");
expect(req.opts.json).toBeFalsy();
expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content", true);
const prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
expect(prom).toBeTruthy();
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
// for backwards compatibility, we return the raw JSON
expect(response).toEqual("content");
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).nodeify(done);
httpBackend.flush();
});
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, { "content_uri": "uri" });
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}, {
rawResponse: false,
}).then(function(response) {
expect(response.content_uri).toEqual("uri");
}).nodeify(done);
httpBackend.flush();
});
it("should parse errors into a MatrixError", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy();
}).respond(400, {
"errcode": "M_SNAFU",
"error": "broken",
});
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}).then(function(response) {
throw Error("request not failed");
}, function(error) {
expect(error.httpStatus).toEqual(400);
expect(error.errcode).toEqual("M_SNAFU");
expect(error.message).toEqual("broken");
}).nodeify(done);
httpBackend.flush();
});
it("should return a promise which can be cancelled", function(done) {
const prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
throw Error("request not aborted");
}, function(error) {
expect(error).toEqual("aborted");
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).nodeify(done);
const r = client.cancelUpload(prom);
expect(r).toBe(true);
});
});
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
var room = new Room(roomId);
const roomId = "!foo:bar";
const room = new Room(roomId);
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true
})
user: userId, room: roomId, mship: "join", event: true,
}),
]);
store.storeRoom(room);
client.joinRoom(roomId);
@@ -53,11 +172,11 @@ describe("MatrixClient", function() {
});
describe("getFilter", function() {
var filterId = "f1lt3r1d";
const filterId = "f1lt3r1d";
it("should return a filter from the store if allowCached", function(done) {
var filter = Filter.fromJson(userId, filterId, {
event_format: "client"
const filter = Filter.fromJson(userId, filterId, {
event_format: "client",
});
store.storeFilter(filter);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
@@ -69,16 +188,16 @@ describe("MatrixClient", function() {
it("should do an HTTP request if !allowCached even if one exists",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
const httpFilterDefinition = {
event_format: "federation",
};
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
var storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client"
const storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client",
});
store.storeFilter(storeFilter);
client.getFilter(userId, filterId, false).done(function(gotFilter) {
@@ -91,17 +210,17 @@ describe("MatrixClient", function() {
it("should do an HTTP request if nothing is in the cache and then store it",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
const httpFilterDefinition = {
event_format: "federation",
};
expect(store.getFilter(userId, filterId)).toBeNull();
expect(store.getFilter(userId, filterId)).toBe(null);
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeDefined();
expect(store.getFilter(userId, filterId)).toBeTruthy();
done();
});
@@ -110,21 +229,21 @@ describe("MatrixClient", function() {
});
describe("createFilter", function() {
var filterId = "f1llllllerid";
const filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function(done) {
expect(store.getFilter(userId, filterId)).toBeNull();
expect(store.getFilter(userId, filterId)).toBe(null);
var filterDefinition = {
event_format: "client"
const filterDefinition = {
event_format: "client",
};
httpBackend.when(
"POST", "/user/" + encodeURIComponent(userId) + "/filter"
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
).check(function(req) {
expect(req.data).toEqual(filterDefinition);
}).respond(200, {
filter_id: filterId
filter_id: filterId,
});
client.createFilter(filterDefinition).done(function(gotFilter) {
@@ -138,8 +257,7 @@ describe("MatrixClient", function() {
});
describe("searching", function() {
var response = {
const response = {
search_categories: {
room_events: {
count: 24,
@@ -152,26 +270,26 @@ describe("MatrixClient", function() {
room_id: "!feuiwhf:localhost",
content: {
body: "a result",
msgtype: "m.text"
}
}
}
}
}
}
msgtype: "m.text",
},
},
},
},
},
},
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys"
query: "monkeys",
});
httpBackend.when("POST", "/search").check(function(req) {
expect(req.data).toEqual({
search_categories: {
room_events: {
search_term: "monkeys"
}
}
search_term: "monkeys",
},
},
});
}).respond(200, response);
@@ -183,59 +301,77 @@ describe("MatrixClient", function() {
describe("downloadKeys", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
beforeEach(function() {
return client.initCrypto();
});
it("should do an HTTP request and then store the keys", function(done) {
var ed25519key = "wV5E3EUSHpHuoZLljNzojlabjGdXT3Mz7rugG9zgbkI";
var borisKeys = {
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
const borisKeys = {
dev1: {
algorithms: ["1"], keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
device_id: "dev1",
keys: { "ed25519:dev1": ed25519key },
signatures: {
boris: {
"ed25519:dev1":
"u99n8WZ61G//K6eVgYc+RDLVapmjttxqhjNucIFGEIJ" +
"oA4TUY8FmiGv3zl0EA71zrvPDfnFL5XLNsdc55NGbDg"
}
"ed25519:dev1":
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
},
},
unsigned: { "abc": "def" },
}
user_id: "boris",
},
};
var chazKeys = {
const chazKeys = {
dev2: {
algorithms: ["2"], keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
device_id: "dev2",
keys: { "ed25519:dev2": ed25519key },
signatures: {
chaz: {
"ed25519:dev2":
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
}
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
},
},
unsigned: { "ghi": "def" },
}
};
var daveKeys = {
dev3: {
algorithms: ["3"], keys: { "ed25519:dev2": ed25519key },
signatures: {
dave: {
"ed25519:dev2":
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
}
},
unsigned: { "ghi": "def" },
}
user_id: "chaz",
},
};
/*
function sign(o) {
var anotherjson = require('another-json');
var b = JSON.parse(JSON.stringify(o));
delete(b.signatures);
delete(b.unsigned);
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
};
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: {}, dave: {}}});
expect(req.data).toEqual({device_keys: {
'boris': {},
'chaz': {},
}});
}).respond(200, {
device_keys: {
boris: borisKeys,
chaz: chazKeys,
dave: daveKeys,
},
});
client.downloadKeys(["boris", "chaz", "dave"]).then(function(res) {
client.downloadKeys(["boris", "chaz"]).then(function(res) {
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
@@ -245,29 +381,28 @@ describe("MatrixClient", function() {
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
// dave's key fails validation.
expect(res.dave).toEqual({});
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
});
it("should return a rejected promise if the request fails", function(done) {
httpBackend.when("POST", "/keys/query").respond(400);
describe("deleteDevice", function() {
const auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device",
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
var exceptionThrown;
client.downloadKeys(["bottom"]).then(function() {
fail("download didn't fail");
}, function(err) {
exceptionThrown = err;
}).then(function() {
expect(exceptionThrown).toBeTruthy();
}).catch(utils.failTest).done(done);
client.deleteDevice(
"my_device", auth,
).nodeify(done);
httpBackend.flush();
});
@@ -275,7 +410,7 @@ describe("MatrixClient", function() {
});
function assertObjectContains(obj, expected) {
for (var k in expected) {
for (const k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
+52 -44
View File
@@ -1,17 +1,22 @@
"use strict";
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient opts", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var userB = "@bob:localhost";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var syncData = {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
const userB = "@bob:localhost";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const syncData = {
next_batch: "s_5_3",
presence: {},
rooms: {
@@ -20,40 +25,40 @@ describe("MatrixClient opts", function() {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
room: roomId, user: userB, msg: "hello",
}),
],
prev_batch: "f_1_1"
prev_batch: "f_1_1",
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
}
name: "Old room name",
},
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
room: roomId, mship: "join", user: userB, name: "Bob",
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
room: roomId, mship: "join", user: userId, name: "Alice",
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
creator: userId,
},
}),
],
},
},
},
},
};
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
});
@@ -69,7 +74,7 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: new sdk.MatrixScheduler()
scheduler: new sdk.MatrixScheduler(),
});
});
@@ -78,9 +83,9 @@ describe("MatrixClient opts", function() {
});
it("should be able to send messages", function(done) {
var eventId = "$flibble:wibble";
const eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
event_id: eventId,
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(res.event_id).toEqual(eventId);
@@ -90,16 +95,16 @@ describe("MatrixClient opts", function() {
});
it("should be able to sync / get new events", function(done) {
var expectedEventTypes = [ // from /initialSync
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create"
"m.room.create",
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1, "Recv unexpected event type: " + event.getType()
expect(expectedEventTypes.indexOf(event.getType())).toNotEqual(
-1, "Recv unexpected event type: " + event.getType(),
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1
expectedEventTypes.indexOf(event.getType()), 1,
);
});
httpBackend.when("GET", "/pushrules").respond(200, {});
@@ -109,10 +114,13 @@ describe("MatrixClient opts", function() {
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/filter", 1);
}).then(function() {
return httpBackend.flush("/sync", 1);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
@@ -127,14 +135,14 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: undefined
scheduler: undefined,
});
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, {
errcode: "M_SOMETHING",
error: "Ruh roh"
error: "Ruh roh",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
@@ -147,13 +155,13 @@ describe("MatrixClient opts", function() {
it("shouldn't queue events", function(done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "AAA"
event_id: "AAA",
});
httpBackend.when("PUT", "/txn2").respond(200, {
event_id: "BBB"
event_id: "BBB",
});
var sentA = false;
var sentB = false;
let sentA = false;
let sentB = false;
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
sentA = true;
expect(sentB).toBe(true);
@@ -171,7 +179,7 @@ describe("MatrixClient opts", function() {
it("should be able to send messages", function(done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo"
event_id: "foo",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(res.event_id).toEqual("foo");
+50 -27
View File
@@ -1,20 +1,26 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventStatus = sdk.EventStatus;
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventStatus = sdk.EventStatus;
import expect from 'expect';
describe("MatrixClient retrying", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var scheduler;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
var roomId = "!room:here";
var room;
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
@@ -48,22 +54,25 @@ describe("MatrixClient retrying", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued
var ev1, ev2;
client.sendMessage(roomId, "m1").then(function(ev) {
expect(ev).toEqual(ev1);
});
client.sendMessage(roomId, "m2").then(function(ev) {
expect(ev).toEqual(ev2);
const p1 = client.sendMessage(roomId, "m1").then(function(ev) {
// we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully');
}, (e) => {
// this is expected
});
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, "m2");
// both events should be in the timeline at this point
var tl = room.getLiveTimeline().getEvents();
const tl = room.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
ev1 = tl[0];
ev2 = tl[1];
const ev1 = tl[0];
const ev2 = tl[1];
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.SENDING);
@@ -79,11 +88,19 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
expect(function() {
client.cancelPendingEvent(ev1);
}).toThrow();
}).respond(400); // fail the first message
httpBackend.flush().then(function() {
// wait for the localecho of ev1 to be updated
const p3 = new Promise((resolve, reject) => {
room.on("Room.localEchoUpdated", (ev0) => {
if(ev0 === ev1) {
resolve();
}
});
}).then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
@@ -91,7 +108,13 @@ describe("MatrixClient retrying", function() {
client.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
}).catch(utils.failTest).done(done);
});
return Promise.all([
p1,
p3,
httpBackend.flushAllExpected(),
]);
});
describe("resending", function() {
+221 -171
View File
@@ -1,28 +1,33 @@
"use strict";
var sdk = require("../..");
var EventStatus = sdk.EventStatus;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import Promise from 'bluebird';
import expect from 'expect';
describe("MatrixClient room timelines", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var userName = "Alice";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
var ROOM_NAME_EVENT = utils.mkEvent({
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
});
var NEXT_SYNC_DATA;
var SYNC_DATA = {
let NEXT_SYNC_DATA;
const SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
@@ -30,30 +35,30 @@ describe("MatrixClient room timelines", function() {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1"
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
creator: userId,
},
}),
],
},
},
},
},
};
function setNextSyncData(events) {
@@ -67,11 +72,11 @@ describe("MatrixClient room timelines", function() {
"!foo:bar": {
timeline: { events: [] },
state: { events: [] },
ephemeral: { events: [] }
}
ephemeral: { events: [] },
},
},
leave: {}
}
leave: {},
},
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
@@ -81,7 +86,7 @@ describe("MatrixClient room timelines", function() {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type
"for " + e.type,
);
}
if (e.__prev_event !== null) {
@@ -90,18 +95,16 @@ describe("MatrixClient room timelines", function() {
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
}
else {
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
});
}
beforeEach(function(done) {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
@@ -121,7 +124,7 @@ describe("MatrixClient room timelines", function() {
client.startClient();
httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
}).done(done);
}).nodeify(done);
});
afterEach(function() {
@@ -130,12 +133,13 @@ describe("MatrixClient room timelines", function() {
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1");
@@ -144,7 +148,7 @@ describe("MatrixClient room timelines", function() {
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
var member = room.timeline[1].sender;
const member = room.timeline[1].sender;
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
@@ -157,21 +161,23 @@ describe("MatrixClient room timelines", function() {
it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) {
var eventId = "$foo:bar";
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
event_id: eventId,
});
var ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = {transaction_id: "txn1"};
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
@@ -187,22 +193,24 @@ describe("MatrixClient room timelines", function() {
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
var eventId = "$foo:bar";
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
event_id: eventId,
});
var ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = {transaction_id: "txn1"};
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
@@ -212,15 +220,14 @@ describe("MatrixClient room timelines", function() {
done();
});
});
});
httpBackend.flush("/sync", 1);
});
});
describe("paginated events", function() {
var sbEvents;
var sbEndTok = "pagin_end";
let sbEvents;
const sbEndTok = "pagin_end";
beforeEach(function() {
sbEvents = [];
@@ -228,7 +235,7 @@ describe("MatrixClient room timelines", function() {
return {
chunk: sbEvents,
start: "pagin_start",
end: sbEndTok
end: sbEndTok,
};
});
});
@@ -236,18 +243,23 @@ describe("MatrixClient room timelines", function() {
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBeNull();
done();
expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/sync", 1);
});
@@ -263,55 +275,60 @@ describe("MatrixClient room timelines", function() {
// <Bob> hello
// make an m.room.member event for alice's join
var joinMshipEvent = utils.mkMembership({
const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null
url: null,
});
// make an m.room.member event with prev_content for alice's nick
// change
var oldMshipEvent = utils.mkMembership({
const oldMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: userName,
url: "mxc://some/url"
url: "mxc://some/url",
});
oldMshipEvent.prev_content = {
displayname: "Old Alice",
avatar_url: null,
membership: "join"
membership: "join",
};
// set the list of events to return on scrollback (/messages)
// N.B. synapse returns /messages in reverse chronological order
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I'm alice"
user: userId, room: roomId, msg: "I'm alice",
}),
oldMshipEvent,
utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice"
user: userId, room: roomId, msg: "I'm old alice",
}),
joinMshipEvent,
];
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
// sync response
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(5);
var joinMsg = room.timeline[0];
const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
var oldMsg = room.timeline[1];
const oldMsg = room.timeline[1];
expect(oldMsg.sender.name).toEqual("Old Alice");
var newMsg = room.timeline[3];
const newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
done();
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/sync", 1);
});
@@ -320,27 +337,32 @@ describe("MatrixClient room timelines", function() {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new"
user: userId, room: roomId, msg: "I am new",
}),
utils.mkMessage({
user: userId, room: roomId, msg: "I am old"
})
user: userId, room: roomId, msg: "I am old",
}),
];
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
done();
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/sync", 1);
});
@@ -349,22 +371,26 @@ describe("MatrixClient room timelines", function() {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new"
})
user: userId, room: roomId, msg: "I am new",
}),
];
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).done(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/sync", 1);
httpBackend.flush("/messages", 1).done(function() {
done();
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
});
httpBackend.flush("/sync", 1);
@@ -372,18 +398,20 @@ describe("MatrixClient room timelines", function() {
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
var eventData = [
it("should be added to the right place in the timeline", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId})
];
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
var index = 0;
let index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
@@ -392,172 +420,194 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1]
eventData[1],
);
expect(room.timeline[1].event).toEqual(
eventData[0]
eventData[0],
);
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
var eventData = [
it("should set the right event.sender values", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name"
user: userId, room: roomId, mship: "join", name: "New Name",
}),
utils.mkMessage({user: userId, room: roomId})
utils.mkMessage({user: userId, room: roomId}),
];
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
it("should set the right room.name", function(done) {
var secondRoomNameEvent = utils.mkEvent({
it("should set the right room.name", function() {
const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
name: "Room 2",
},
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var nameEmitCount = 0;
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
httpBackend.flush("/sync", 1).done(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
var thirdRoomNameEvent = utils.mkEvent({
const thirdRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
name: "Room 3",
},
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
});
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).then(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
});
});
httpBackend.flush("/sync", 1);
});
it("should set the right room members", function(done) {
var userC = "@cee:bar";
var userD = "@dee:bar";
var eventData = [
it("should set the right room members", function() {
const userC = "@cee:bar";
const userD = "@dee:bar";
const eventData = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C"
user: userC, room: roomId, mship: "join", name: "C",
}),
utils.mkMembership({
user: userC, room: roomId, mship: "invite", skey: userD
})
user: userC, room: roomId, mship: "invite", skey: userD,
}),
];
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
"join"
"join",
);
expect(room.currentState.getMember(userD).name).toEqual(userD);
expect(room.currentState.getMember(userD).membership).toEqual(
"invite"
"invite",
);
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function(done) {
var eventData = [
it("should copy the last known state to the new timeline", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
"join"
"join",
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join"
"join",
);
done();
});
});
httpBackend.flush("/sync", 1);
});
it("should emit a 'Room.timelineReset' event", function(done) {
var eventData = [
it("should emit a 'Room.timelineReset' event", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
var emitCount = 0;
let emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(emitCount).toEqual(1);
done();
});
});
httpBackend.flush("/sync", 1);
});
});
});
+265 -216
View File
@@ -1,30 +1,35 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var MatrixEvent = sdk.MatrixEvent;
var EventTimeline = sdk.EventTimeline;
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const MatrixEvent = sdk.MatrixEvent;
const EventTimeline = sdk.EventTimeline;
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient syncing", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
var otherUserId = "@bob:localhost";
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const otherUserId = "@bob:localhost";
const userA = "@alice:bar";
const userB = "@bob:bar";
const userC = "@claire:bar";
const roomOne = "!foo:localhost";
const roomTwo = "!bar:localhost";
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken
accessToken: selfAccessToken,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
@@ -36,10 +41,10 @@ describe("MatrixClient syncing", function() {
});
describe("startClient", function() {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {},
presence: {}
presence: {},
};
it("should /sync after /pushrules and /filter.", function(done) {
@@ -47,7 +52,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
done();
});
});
@@ -61,24 +66,23 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
done();
});
});
});
describe("resolving invites to profile info", function() {
var syncData = {
const syncData = {
next_batch: "s_5_3",
presence: {
events: []
events: [],
},
rooms: {
join: {
}
}
},
},
};
beforeEach(function() {
@@ -87,98 +91,103 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
});
it("should resolve incoming invites from /sync", function(done) {
it("should resolve incoming invites from /sync", function() {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
200, {
avatar_url: "mxc://flibble/wibble",
displayname: "The Boss"
}
displayname: "The Boss",
},
);
client.startClient({
resolveInvitesToProfiles: true
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeDefined();
done();
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeTruthy();
});
});
it("should use cached values from m.presence wherever possible", function(done) {
it("should use cached values from m.presence wherever possible", function() {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient({
resolveInvitesToProfiles: true
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Ghost");
done();
});
});
it("should result in events on the room member firing", function(done) {
it("should result in events on the room member firing", function() {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
})
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
var latestFiredName = null;
let latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
@@ -186,141 +195,147 @@ describe("MatrixClient syncing", function() {
});
client.startClient({
resolveInvitesToProfiles: true
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
expect(latestFiredName).toEqual("The Ghost");
done();
});
});
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
it("should no-op if resolveInvitesToProfiles is not set", function() {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeNull();
done();
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBe(null);
});
});
});
describe("users", function() {
var syncData = {
const syncData = {
next_batch: "nb",
presence: {
events: [
utils.mkPresence({
user: userA, presence: "online"
user: userA, presence: "online",
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
]
}
user: userB, presence: "unavailable",
}),
],
},
};
it("should create users for presence events from /sync",
function(done) {
function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
expect(client.getUser(userA).presence).toEqual("online");
expect(client.getUser(userB).presence).toEqual("unavailable");
done();
});
});
});
describe("room state", function() {
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
const msgText = "some text here";
const otherDisplayName = "Bob Smith";
var syncData = {
const syncData = {
rooms: {
join: {
}
}
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
var nextSyncData = {
const nextSyncData = {
rooms: {
join: {
}
}
},
},
};
nextSyncData.rooms.join[roomOne] = {
@@ -328,89 +343,97 @@ describe("MatrixClient syncing", function() {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
})
]
}
content: { name: "A new room name" },
}),
],
},
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
})
]
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
})
]
}
content: { user_ids: [otherUserId] },
}),
],
},
};
it("should continually recalculate the right room name.", function(done) {
it("should continually recalculate the right room name.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
);
done();
});
});
it("should store the right events in the timeline.", function(done) {
it("should store the right events in the timeline.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
// should have added the message from /events
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getContent().body).toEqual(msgText);
done();
});
});
it("should set the right room name.", function(done) {
it("should set the right room name.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
// should use the display name of the other person.
expect(room.name).toEqual(otherDisplayName);
done();
});
});
it("should set the right user's typing flag.", function(done) {
it("should set the right user's typing flag.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
var member = room.getMember(otherUserId);
expect(member).toBeDefined();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
let member = room.getMember(otherUserId);
expect(member).toBeTruthy();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
expect(member).toBeDefined();
expect(member).toBeTruthy();
expect(member.typing).toEqual(false);
done();
});
});
@@ -425,7 +448,7 @@ describe("MatrixClient syncing", function() {
describe("timeline", function() {
beforeEach(function() {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
@@ -435,7 +458,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
room: roomOne, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
@@ -445,11 +468,14 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
});
it("should set the back-pagination token on new rooms", function(done) {
var syncData = {
it("should set the back-pagination token on new rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
@@ -459,7 +485,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "roomtwo"
room: roomTwo, user: otherUserId, msg: "roomtwo",
}),
],
prev_batch: "roomtwotok",
@@ -468,17 +494,20 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
var room = client.getRoom(roomTwo);
var tok = room.getLiveTimeline()
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomTwo);
expect(room).toExist();
const tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("roomtwotok");
done();
}).catch(utils.failTest).done();
});
});
it("should set the back-pagination token on gappy syncs", function(done) {
var syncData = {
it("should set the back-pagination token on gappy syncs", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
@@ -488,7 +517,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world"
room: roomOne, user: otherUserId, msg: "world",
}),
],
limited: true,
@@ -497,104 +526,108 @@ describe("MatrixClient syncing", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
var resetCallCount = 0;
let resetCallCount = 0;
// the token should be set *before* timelineReset is emitted
client.on("Room.timelineReset", function(room) {
resetCallCount++;
var tl = room.getLiveTimeline();
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(0);
var tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("newerTok");
});
httpBackend.flush().then(function() {
var room = client.getRoom(roomOne);
var tl = room.getLiveTimeline();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomOne);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
expect(resetCallCount).toEqual(1);
done();
}).catch(utils.failTest).done();
});
});
});
describe("receipts", function() {
var syncData = {
const syncData = {
rooms: {
join: {
}
}
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world"
})
]
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
beforeEach(function() {
syncData.rooms.join[roomOne].ephemeral = {
events: []
events: [],
};
});
it("should sync receipts from /sync.", function(done) {
var ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
var receipt = {};
it("should sync receipts from /sync.", function() {
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {};
receipt[ackEvent.event_id] = {
"m.read": {}
"m.read": {},
};
receipt[ackEvent.event_id]["m.read"][userC] = {
ts: 176592842636
ts: 176592842636,
};
syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt"
type: "m.receipt",
}];
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
userId: userC,
data: {
ts: 176592842636
}
ts: 176592842636,
},
}]);
done();
});
});
});
@@ -614,7 +647,7 @@ describe("MatrixClient syncing", function() {
beforeEach(function(done) {
client.startClient();
httpBackend.flush().then(function() {
httpBackend.flushAllExpected().then(function() {
// the /sync call from syncLeftRooms ends up in the request
// queue behind the call from the running client; add a response
// to flush the client's one out.
@@ -624,33 +657,38 @@ describe("MatrixClient syncing", function() {
});
});
it("should create and use an appropriate filter", function(done) {
it("should create and use an appropriate filter", function() {
httpBackend.when("POST", "/filter").check(function(req) {
expect(req.data).toEqual({
room: { timeline: {limit: 1},
include_leave: true }});
}).respond(200, { filter_id: "another_id" });
const defer = Promise.defer();
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.filter).toEqual("another_id");
done();
defer.resolve();
}).respond(200, {});
client.syncLeftRooms();
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
// flush the syncs
return httpBackend.flush();
}).catch(utils.failTest);
return Promise.all([
httpBackend.flush("/filter").then(function() {
// flush the syncs
return httpBackend.flushAllExpected();
}),
defer.promise,
]);
});
it("should set the back-pagination token on left rooms", function(done) {
var syncData = {
it("should set the back-pagination token on left rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
leave: {}
leave: {},
},
};
@@ -658,7 +696,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hello"
room: roomTwo, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
@@ -666,25 +704,36 @@ describe("MatrixClient syncing", function() {
};
httpBackend.when("POST", "/filter").respond(200, {
filter_id: "another_id"
filter_id: "another_id",
});
httpBackend.when("GET", "/sync").respond(200, syncData);
client.syncLeftRooms().then(function() {
var room = client.getRoom(roomTwo);
var tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
return Promise.all([
client.syncLeftRooms().then(function() {
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
expect(tok).toEqual("pagTok");
done();
}).catch(utils.failTest).done();
expect(tok).toEqual("pagTok");
}),
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flush();
}).catch(utils.failTest);
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flushAllExpected();
}),
]);
});
});
/**
* waits for the MatrixClient to emit one or more 'sync' events.
*
* @param {Number?} numSyncs number of syncs to wait for
* @returns {Promise} promise which resolves after the sync events have happened
*/
function awaitSyncEvent(numSyncs) {
return utils.syncPromise(client, numSyncs);
}
});
+962
View File
@@ -0,0 +1,962 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
const anotherjson = require('another-json');
import Promise from 'bluebird';
import expect from 'expect';
const utils = require('../../lib/utils');
const testUtils = require('../test-utils');
const TestClient = require('../TestClient').default;
const ROOM_ID = "!room:id";
/**
* start an Olm session with a given recipient
*
* @param {Olm.Account} olmAccount
* @param {TestClient} recipientTestClient
* @return {Promise} promise for Olm.Session
*/
function createOlmSession(olmAccount, recipientTestClient) {
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
const otkId = utils.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
return session;
});
}
/**
* encrypt an event with olm
*
* @param {object} opts
* @param {string=} opts.sender
* @param {string} opts.senderKey
* @param {Olm.Session} opts.p2pSession
* @param {TestClient} opts.recipient
* @param {object=} opts.plaincontent
* @param {string=} opts.plaintype
*
* @return {object} event
*/
function encryptOlmEvent(opts) {
expect(opts.senderKey).toBeTruthy();
expect(opts.p2pSession).toBeTruthy();
expect(opts.recipient).toBeTruthy();
const plaintext = {
content: opts.plaincontent || {},
recipient: opts.recipient.userId,
recipient_keys: {
ed25519: opts.recipient.getSigningKey(),
},
sender: opts.sender || '@bob:xyz',
type: opts.plaintype || 'm.test',
};
const event = {
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
ciphertext: {},
sender_key: opts.senderKey,
},
sender: opts.sender || '@bob:xyz',
type: 'm.room.encrypted',
};
event.content.ciphertext[opts.recipient.getDeviceKey()] =
opts.p2pSession.encrypt(JSON.stringify(plaintext));
return event;
}
/**
* encrypt an event with megolm
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {object=} opts.plaintext
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptMegolmEvent(opts) {
expect(opts.senderKey).toBeTruthy();
expect(opts.groupSession).toBeTruthy();
const plaintext = opts.plaintext || {};
if (!plaintext.content) {
plaintext.content = {
body: '42',
msgtype: "m.text",
};
}
if (!plaintext.type) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeTruthy();
plaintext.room_id = opts.room_id;
}
return {
event_id: 'test_megolm_event',
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
device_id: "testDevice",
sender_key: opts.senderKey,
session_id: opts.groupSession.session_id(),
},
type: "m.room.encrypted",
};
}
/**
* build an encrypted room_key event to share a group session
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {TestClient} opts.recipient
* @param {Olm.Session} opts.p2pSession
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptGroupSessionKey(opts) {
return encryptOlmEvent({
senderKey: opts.senderKey,
recipient: opts.recipient,
p2pSession: opts.p2pSession,
plaincontent: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: opts.room_id,
session_id: opts.groupSession.session_id(),
session_key: opts.groupSession.session_key(),
},
plaintype: 'm.room_key',
});
}
/**
* get a /sync response which contains a single room (ROOM_ID),
* with the members given
*
* @param {string[]} roomMembers
*
* @return {object} event
*/
function getSyncResponse(roomMembers) {
const roomResponse = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
],
},
};
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
testUtils.mkMembership({
mship: 'join',
sender: roomMembers[i],
}),
);
}
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = roomResponse;
return syncResponse;
}
describe("megolm", function() {
if (!global.Olm) {
console.warn('not running megolm tests: Olm not present');
return;
}
const Olm = global.Olm;
let testOlmAccount;
let testSenderKey;
let aliceTestClient;
/**
* Get the device keys for testOlmAccount in a format suitable for a
* response to /keys/query
*
* @param {string} userId The user ID to query for
* @returns {Object} The fake query response
*/
function getTestKeysQueryResponse(userId) {
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
const testDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'DEVICE_ID',
keys: {
'curve25519:DEVICE_ID': testE2eKeys.curve25519,
'ed25519:DEVICE_ID': testE2eKeys.ed25519,
},
user_id: userId,
};
const j = anotherjson.stringify(testDeviceKeys);
const sig = testOlmAccount.sign(j);
testDeviceKeys.signatures = {};
testDeviceKeys.signatures[userId] = {
'ed25519:DEVICE_ID': sig,
};
const queryResponse = {
device_keys: {},
};
queryResponse.device_keys[userId] = {
'DEVICE_ID': testDeviceKeys,
};
return queryResponse;
}
/**
* Get a one-time key for testOlmAccount in a format suitable for a
* response to /keys/claim
* @param {string} userId The user ID to query for
* @returns {Object} The fake key claim response
*/
function getTestKeysClaimResponse(userId) {
testOlmAccount.generate_one_time_keys(1);
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
testOlmAccount.mark_keys_as_published();
const keyId = utils.keys(testOneTimeKeys.curve25519)[0];
const oneTimeKey = testOneTimeKeys.curve25519[keyId];
const keyResult = {
'key': oneTimeKey,
};
const j = anotherjson.stringify(keyResult);
const sig = testOlmAccount.sign(j);
keyResult.signatures = {};
keyResult.signatures[userId] = {
'ed25519:DEVICE_ID': sig,
};
const claimResponse = {one_time_keys: {}};
claimResponse.one_time_keys[userId] = {
'DEVICE_ID': {},
};
claimResponse.one_time_keys[userId].DEVICE_ID['signed_curve25519:' + keyId] =
keyResult;
return claimResponse;
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
await aliceTestClient.client.initCrypto();
testOlmAccount = new Olm.Account();
testOlmAccount.create();
const testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice receives a megolm message", function() {
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getContent().body).toEqual('42');
});
});
it("Alice receives a megolm message before the session keys", function() {
// https://github.com/vector-im/riot-web/issues/2273
let roomKeyEncrypted;
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice just gets the message event to start with
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().msgtype).toEqual('m.bad.encrypted');
// now she gets the room_key event
const syncResponse = {
next_batch: 2,
to_device: {
events: [roomKeyEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent().msgtype != 'm.bad.encrypted') {
return event;
}
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}).then((event) => {
expect(event.getContent().body).toEqual('42');
});
});
it("Alice gets a second room_key message", function() {
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted1 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// make a second room_key event now that we have advanced the group
// session.
const roomKeyEncrypted2 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// on the first sync, send the best room key
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted1],
},
});
// on the second sync, send the advanced room key, along with the
// message. This simulates the situation where Alice has been sent a
// later copy of the room key and is reloading the client.
const syncResponse2 = {
next_batch: 2,
to_device: {
events: [roomKeyEncrypted2],
},
rooms: {
join: {},
},
};
syncResponse2.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
});
});
it('Alice sends a megolm message', function() {
let p2pSession;
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((_p2pSession) => {
p2pSession = _p2pSession;
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
// start out with the device unknown - the send should be rejected.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
throw new Error("sendTextMessage failed on an unknown device");
}, (e) => {
expect(e.name).toEqual("UnknownDeviceError");
}),
aliceTestClient.httpBackend.flushAllExpected(),
]);
}).then(function() {
// mark the device as known, and resend.
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
let inboundGroupSession;
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
const ct = content.ciphertext;
const r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
expect(r.message_index).toEqual(0);
const decrypted = JSON.parse(r.plaintext);
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
return {
event_id: '$event_id',
};
});
const room = aliceTestClient.client.getRoom(ROOM_ID);
const pendingMsg = room.getPendingEvents()[0];
return Promise.all([
aliceTestClient.client.resendEvent(pendingMsg, room),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it("We shouldn't attempt to send to blocked devices", function() {
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
console.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
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 Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it("We should start a new megolm session when a device is blocked", function() {
let p2pSession;
let megolmSessionId;
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((_p2pSession) => {
p2pSession = _p2pSession;
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Fetching bob's devices and marking known");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flushAllExpected(),
]).then((keys) => {
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
});
}).then(function() {
console.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
console.log('sendToDevice: ', content);
const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(1); // normal message
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
console.log('decrypted sendToDevice:', decrypted);
expect(decrypted.type).toEqual('m.room_key');
megolmSessionId = decrypted.content.session_id;
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
console.log('/send:', content);
expect(content.session_id).toEqual(megolmSessionId);
return {
event_id: '$event_id',
};
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).then(function() {
console.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
console.log('Telling alice to send another megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
console.log('/send:', content);
expect(content.session_id).toNotEqual(megolmSessionId);
return {
event_id: '$event_id',
};
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
aliceTestClient.httpBackend.flushAllExpected(),
]);
});
});
// https://github.com/vector-im/riot-web/issues/2676
it("Alice should send to her other devices", function() {
// for this test, we make the testOlmAccount be another of Alice's devices.
// it ought to get included in messages Alice sends.
let p2pSession;
let inboundGroupSession;
let decrypted;
return aliceTestClient.start().then(function() {
// an encrypted room with just alice
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
testUtils.mkMembership({
mship: 'join',
sender: aliceTestClient.userId,
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
// the completion of the first initialsync hould make Alice
// invalidate the device cache for all members in e2e rooms (ie,
// herself), and do a key query.
aliceTestClient.expectKeyQuery(
getTestKeysQueryResponse(aliceTestClient.userId),
);
return aliceTestClient.httpBackend.flushAllExpected();
}).then(function() {
// start out with the device unknown - the send should be rejected.
return aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
throw new Error("sendTextMessage failed on an unknown device");
}, (e) => {
expect(e.name).toEqual("UnknownDeviceError");
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
expect(Object.keys(e.devices[aliceTestClient.userId])).
toEqual(['DEVICE_ID']);
});
}).then(function() {
// mark the device as known, and resend.
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
200, function(path, content) {
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
.toEqual("signed_curve25519");
return getTestKeysClaimResponse(aliceTestClient.userId);
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
console.log("sendToDevice: ", content);
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(0); // pre-key message
p2pSession = new Olm.Session();
p2pSession.create_inbound(testOlmAccount, ct.body);
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
const ct = content.ciphertext;
const r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
decrypted = JSON.parse(r.plaintext);
return {
event_id: '$event_id',
};
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).then(function() {
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
});
});
it('Alice should wait for device list to complete when sending a megolm message',
function() {
let downloadPromise;
let sendPromise;
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
// this will block
console.log('Forcing alice to download our device keys');
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
// so will this.
sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
.then(() => {
throw new Error("sendTextMessage failed on an unknown device");
}, (e) => {
expect(e.name).toEqual("UnknownDeviceError");
});
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
return aliceTestClient.httpBackend.flushAllExpected();
}).then(function() {
return Promise.all([downloadPromise, sendPromise]);
});
});
it("Alice exports megolm keys and imports them to a new device", function() {
let messageEncrypted;
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
return aliceTestClient.client.exportRoomKeys();
}).then(function(exported) {
// start a new client
aliceTestClient.stop();
aliceTestClient = new TestClient(
"@alice:localhost", "device2", "access_token2",
);
return aliceTestClient.client.initCrypto().then(() => {
aliceTestClient.client.importRoomKeys(exported);
return aliceTestClient.start();
});
}).then(function() {
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
});
});
});
-227
View File
@@ -1,227 +0,0 @@
"use strict";
var q = require("q");
/**
* Construct a mock HTTP backend, heavily inspired by Angular.js.
* @constructor
*/
function HttpBackend() {
this.requests = [];
this.expectedRequests = [];
var self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
var abort = function() {
var idx = self.requests.indexOf(realReq);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
realReq.callback("aborted");
}
};
return {
abort: abort
};
};
}
HttpBackend.prototype = {
/**
* Respond to all of the requests (flush the queue).
* @param {string} path The path to flush (optional) default: all.
* @param {integer} numToFlush The number of things to flush (optional), default: all.
* @return {Promise} resolved when there is nothing left to flush.
*/
flush: function(path, numToFlush) {
var defer = q.defer();
var self = this;
var flushed = 0;
var triedWaiting = false;
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
);
var tryFlush = function() {
// if there's more real requests and more expected requests, flush 'em.
console.log(
" trying to flush queue => reqs=%s expected=%s [%s]",
self.requests.length, self.expectedRequests.length, path
);
if (self._takeFromQueue(path)) {
// try again on the next tick.
console.log(" flushed. Trying for more. [%s]", path);
flushed += 1;
if (numToFlush && flushed === numToFlush) {
console.log(" [%s] Flushed assigned amount: %s", path, numToFlush);
defer.resolve();
}
else {
setTimeout(tryFlush, 0);
}
}
else if (flushed === 0 && !triedWaiting) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
setTimeout(tryFlush, 5);
triedWaiting = true;
}
else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
}
};
setTimeout(tryFlush, 0);
return defer.promise;
},
/**
* Attempts to resolve requests/expected requests.
* @param {string} path The path to flush (optional) default: all.
* @return {boolean} true if something was resolved.
*/
_takeFromQueue: function(path) {
var req = null;
var i, j;
var matchingReq, expectedReq, testResponse = null;
for (i = 0; i < this.requests.length; i++) {
req = this.requests[i];
for (j = 0; j < this.expectedRequests.length; j++) {
expectedReq = this.expectedRequests[j];
if (path && path !== expectedReq.path) { continue; }
if (expectedReq.method === req.method &&
req.path.indexOf(expectedReq.path) !== -1) {
if (!expectedReq.data || (JSON.stringify(expectedReq.data) ===
JSON.stringify(req.data))) {
matchingReq = expectedReq;
this.expectedRequests.splice(j, 1);
break;
}
}
}
if (matchingReq) {
// remove from request queue
this.requests.splice(i, 1);
i--;
for (j = 0; j < matchingReq.checks.length; j++) {
matchingReq.checks[j](req);
}
testResponse = matchingReq.response;
console.log(" responding to %s", matchingReq.path);
var body = testResponse.body;
if (Object.prototype.toString.call(body) == "[object Function]") {
body = body(req.path, req.data);
}
req.callback(
testResponse.err, testResponse.response, body
);
matchingReq = null;
}
}
if (testResponse) { // flushed something
return true;
}
return false;
},
/**
* Makes sure that the SDK hasn't sent any more requests to the backend.
*/
verifyNoOutstandingRequests: function() {
var firstOutstandingReq = this.requests[0] || {};
expect(this.requests.length).toEqual(0,
"Expected no more HTTP requests but received request to " +
firstOutstandingReq.path
);
},
/**
* Makes sure that the test doesn't have any unresolved requests.
*/
verifyNoOutstandingExpectation: function() {
var firstOutstandingExpectation = this.expectedRequests[0] || {};
expect(this.expectedRequests.length).toEqual(0,
"Expected to see HTTP request for " + firstOutstandingExpectation.path
);
},
/**
* Create an expected request.
* @param {string} method The HTTP method
* @param {string} path The path (which can be partial)
* @param {Object} data The expected data.
* @return {Request} An expected request.
*/
when: function(method, path, data) {
var pendingReq = new Request(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
}
};
function Request(method, path, data, queryParams) {
this.method = method;
this.path = path;
this.data = data;
this.queryParams = queryParams;
this.callback = null;
this.response = null;
this.checks = [];
}
Request.prototype = {
/**
* Execute a check when this request has been satisfied.
* @param {Function} fn The function to execute.
* @return {Request} for chaining calls.
*/
check: function(fn) {
this.checks.push(fn);
return this;
},
/**
* Respond with the given data when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Object|Function} data The HTTP JSON body. If this is a function,
* it will be invoked when the JSON body is required (which should be returned).
*/
respond: function(code, data) {
this.response = {
response: {
statusCode: code,
headers: {}
},
body: data,
err: null
};
},
/**
* Fail with an Error when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Error} err The error to throw (e.g. Network Error)
*/
fail: function(code, err) {
this.response = {
response: {
statusCode: code,
headers: {}
},
body: null,
err: err
};
}
};
/**
* The HttpBackend class.
*/
module.exports = HttpBackend;
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2017 Vector creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// try to load the olm library.
try {
global.Olm = require('olm');
console.log('loaded libolm');
} catch (e) {
console.warn("unable to run crypto tests: libolm not available");
}
+115 -45
View File
@@ -1,14 +1,53 @@
"use strict";
var sdk = require("..");
var MatrixEvent = sdk.MatrixEvent;
import expect from 'expect';
import Promise from 'bluebird';
// load olm before the sdk if possible
import './olm-loader';
import sdk from '..';
const MatrixEvent = sdk.MatrixEvent;
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
module.exports.syncPromise = function(client, count) {
if (count === undefined) {
count = 1;
}
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise((resolve, reject) => {
const cb = (state) => {
console.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state == 'SYNCING') {
resolve();
} else {
client.once('sync', cb);
}
};
client.once('sync', cb);
});
return p.then(() => {
return module.exports.syncPromise(client, count-1);
});
};
/**
* Perform common actions before each test case, e.g. printing the test case
* name to stdout.
* @param {TestCase} testCase The test case that is about to be run.
* @param {Mocha.Context} context The test context
*/
module.exports.beforeEach = function(testCase) {
var desc = testCase.suite.description + " : " + testCase.description;
module.exports.beforeEach = function(context) {
const desc = context.currentTest.fullTitle();
console.log(desc);
console.log(new Array(1 + desc.length).join("="));
};
@@ -20,21 +59,20 @@ module.exports.beforeEach = function(testCase) {
* @return {Object} An instantiated object with spied methods/properties.
*/
module.exports.mock = function(constr, name) {
// By Tim Buschtöns
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
var HelperConstr = new Function(); // jshint ignore:line
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
var result = new HelperConstr();
result.jasmineToString = function() {
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (var key in constr.prototype) { // jshint ignore:line
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jasmine.createSpy((name || "mock") + '.' + key);
result[key] = expect.createSpy();
}
}
catch (ex) {
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
@@ -48,7 +86,7 @@ module.exports.mock = function(constr, name) {
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.user The event.user_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
@@ -58,17 +96,16 @@ module.exports.mkEvent = function(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
var event = {
const event = {
type: opts.type,
room_id: opts.room,
sender: opts.user,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
event_id: "$" + Math.random() + "-" + Math.random(),
};
if (opts.skey) {
if (opts.skey !== undefined) {
event.state_key = opts.skey;
}
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].indexOf(opts.type) !== -1) {
event.state_key = "";
@@ -85,16 +122,16 @@ module.exports.mkPresence = function(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
var event = {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.user,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline"
}
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
};
@@ -104,8 +141,8 @@ module.exports.mkPresence = function(opts) {
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.skey The other user ID for the event if applicable
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
@@ -115,16 +152,20 @@ module.exports.mkPresence = function(opts) {
module.exports.mkMembership = function(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
opts.content = {
membership: opts.mship
membership: opts.mship,
};
if (opts.name) { opts.content.displayname = opts.name; }
if (opts.url) { opts.content.avatar_url = opts.url; }
if (opts.name) {
opts.content.displayname = opts.name;
}
if (opts.url) {
opts.content.avatar_url = opts.url;
}
return module.exports.mkEvent(opts);
};
@@ -147,27 +188,56 @@ module.exports.mkMessage = function(opts) {
}
opts.content = {
msgtype: "m.text",
body: opts.msg
body: opts.msg,
};
return module.exports.mkEvent(opts);
};
/**
* make the test fail, with the given exception
* A mock implementation of webstorage
*
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} error exception to be reported
*
* @example
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).catch(utils.failTest).done(done);
* });
* @constructor
*/
module.exports.failTest = function(error) {
expect(error.stack).toBe(null);
module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
key: function(i) {
return Object.keys(this.data)[i];
},
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
},
};
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
module.exports.awaitDecryption = function(event) {
if (!event.isBeingDecrypted()) {
return Promise.resolve(event);
}
console.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
};
+22 -19
View File
@@ -1,34 +1,37 @@
"use strict";
var ContentRepo = require("../../lib/content-repo");
var testUtils = require("../test-utils");
import 'source-map-support/register';
const ContentRepo = require("../../lib/content-repo");
const testUtils = require("../test-utils");
import expect from 'expect';
describe("ContentRepo", function() {
var baseUrl = "https://my.home.server";
const baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this);
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
var httpUrl = "http://example.com/image.jpeg";
const httpUrl = "http://example.com/image.jpeg";
expect(
ContentRepo.getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true
)
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
var httpUrl = "http://example.com/image.jpeg";
const httpUrl = "http://example.com/image.jpeg";
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid"
baseUrl + "/_matrix/media/v1/download/server.name/resourceid",
);
});
@@ -38,27 +41,27 @@ describe("ContentRepo", function() {
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop"
"?width=32&height=64&method=crop",
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32#automade"
"?width=32#automade",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade"
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade",
);
});
});
@@ -71,21 +74,21 @@ describe("ContentRepo", function() {
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=96&height=96"
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=32&height=64"
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
"?width=32&height=64"
"?width=32&height=64",
);
});
});
+20
View File
@@ -0,0 +1,20 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
let Crypto;
if (sdk.CRYPTO_ENABLED) {
Crypto = require("../../lib/crypto");
}
import expect from 'expect';
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(2);
});
});
+128
View File
@@ -0,0 +1,128 @@
import DeviceList from '../../../lib/crypto/DeviceList';
import MockStorageApi from '../../MockStorageApi';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import testUtils from '../../test-utils';
import utils from '../../../lib/utils';
import expect from 'expect';
import Promise from 'bluebird';
const signedDeviceList = {
"failures": {},
"device_keys": {
"@test1:sw1v.org": {
"HGKAWHRVJQ": {
"signatures": {
"@test1:sw1v.org": {
"ed25519:HGKAWHRVJQ":
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
},
},
"user_id": "@test1:sw1v.org",
"keys": {
"ed25519:HGKAWHRVJQ":
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ":
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "HGKAWHRVJQ",
"unsigned": {},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let sessionStore;
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
});
function createTestDeviceList() {
const baseApis = {
downloadKeysForUsers: downloadSpy,
};
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
return new DeviceList(baseApis, sessionStore, mockOlm);
}
it("should successfully download and store device keys", function() {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = Promise.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
return prom1.then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
it("should have an outdated devicelist on an invalidation while an " +
"update is in progress", function() {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = Promise.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
downloadSpy.reset();
// outdated notif arrives while the request is in flight.
const queryDefer2 = Promise.defer();
downloadSpy.andReturn(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org');
dl.refreshOutdatedDeviceLists();
// the first request completes
queryDefer1.resolve({
device_keys: {
'@test1:sw1v.org': {},
},
});
return prom1.then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
console.log("Creating new devicelist to simulate app reload");
downloadSpy.reset();
const dl2 = createTestDeviceList();
const queryDefer3 = Promise.defer();
downloadSpy.andReturn(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
// allow promise chain to complete
return prom3;
}).then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
});
+185
View File
@@ -0,0 +1,185 @@
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run megolm tests: libolm not available");
}
import expect from 'expect';
import Promise from 'bluebird';
import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
// Crypto and OlmDevice won't import unless we have global.Olm
let OlmDevice;
let Crypto;
if (global.Olm) {
OlmDevice = require('../../../../lib/crypto/OlmDevice');
Crypto = require('../../../../lib/crypto');
}
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
describe("MegolmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
return;
}
let megolmDecryption;
let mockOlmLib;
let mockCrypto;
let mockBaseApis;
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockBaseApis = {};
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
mockOlmLib.encryptMessageForDevice =
expect.createSpy().andReturn(Promise.resolve());
megolmDecryption.olmlib = mockOlmLib;
});
describe('receives some keys:', function() {
let groupSession;
beforeEach(function() {
groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
const decryptedData = {
clearEvent: {
type: 'm.room_key',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: ROOM_ID,
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
},
},
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
const mockCrypto = {
decryptEvent: function() {
return Promise.resolve(decryptedData);
},
};
return event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
});
});
it('can decrypt an event', function() {
const event = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: groupSession.session_id(),
ciphertext: groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
})),
},
});
return megolmDecryption.decryptEvent(event).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('can respond to a key request event', function() {
const keyRequest = {
userId: '@alice:foo',
deviceId: 'alidevice',
requestBody: {
room_id: ROOM_ID,
sender_key: "SENDER_CURVE25519",
session_id: groupSession.session_id(),
},
};
return megolmDecryption.hasKeysForKeyRequest(
keyRequest,
).then((hasKeys) => {
expect(hasKeys).toBe(true);
// set up some pre-conditions for the share call
const deviceInfo = {};
mockCrypto.getStoredDevice.andReturn(deviceInfo);
mockOlmLib.ensureOlmSessionsForDevices.andReturn(
Promise.resolve({'@alice:foo': {'alidevice': {
sessionId: 'alisession',
}}}),
);
const awaitEncryptForDevice = new Promise((res, rej) => {
mockOlmLib.encryptMessageForDevice.andCall(() => {
res();
return Promise.resolve();
});
});
mockBaseApis.sendToDevice = expect.createSpy();
// do the share
megolmDecryption.shareKeysWithDevice(keyRequest);
// it's asynchronous, so we have to wait a bit
return awaitEncryptForDevice;
}).then(() => {
// check that it called encryptMessageForDevice with
// appropriate args.
expect(mockOlmLib.encryptMessageForDevice.calls.length)
.toEqual(1);
const call = mockOlmLib.encryptMessageForDevice.calls[0];
const payload = call.arguments[6];
expect(payload.type).toEqual("m.forwarded_room_key");
expect(payload.content).toInclude({
sender_key: "SENDER_CURVE25519",
sender_claimed_ed25519_key: "SENDER_ED25519",
session_id: groupSession.session_id(),
chain_index: 0,
forwarding_curve25519_key_chain: [],
});
expect(payload.content.session_key).toExist();
});
});
});
});
+77 -64
View File
@@ -1,27 +1,37 @@
"use strict";
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
timeline._endState = utils.mock(sdk.RoomState, "endState");
}
import expect from 'expect';
describe("EventTimeline", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bertha:bar";
var timeline;
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
let timeline;
beforeEach(function() {
utils.beforeEach(this);
timeline = new EventTimeline(roomId);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
// XXX: this is a horrid hack; should use sinon or something instead to mock
const timelineSet = { room: { roomId: roomId }};
timelineSet.room.getUnfilteredTimelineSet = function() {
return timelineSet;
};
timeline = new EventTimeline(timelineSet);
});
describe("construction", function() {
it("getRoomId should get room id", function() {
var v = timeline.getRoomId();
const v = timeline.getRoomId();
expect(v).toEqual(roomId);
});
});
@@ -32,7 +42,7 @@ describe("EventTimeline", function() {
});
it("should copy state events to start and end state", function() {
var events = [
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
@@ -41,34 +51,38 @@ describe("EventTimeline", function() {
type: "m.room.name", room: roomId, user: userB,
event: true,
content: { name: "New room" },
})
}),
];
timeline.initialiseState(events);
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
events
events,
);
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
events
events,
);
});
it("should raise an exception if called after events are added", function() {
var event =
const event =
utils.mkMessage({
room: roomId, user: userA, msg: "Adam stole the plushies",
event: true,
});
var state = [
const state = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
})
}),
];
expect(function() { timeline.initialiseState(state); }).not.toThrow();
expect(function() {
timeline.initialiseState(state);
}).toNotThrow();
timeline.addEvent(event, false);
expect(function() { timeline.initialiseState(state); }).toThrow();
expect(function() {
timeline.initialiseState(state);
}).toThrow();
});
});
@@ -94,8 +108,8 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should set neighbour", function() {
var prev = {a: "a"};
var next = {b: "b"};
const prev = {a: "a"};
const next = {b: "b"};
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
@@ -103,11 +117,11 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should throw if called twice", function() {
var prev = {a: "a"};
var next = {b: "b"};
const prev = {a: "a"};
const next = {b: "b"};
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
}).toNotThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(prev);
expect(function() {
@@ -116,7 +130,7 @@ describe("EventTimeline", function() {
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow();
}).toNotThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(next);
expect(function() {
@@ -130,7 +144,7 @@ describe("EventTimeline", function() {
mockRoomStates(timeline);
});
var events = [
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
@@ -143,7 +157,7 @@ describe("EventTimeline", function() {
it("should be able to add events to the end", function() {
timeline.addEvent(events[0], false);
var initialIndex = timeline.getBaseIndex();
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
@@ -153,7 +167,7 @@ describe("EventTimeline", function() {
it("should be able to add events to the start", function() {
timeline.addEvent(events[0], true);
var initialIndex = timeline.getBaseIndex();
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], true);
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
@@ -162,38 +176,38 @@ describe("EventTimeline", function() {
});
it("should set event.sender for new and old events", function() {
var sentinel = {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice"
name: "Alice",
};
var oldSentinel = {
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
const newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
content: { name: "New Room Name" },
});
var oldEv = utils.mkEvent({
const oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
content: { name: "Old Room Name" },
});
timeline.addEvent(newEv, false);
@@ -204,36 +218,36 @@ describe("EventTimeline", function() {
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice"
name: "Alice",
};
var oldSentinel = {
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
});
timeline.addEvent(newEv, false);
expect(newEv.target).toEqual(sentinel);
@@ -243,16 +257,16 @@ describe("EventTimeline", function() {
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
name: "New room",
},
}),
];
timeline.addEvent(events[0], false);
@@ -267,22 +281,22 @@ describe("EventTimeline", function() {
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
not.toHaveBeenCalled();
toNotHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
name: "New room",
},
}),
];
timeline.addEvent(events[0], true);
@@ -297,12 +311,12 @@ describe("EventTimeline", function() {
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
not.toHaveBeenCalled();
toNotHaveBeenCalled();
});
});
describe("removeEvent", function() {
var events = [
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
@@ -322,7 +336,7 @@ describe("EventTimeline", function() {
timeline.addEvent(events[1], false);
expect(timeline.getEvents().length).toEqual(2);
var ev = timeline.removeEvent(events[0].getId());
let ev = timeline.removeEvent(events[0].getId());
expect(ev).toBe(events[0]);
expect(timeline.getEvents().length).toEqual(1);
@@ -354,7 +368,7 @@ describe("EventTimeline", function() {
function() {
timeline.addEvent(events[0], true);
timeline.removeEvent(events[0].getId());
var initialIndex = timeline.getBaseIndex();
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
timeline.addEvent(events[2], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
@@ -362,4 +376,3 @@ describe("EventTimeline", function() {
});
});
});
+82
View File
@@ -0,0 +1,82 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import sdk from '../..';
const MatrixEvent = sdk.MatrixEvent;
import testUtils from '../test-utils';
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixEvent", () => {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe(".attemptDecryption", () => {
let encryptedEvent;
beforeEach(() => {
encryptedEvent = new MatrixEvent({
id: 'test_encrypted_event',
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
},
});
});
it('should retry decryption if a retry is queued', () => {
let callCount = 0;
let prom2;
const crypto = {
decryptEvent: function() {
++callCount;
console.log(`decrypt: ${callCount}`);
if (callCount == 1) {
// schedule a second decryption attempt while
// the first one is still running.
prom2 = encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
} else {
expect(prom2.isFulfilled()).toBe(
false, 'second attemptDecryption resolved too soon');
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}
},
};
return encryptedEvent.attemptDecryption(crypto).then(() => {
expect(callCount).toEqual(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
// make sure the second attemptDecryption resolves
return prom2;
});
});
});
});
+18 -15
View File
@@ -1,24 +1,27 @@
"use strict";
var sdk = require("../..");
var Filter = sdk.Filter;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const Filter = sdk.Filter;
const utils = require("../test-utils");
import expect from 'expect';
describe("Filter", function() {
var filterId = "f1lt3ring15g00d4ursoul";
var userId = "@sir_arthur_david:humming.tiger";
var filter;
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
var definition = {
event_fields: ["type", "content"]
const definition = {
event_fields: ["type", "content"],
};
var f = Filter.fromJson(userId, filterId, definition);
const f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
@@ -31,17 +34,17 @@ describe("Filter", function() {
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10
}
}
limit: 10,
},
},
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
var definition = {
event_format: "client"
const definition = {
event_format: "client",
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
+156
View File
@@ -0,0 +1,156 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const utils = require("../test-utils");
const InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
import expect from 'expect';
// Trivial client object to test interactive auth
// (we do not need TestClient here)
class FakeClient {
generateClientSecret() {
return "testcl1Ent5EcreT";
}
}
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
it("should start an auth stage and complete it", function(done) {
const doRequest = expect.createSpy();
const stateUpdated = expect.createSpy();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
authData: {
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// first we expect a call here
stateUpdated.andCall(function(stage) {
console.log('aaaa');
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
// .. which should trigger a call here
const requestRes = {"a": "b"};
doRequest.andCall(function(authData) {
console.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return Promise.resolve(requestRes);
});
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(1);
expect(stateUpdated.calls.length).toEqual(1);
}).nodeify(done);
});
it("should make a request if no authdata is provided", function(done) {
const doRequest = expect.createSpy();
const stateUpdated = expect.createSpy();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
stateUpdated: stateUpdated,
doRequest: doRequest,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams("logintype")).toBe(undefined);
// first we expect a call to doRequest
doRequest.andCall(function(authData) {
console.log("request1", authData);
expect(authData).toEqual({});
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to stateUpdated
const requestRes = {"a": "b"};
stateUpdated.andCall(function(stage) {
expect(stage).toEqual("logintype");
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.andCall(function(authData) {
console.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return Promise.resolve(requestRes);
});
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(2);
expect(stateUpdated.calls.length).toEqual(1);
}).nodeify(done);
});
});
+166 -93
View File
@@ -1,44 +1,51 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var utils = require("../test-utils");
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
describe("MatrixClient", function() {
var userId = "@alice:bar";
var identityServerUrl = "https://identity.server";
var identityServerDomain = "identity.server";
var client, store, scheduler;
const userId = "@alice:bar";
const identityServerUrl = "https://identity.server";
const identityServerDomain = "identity.server";
let client;
let store;
let scheduler;
let clock;
var KEEP_ALIVE_PATH = "/_matrix/client/versions";
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
var PUSH_RULES_RESPONSE = {
const PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {}
data: {},
};
var FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
var FILTER_RESPONSE = {
const FILTER_RESPONSE = {
method: "POST",
path: FILTER_PATH,
data: { filter_id: "f1lt3r" }
data: { filter_id: "f1lt3r" },
};
var SYNC_DATA = {
const SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {}
rooms: {},
};
var SYNC_RESPONSE = {
const SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: SYNC_DATA
data: SYNC_DATA,
};
var httpLookups = [
let httpLookups = [
// items are objects which look like:
// {
// method: "GET",
@@ -51,13 +58,14 @@ describe("MatrixClient", function() {
// }
// items are popped off when processed and block if no items left.
];
var pendingLookup = null;
let acceptKeepalives;
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH) {
return q();
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return Promise.resolve();
}
var next = httpLookups.shift();
var logLine = (
const next = httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
@@ -72,20 +80,20 @@ describe("MatrixClient", function() {
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
method + " " + path
method + " " + path,
);
}
pendingLookup = {
promise: q.defer().promise,
promise: Promise.defer().promise,
method: method,
path: path
path: path,
};
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
console.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response"
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
@@ -101,31 +109,35 @@ describe("MatrixClient", function() {
}
if (next.error) {
return q.reject({
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error
data: next.error,
});
}
return q(next.data);
return Promise.resolve(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
return q.defer().promise;
return Promise.defer().promise;
}
beforeEach(function() {
utils.beforeEach(this);
jasmine.Clock.useMock();
scheduler = jasmine.createSpyObj("scheduler", [
utils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction"
]);
store = jasmine.createSpyObj("store", [
"setProcessFunction",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
]);
"save", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
"getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
@@ -133,18 +145,20 @@ describe("MatrixClient", function() {
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: userId
userId: userId,
});
// FIXME: We shouldn't be yanking _http like this.
client._http = jasmine.createSpyObj("httpApi", [
client._http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent"
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq);
// set reasonable working defaults
acceptKeepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
@@ -153,16 +167,17 @@ describe("MatrixClient", function() {
});
afterEach(function() {
clock.uninstall();
// need to re-stub the requests with NOPs because there are no guarantees
// clients from previous tests will be GC'd before the next test. This
// means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.andCallFake(function() {
return q.defer().promise;
client._http.authedRequest.andCall(function() {
return Promise.defer().promise;
});
client._http.authedRequestWithPrefix.andCallFake(function() {
return q.defer().promise;
client._http.authedRequestWithPrefix.andCall(function() {
return Promise.defer().promise;
});
});
@@ -170,9 +185,9 @@ describe("MatrixClient", function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
var filterId = "ehfewf";
const filterId = "ehfewf";
store.getFilterIdByName.andReturn(filterId);
var filter = new sdk.Filter(0, filterId);
const filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter);
client.startClient();
@@ -187,9 +202,8 @@ describe("MatrixClient", function() {
});
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
expect(client.getSyncState()).toBeNull();
expect(client.getSyncState()).toBe(null);
});
it("should return the same sync state as emitted sync events", function(done) {
@@ -204,6 +218,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 : "");
}
const invalidFilterId = 'invalidF1lt3r';
httpLookups = [];
httpLookups.push({
method: "GET",
path: FILTER_PATH + '/' + invalidFilterId,
error: {
errcode: "M_UNKNOWN",
name: "M_UNKNOWN",
message: "No row found",
data: { errcode: "M_UNKNOWN", error: "No row found" },
httpStatus: 404,
},
});
httpLookups.push(FILTER_RESPONSE);
store.getFilterIdByName.andReturn(invalidFilterId);
const filterName = getFilterName(client.credentials.userId);
client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new sdk.Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
done();
});
});
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
@@ -214,7 +266,7 @@ describe("MatrixClient", function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -223,7 +275,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -237,19 +289,21 @@ describe("MatrixClient", function() {
it("should work on /sync", function(done) {
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
method: "GET", path: "/sync", data: SYNC_DATA,
});
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false"
true, "retryImmediately returned false",
);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
clock.tick(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -261,7 +315,7 @@ describe("MatrixClient", function() {
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
@@ -271,7 +325,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -285,12 +339,11 @@ describe("MatrixClient", function() {
});
describe("emitted sync events", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
var expected = expectedStates.shift();
const expected = expectedStates.shift();
console.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
);
if (!expected) {
done();
@@ -303,23 +356,23 @@ describe("MatrixClient", function() {
done();
}
// standard retry time is 5 to 10 seconds
jasmine.Clock.tick(10000);
clock.tick(10000);
};
}
it("should transition null -> PREPARED after the first /sync", function(done) {
var expectedStates = [];
const expectedStates = [];
expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition null -> ERROR after a failed /filter", function(done) {
var expectedStates = [];
const expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
});
expectedStates.push(["ERROR", null]);
client.on("sync", syncChecker(expectedStates, done));
@@ -328,25 +381,34 @@ describe("MatrixClient", function() {
it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
var expectedStates = [];
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["ERROR", null]);
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
var expectedStates = [];
const expectedStates = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done));
@@ -354,23 +416,29 @@ describe("MatrixClient", function() {
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
var expectedStates = [];
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
var expectedStates = [];
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
@@ -383,7 +451,7 @@ describe("MatrixClient", function() {
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
var expectedStates = [];
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -394,18 +462,25 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
var expectedStates = [];
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
@@ -413,7 +488,7 @@ describe("MatrixClient", function() {
});
describe("inviteByEmail", function() {
var roomId = "!foo:bar";
const roomId = "!foo:bar";
it("should send an invite HTTP POST", function() {
httpLookups = [{
@@ -423,17 +498,15 @@ describe("MatrixClient", function() {
expectBody: {
id_server: identityServerDomain,
medium: "email",
address: "alice@gmail.com"
}
address: "alice@gmail.com",
},
}];
client.inviteByEmail(roomId, "alice@gmail.com");
expect(httpLookups.length).toEqual(0);
});
});
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filter
httpLookups.push({
@@ -442,7 +515,7 @@ describe("MatrixClient", function() {
data: SYNC_DATA,
thenCall: function() {
done();
}
},
});
client.setGuest(true);
client.startClient();
+78 -76
View File
@@ -1,33 +1,35 @@
"use strict";
var PushProcessor = require("../../lib/pushprocessor");
var MatrixEvent = MatrixEvent;
var utils = require("../test-utils");
import 'source-map-support/register';
const PushProcessor = require("../../lib/pushprocessor");
const utils = require("../test-utils");
import expect from 'expect';
describe('NotificationService', function() {
var testUserId = "@ali:matrix.org";
var testDisplayName = "Alice M";
var testRoomId = "!fl1bb13:localhost";
const testUserId = "@ali:matrix.org";
const testDisplayName = "Alice M";
const testRoomId = "!fl1bb13:localhost";
var testEvent;
let testEvent;
var pushProcessor;
let pushProcessor;
// These would be better if individual rules were configured in the tests themselves.
var matrixClient = {
const matrixClient = {
getRoom: function() {
return {
currentState: {
getMember: function() {
return {
name: testDisplayName
name: testDisplayName,
};
},
members: {}
}
members: {},
},
};
},
credentials: {
userId: testUserId
userId: testUserId,
},
pushRules: {
"device": {},
@@ -38,91 +40,91 @@ describe('NotificationService', function() {
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "ali",
"rule_id": ".m.rule.contains_user_name"
"rule_id": ".m.rule.contains_user_name",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "coffee",
"rule_id": "coffee"
"rule_id": "coffee",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "foo*bar",
"rule_id": "foobar"
"rule_id": "foobar",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "p[io]ng",
"rule_id": "pingpong"
"rule_id": "pingpong",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "I ate [0-9] pies",
"rule_id": "pies"
"rule_id": "pies",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "b[!ai]ke",
"rule_id": "bakebike"
}
"rule_id": "bakebike",
},
],
"override": [
{
@@ -130,70 +132,70 @@ describe('NotificationService', function() {
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"conditions": [
{
"kind": "contains_display_name"
}
"kind": "contains_display_name",
},
],
"enabled": true,
"rule_id": ".m.rule.contains_display_name"
"rule_id": ".m.rule.contains_display_name",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
"value": "default",
},
],
"conditions": [
{
"is": "2",
"kind": "room_member_count"
}
"kind": "room_member_count",
},
],
"enabled": true,
"rule_id": ".m.rule.room_one_to_one"
}
"rule_id": ".m.rule.room_one_to_one",
},
],
"room": [],
"sender": [],
"underride": [
{
"actions": [
"dont-notify"
"dont-notify",
],
"conditions": [
{
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
}
"pattern": "m.notice",
},
],
"enabled": true,
"rule_id": ".m.rule.suppress_notices"
"rule_id": ".m.rule.suppress_notices",
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
"value": false,
},
],
"conditions": [],
"enabled": true,
"rule_id": ".m.rule.fallback"
}
]
}
}
"rule_id": ".m.rule.fallback",
},
],
},
},
};
beforeEach(function() {
@@ -204,8 +206,8 @@ describe('NotificationService', function() {
event: true,
content: {
body: "",
msgtype: "m.text"
}
msgtype: "m.text",
},
});
pushProcessor = new PushProcessor(matrixClient);
});
@@ -214,25 +216,25 @@ describe('NotificationService', function() {
it('should bing on a user ID.', function() {
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID with an @.', function() {
testEvent.event.content.body = "Hello @ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID without @.', function() {
testEvent.event.content.body = "Hello ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive user ID.', function() {
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -240,13 +242,13 @@ describe('NotificationService', function() {
it('should bing on a display name.', function() {
testEvent.event.content.body = "Hello Alice M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive display name.', function() {
testEvent.event.content.body = "Hello ALICE M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -254,25 +256,25 @@ describe('NotificationService', function() {
it('should bing on a bing word.', function() {
testEvent.event.content.body = "I really like coffee";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on case-insensitive bing words.', function() {
testEvent.event.content.body = "Coffee is great";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on wildcard (.*) bing words.', function() {
testEvent.event.content.body = "It was foomahbar I think.";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character group ([abc]) bing words.', function() {
testEvent.event.content.body = "Ping!";
var actions = pushProcessor.actionsForEvent(testEvent);
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent);
@@ -281,13 +283,13 @@ describe('NotificationService', function() {
it('should bing on character range ([a-z]) bing words.', function() {
testEvent.event.content.body = "I ate 6 pies";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character negation ([!a]) bing words.', function() {
testEvent.event.content.body = "boke";
var actions = pushProcessor.actionsForEvent(testEvent);
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent);
@@ -298,7 +300,7 @@ describe('NotificationService', function() {
it('should gracefully handle bad input.', function() {
testEvent.event.content.body = { "foo": "bar" };
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
});
+64 -59
View File
@@ -1,97 +1,100 @@
"use strict";
var callbacks = require("../../lib/realtime-callbacks");
var test_utils = require("../test-utils.js");
import 'source-map-support/register';
const callbacks = require("../../lib/realtime-callbacks");
const testUtils = require("../test-utils.js");
import expect from 'expect';
import lolex from 'lolex';
describe("realtime-callbacks", function() {
var clock = jasmine.Clock;
var fakeDate;
let clock;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
fakeDate += millis;
clock.tick(millis);
}
beforeEach(function() {
test_utils.beforeEach(this);
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() { return fakeDate; });
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
const fakeDate = clock.Date;
callbacks.setNow(fakeDate.now.bind(fakeDate));
});
afterEach(function() {
callbacks.setNow();
clock.uninstall();
});
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
tick(100);
expect(callback).toHaveBeenCalled();
});
it("should default to a zero timeout", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should pass any parameters to the callback", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 0, "a", "b", "c");
tick(0);
expect(callback).toHaveBeenCalledWith("a", "b", "c");
});
it("should set 'this' to the global object", function() {
var callback = jasmine.createSpy();
callback.andCallFake(function() {
expect(this).toBe(global);
expect(this.console).toBeDefined();
});
let passed = false;
const callback = function() {
expect(this).toBe(global); // eslint-disable-line no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line no-invalid-this
passed = true;
};
callbacks.setTimeout(callback);
tick(0);
expect(callback).toHaveBeenCalled();
expect(passed).toBe(true);
});
it("should handle timeouts of several seconds", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
for (var i = 0; i < 4; i++) {
expect(callback).toNotHaveBeenCalled();
for (let i = 0; i < 4; i++) {
tick(500);
}
expect(callback).toHaveBeenCalled();
});
it("should call multiple callbacks in the right order", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
var callback3 = jasmine.createSpy("callback3");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
const callback3 = expect.createSpy();
callbacks.setTimeout(callback2, 200);
callbacks.setTimeout(callback1, 100);
callbacks.setTimeout(callback3, 300);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
@@ -99,81 +102,83 @@ describe("realtime-callbacks", function() {
});
it("should treat -ve timeouts the same as a zero timeout", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
// check that cb1 is called before cb2
callback1.andCallFake(function() {
expect(callback2).not.toHaveBeenCalled();
callback1.andCall(function() {
expect(callback2).toNotHaveBeenCalled();
});
callbacks.setTimeout(callback1);
callbacks.setTimeout(callback2, -100);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should not get confused by chained calls", function() {
var callback2 = jasmine.createSpy("callback2");
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
const callback2 = expect.createSpy();
const callback1 = expect.createSpy();
callback1.andCall(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
});
callbacks.setTimeout(callback1);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
// the fake timer won't actually run callbacks registered during
// one tick until the next tick.
tick(1);
expect(callback2).toHaveBeenCalled();
});
it("should be immune to exceptions", function() {
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
const callback1 = expect.createSpy();
callback1.andCall(function() {
throw new Error("prepare to die");
});
var callback2 = jasmine.createSpy("callback2");
const callback2 = expect.createSpy();
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
});
describe("cancelTimeout", function() {
it("should cancel a pending timeout", function() {
var callback = jasmine.createSpy();
var k = callbacks.setTimeout(callback);
const callback = expect.createSpy();
const k = callbacks.setTimeout(callback);
callbacks.clearTimeout(k);
tick(0);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
});
it("should not affect sooner timeouts", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
callbacks.setTimeout(callback1, 100);
var k = callbacks.setTimeout(callback2, 200);
const k = callbacks.setTimeout(callback2, 200);
callbacks.clearTimeout(k);
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(150);
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
});
});
});
+60 -58
View File
@@ -1,22 +1,25 @@
"use strict";
var sdk = require("../..");
var RoomMember = sdk.RoomMember;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import expect from 'expect';
describe("RoomMember", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bertha:bar";
var userC = "@clarissa:bar";
var member;
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
const userC = "@clarissa:bar";
let member;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
member = new RoomMember(roomId, userA);
});
describe("getAvatarUrl", function() {
var hsUrl = "https://my.home.server";
const hsUrl = "https://my.home.server";
it("should return the URL from m.room.member preferentially", function() {
member.events.member = utils.mkEvent({
@@ -27,31 +30,31 @@ describe("RoomMember", function() {
user: userA,
content: {
membership: "join",
avatar_url: "mxc://flibble/wibble"
}
avatar_url: "mxc://flibble/wibble",
},
});
var url = member.getAvatarUrl(hsUrl);
const url = member.getAvatarUrl(hsUrl);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
"was no m.room.member event", function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
expect(url.indexOf("http")).toEqual(0); // don't care about form
});
it("should return nothing if there is no m.room.member and allowDefault=false",
function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
expect(url).toEqual(null);
});
});
describe("setPowerLevelEvent", function() {
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
@@ -59,16 +62,16 @@ describe("RoomMember", function() {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10 // shouldn't barf on this.
}
"@invalid:user": 10, // shouldn't barf on this.
},
},
event: true
event: true,
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(member.powerLevelNorm).toEqual(10);
var memberB = new RoomMember(roomId, userB);
const memberB = new RoomMember(roomId, userB);
memberB.setPowerLevelEvent(event);
expect(memberB.powerLevel).toEqual(200);
expect(memberB.powerLevelNorm).toEqual(100);
@@ -76,7 +79,7 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.powerLevel' if the power level changes.",
function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
@@ -84,12 +87,12 @@ describe("RoomMember", function() {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10 // shouldn't barf on this.
}
"@invalid:user": 10, // shouldn't barf on this.
},
},
event: true
event: true,
});
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
@@ -105,7 +108,7 @@ describe("RoomMember", function() {
it("should honour power levels of zero.",
function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
@@ -113,11 +116,11 @@ describe("RoomMember", function() {
users_default: 20,
users: {
"@alice:bar": 0,
}
},
},
event: true
event: true,
});
var emitCount = 0;
let emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
@@ -138,21 +141,21 @@ describe("RoomMember", function() {
describe("setTypingEvent", function() {
it("should set 'typing'", function() {
member.typing = false;
var memberB = new RoomMember(roomId, userB);
const memberB = new RoomMember(roomId, userB);
memberB.typing = true;
var memberC = new RoomMember(roomId, userC);
const memberC = new RoomMember(roomId, userC);
memberC.typing = true;
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.typing",
user: userA,
room: roomId,
content: {
user_ids: [
userA, userC
]
userA, userC,
],
},
event: true
event: true,
});
member.setTypingEvent(event);
memberB.setTypingEvent(event);
@@ -165,17 +168,17 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.typing' if the typing state changes",
function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.typing",
room: roomId,
content: {
user_ids: [
userA, userC
]
userA, userC,
],
},
event: true
event: true,
});
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.typing", function(ev, mem) {
expect(mem).toEqual(member);
expect(ev).toEqual(event);
@@ -190,20 +193,20 @@ describe("RoomMember", function() {
});
describe("setMembershipEvent", function() {
var joinEvent = utils.mkMembership({
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: "Alice"
name: "Alice",
});
var inviteEvent = utils.mkMembership({
const inviteEvent = utils.mkMembership({
event: true,
mship: "invite",
user: userB,
skey: userA,
room: roomId
room: roomId,
});
it("should set 'membership' and assign the event to 'events.member'.",
@@ -218,36 +221,38 @@ describe("RoomMember", function() {
it("should set 'name' based on user_id, displayname and room state",
function() {
var roomState = {
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") { return []; }
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice"
user: userC, name: "Alice",
}),
joinEvent
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
}
},
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
expect(member.name).toNotEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
expect(member.name.indexOf(userA)).toNotEqual(-1);
});
it("should emit 'RoomMember.membership' if the membership changes", function() {
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.membership", function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
@@ -260,7 +265,7 @@ describe("RoomMember", function() {
});
it("should emit 'RoomMember.name' if the name changes", function() {
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.name", function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
@@ -271,8 +276,5 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
});
});
+109 -106
View File
@@ -1,35 +1,38 @@
"use strict";
var sdk = require("../..");
var RoomState = sdk.RoomState;
var RoomMember = sdk.RoomMember;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const RoomState = sdk.RoomState;
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import expect from 'expect';
describe("RoomState", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bob:bar";
var state;
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bob:bar";
let state;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
state = new RoomState(roomId);
state.setStateEvents([
utils.mkMembership({ // userA joined
event: true, mship: "join", user: userA, room: roomId
event: true, mship: "join", user: userA, room: roomId,
}),
utils.mkMembership({ // userB joined
event: true, mship: "join", user: userB, room: roomId
event: true, mship: "join", user: userB, room: roomId,
}),
utils.mkEvent({ // Room name is "Room name goes here"
type: "m.room.name", user: userA, room: roomId, event: true, content: {
name: "Room name goes here"
}
name: "Room name goes here",
},
}),
utils.mkEvent({ // Room creation
type: "m.room.create", user: userA, room: roomId, event: true, content: {
creator: userA
}
})
creator: userA,
},
}),
]);
});
@@ -40,11 +43,11 @@ describe("RoomState", function() {
});
it("should return a member for each m.room.member event", function() {
var members = state.getMembers();
const members = state.getMembers();
expect(members.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(members[0].userId)).not.toEqual(-1);
expect([userA, userB].indexOf(members[1].userId)).not.toEqual(-1);
expect([userA, userB].indexOf(members[0].userId)).toNotEqual(-1);
expect([userA, userB].indexOf(members[1].userId)).toNotEqual(-1);
});
});
@@ -54,19 +57,19 @@ describe("RoomState", function() {
});
it("should return a member if they exist", function() {
expect(state.getMember(userB)).toBeDefined();
expect(state.getMember(userB)).toBeTruthy();
});
it("should return a member which changes as state changes", function() {
var member = state.getMember(userB);
const member = state.getMember(userB);
expect(member.membership).toEqual("join");
expect(member.name).toEqual(userB);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userB, mship: "leave", event: true,
name: "BobGone"
})
name: "BobGone",
}),
]);
expect(member.membership).toEqual("leave");
@@ -81,14 +84,14 @@ describe("RoomState", function() {
it("should return a member which doesn't change when the state is updated",
function() {
var preLeaveUser = state.getSentinelMember(userA);
const preLeaveUser = state.getSentinelMember(userA);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userA, mship: "leave", event: true,
name: "AliceIsGone"
})
name: "AliceIsGone",
}),
]);
var postLeaveUser = state.getSentinelMember(userA);
const postLeaveUser = state.getSentinelMember(userA);
expect(preLeaveUser.membership).toEqual("join");
expect(preLeaveUser.name).toEqual(userA);
@@ -111,33 +114,33 @@ describe("RoomState", function() {
it("should return a list of matching events if no state_key was specified",
function() {
var events = state.getStateEvents("m.room.member");
const events = state.getStateEvents("m.room.member");
expect(events.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
expect([userA, userB].indexOf(events[0].getStateKey())).toNotEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).toNotEqual(-1);
});
it("should return a single MatrixEvent if a state_key was specified",
function() {
var event = state.getStateEvents("m.room.member", userA);
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toEqual({
membership: "join"
membership: "join",
});
});
});
describe("setStateEvents", function() {
it("should emit 'RoomState.members' for each m.room.member event", function() {
var memberEvents = [
const memberEvents = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
}),
utils.mkMembership({
user: "@daisy:bar", mship: "join", room: roomId, event: true
})
user: "@daisy:bar", mship: "join", room: roomId, event: true,
}),
];
var emitCount = 0;
let emitCount = 0;
state.on("RoomState.members", function(ev, st, mem) {
expect(ev).toEqual(memberEvents[emitCount]);
expect(st).toEqual(state);
@@ -149,15 +152,15 @@ describe("RoomState", function() {
});
it("should emit 'RoomState.newMember' for each new member added", function() {
var memberEvents = [
const memberEvents = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
}),
utils.mkMembership({
user: "@daisy:bar", mship: "join", room: roomId, event: true
})
user: "@daisy:bar", mship: "join", room: roomId, event: true,
}),
];
var emitCount = 0;
let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet
@@ -168,21 +171,21 @@ describe("RoomState", function() {
});
it("should emit 'RoomState.events' for each state event", function() {
var events = [
const events = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
}),
utils.mkEvent({
user: userB, room: roomId, type: "m.room.topic", event: true,
content: {
topic: "boo!"
}
topic: "boo!",
},
}),
utils.mkMessage({ // Not a state event
user: userA, room: roomId, event: true
})
user: userA, room: roomId, event: true,
}),
];
var emitCount = 0;
let emitCount = 0;
state.on("RoomState.events", function(ev, st) {
expect(ev).toEqual(events[emitCount]);
expect(st).toEqual(state);
@@ -198,39 +201,39 @@ describe("RoomState", function() {
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
var powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25
}
});
state.setStateEvents([powerLevelEvent]);
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent
);
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent
);
});
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
var userC = "@cleo:bar";
var memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true
});
var powerLevelEvent = utils.mkEvent({
const powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25,
users: {}
}
},
});
state.setStateEvents([powerLevelEvent]);
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent,
);
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent,
);
});
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
const userC = "@cleo:bar";
const memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true,
});
const powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25,
users: {},
},
});
state.setStateEvents([powerLevelEvent]);
@@ -238,7 +241,7 @@ describe("RoomState", function() {
// TODO: We do this because we don't DI the RoomMember constructor
// so we can't inject a mock :/ so we have to infer.
expect(state.members[userC]).toBeDefined();
expect(state.members[userC]).toBeTruthy();
expect(state.members[userC].powerLevel).toEqual(10);
});
@@ -247,24 +250,24 @@ describe("RoomState", function() {
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
var memberEvent = utils.mkMembership({
user: userB, mship: "leave", room: roomId, event: true
const memberEvent = utils.mkMembership({
user: userB, mship: "leave", room: roomId, event: true,
});
state.setStateEvents([memberEvent]);
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
expect(state.members[userA].setMembershipEvent).toNotHaveBeenCalled();
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(
memberEvent, state
memberEvent, state,
);
});
});
describe("setTypingEvent", function() {
it("should call setTypingEvent on each RoomMember", function() {
var typingEvent = utils.mkEvent({
const typingEvent = utils.mkEvent({
type: "m.typing", room: roomId, event: true, content: {
user_ids: [userA]
}
user_ids: [userA],
},
});
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
@@ -272,10 +275,10 @@ describe("RoomState", function() {
state.setTypingEvent(typingEvent);
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
typingEvent
typingEvent,
);
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
typingEvent
typingEvent,
);
});
});
@@ -284,7 +287,7 @@ describe("RoomState", function() {
it("should say non-joined members may not send state",
function() {
expect(state.maySendStateEvent(
'm.room.name', "@nobody:nowhere"
'm.room.name', "@nobody:nowhere",
)).toEqual(false);
});
@@ -296,15 +299,15 @@ describe("RoomState", function() {
it("should say members with power >=50 may send state with power level event " +
"but no state default",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
// state_default: 50, "intentionally left blank"
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 50;
@@ -316,15 +319,15 @@ describe("RoomState", function() {
it("should obey state_default",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 30;
powerLevelEvent.content.users[userB] = 29;
@@ -337,18 +340,18 @@ describe("RoomState", function() {
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 76
"m.room.other_thing": 76,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 80;
powerLevelEvent.content.users[userB] = 50;
@@ -367,7 +370,7 @@ describe("RoomState", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere"
'm.room.message', "@nobody:nowhere",
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
});
@@ -380,15 +383,15 @@ describe("RoomState", function() {
it("should obey events_default",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
@@ -404,18 +407,18 @@ describe("RoomState", function() {
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33
"m.room.other_thing": 33,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
+309 -305
View File
File diff suppressed because it is too large Load Diff
+82 -69
View File
@@ -1,25 +1,33 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixScheduler = sdk.MatrixScheduler;
var MatrixError = sdk.MatrixError;
var utils = require("../test-utils");
// This file had a function whose name is all caps, which displeases eslint
/* eslint new-cap: "off" */
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixScheduler = sdk.MatrixScheduler;
const MatrixError = sdk.MatrixError;
const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
describe("MatrixScheduler", function() {
var scheduler;
var retryFn, queueFn;
var defer;
var roomId = "!foo:bar";
var eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true
let clock;
let scheduler;
let retryFn;
let queueFn;
let defer;
const roomId = "!foo:bar";
const eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true,
});
var eventB = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true
const eventB = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true,
});
beforeEach(function() {
utils.beforeEach(this);
jasmine.Clock.useMock();
utils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {
return retryFn(ev, attempts, err);
@@ -33,7 +41,11 @@ describe("MatrixScheduler", function() {
});
retryFn = null;
queueFn = null;
defer = q.defer();
defer = Promise.defer();
});
afterEach(function() {
clock.uninstall();
});
it("should process events in a queue in a FIFO manner", function(done) {
@@ -43,15 +55,14 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "one_big_queue";
};
var deferA = q.defer();
var deferB = q.defer();
var resolvedA = false;
const deferA = Promise.defer();
const deferB = Promise.defer();
let resolvedA = false;
scheduler.setProcessFunction(function(event) {
if (resolvedA) {
expect(event).toEqual(eventB);
return deferB.promise;
}
else {
} else {
expect(event).toEqual(eventA);
return deferA.promise;
}
@@ -68,24 +79,25 @@ describe("MatrixScheduler", function() {
it("should invoke the retryFn on failure and wait the amount of time specified",
function(done) {
var waitTimeMs = 1500;
var retryDefer = q.defer();
const waitTimeMs = 1500;
const retryDefer = Promise.defer();
retryFn = function() {
retryDefer.resolve();
return waitTimeMs;
};
queueFn = function() { return "yep"; };
queueFn = function() {
return "yep";
};
var procCount = 0;
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
if (procCount === 1) {
expect(ev).toEqual(eventA);
return defer.promise;
}
else if (procCount === 2) {
} else if (procCount === 2) {
// don't care about this defer
return q.defer().promise;
return Promise.defer().promise;
}
expect(procCount).toBeLessThan(3);
});
@@ -95,7 +107,7 @@ describe("MatrixScheduler", function() {
defer.reject({});
retryDefer.promise.done(function() {
expect(procCount).toEqual(1);
jasmine.Clock.tick(waitTimeMs);
clock.tick(waitTimeMs);
expect(procCount).toEqual(2);
done();
});
@@ -109,25 +121,26 @@ describe("MatrixScheduler", function() {
retryFn = function() {
return -1;
};
queueFn = function() { return "yep"; };
queueFn = function() {
return "yep";
};
var deferA = q.defer();
var deferB = q.defer();
var procCount = 0;
const deferA = Promise.defer();
const deferB = Promise.defer();
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
if (procCount === 1) {
expect(ev).toEqual(eventA);
return deferA.promise;
}
else if (procCount === 2) {
} else if (procCount === 2) {
expect(ev).toEqual(eventB);
return deferB.promise;
}
expect(procCount).toBeLessThan(3);
});
var globalA = scheduler.queueEvent(eventA);
const globalA = scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
expect(procCount).toEqual(1);
@@ -145,10 +158,10 @@ describe("MatrixScheduler", function() {
// Expect to have processFn invoked for A&B.
// Resolve A.
// Expect to have processFn invoked for D.
var eventC = utils.mkMessage({user: "@a:bar", room: roomId, event: true});
var eventD = utils.mkMessage({user: "@b:bar", room: roomId, event: true});
const eventC = utils.mkMessage({user: "@a:bar", room: roomId, event: true});
const eventD = utils.mkMessage({user: "@b:bar", room: roomId, event: true});
var buckets = {};
const buckets = {};
buckets[eventA.getId()] = "queue_A";
buckets[eventD.getId()] = "queue_A";
buckets[eventB.getId()] = "queue_B";
@@ -161,12 +174,12 @@ describe("MatrixScheduler", function() {
return buckets[event.getId()];
};
var expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId()
const expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId(),
];
var deferA = q.defer();
const deferA = Promise.defer();
scheduler.setProcessFunction(function(event) {
var id = expectOrder.shift();
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
if (expectOrder.length === 0) {
done();
@@ -182,7 +195,7 @@ describe("MatrixScheduler", function() {
setTimeout(function() {
deferA.resolve({});
}, 1000);
jasmine.Clock.tick(1000);
clock.tick(1000);
});
describe("queueEvent", function() {
@@ -197,9 +210,9 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "yep";
};
var prom = scheduler.queueEvent(eventA);
expect(prom).toBeDefined();
expect(prom.then).toBeDefined();
const prom = scheduler.queueEvent(eventA);
expect(prom).toBeTruthy();
expect(prom.then).toBeTruthy();
});
});
@@ -208,14 +221,14 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return null;
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
expect(scheduler.getQueueForEvent(eventA)).toBe(null);
});
it("should return null if the mapped queue doesn't exist", function() {
queueFn = function() {
return "yep";
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
expect(scheduler.getQueueForEvent(eventA)).toBe(null);
});
it("should return a list of events in the queue and modifications to" +
@@ -225,15 +238,15 @@ describe("MatrixScheduler", function() {
};
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
var queue = scheduler.getQueueForEvent(eventA);
const queue = scheduler.getQueueForEvent(eventA);
expect(queue.length).toEqual(2);
expect(queue).toEqual([eventA, eventB]);
// modify the queue
var eventC = utils.mkMessage(
{user: "@a:bar", room: roomId, event: true}
const eventC = utils.mkMessage(
{user: "@a:bar", room: roomId, event: true},
);
queue.push(eventC);
var queueAgain = scheduler.getQueueForEvent(eventA);
const queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain.length).toEqual(2);
});
@@ -244,9 +257,9 @@ describe("MatrixScheduler", function() {
};
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
var queue = scheduler.getQueueForEvent(eventA);
const queue = scheduler.getQueueForEvent(eventA);
queue[1].event.content.body = "foo";
var queueAgain = scheduler.getQueueForEvent(eventA);
const queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain[1].event.content.body).toEqual("foo");
});
});
@@ -280,7 +293,7 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "yep";
};
var procCount = 0;
let procCount = 0;
scheduler.queueEvent(eventA);
scheduler.setProcessFunction(function(ev) {
procCount += 1;
@@ -294,7 +307,7 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "yep";
};
var procCount = 0;
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
return defer.promise;
@@ -308,41 +321,41 @@ describe("MatrixScheduler", function() {
expect(MatrixScheduler.QUEUE_MESSAGES(eventA)).toEqual("message");
expect(MatrixScheduler.QUEUE_MESSAGES(
utils.mkMembership({
user: "@alice:bar", room: roomId, mship: "join", event: true
})
user: "@alice:bar", room: roomId, mship: "join", event: true,
}),
)).toEqual(null);
});
});
describe("RETRY_BACKOFF_RATELIMIT", function() {
it("should wait at least the time given on M_LIMIT_EXCEEDED", function() {
var res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
const res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 1, new MatrixError({
errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 5000
})
errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 5000,
}),
);
expect(res >= 500).toBe(true, "Didn't wait long enough.");
});
it("should give up after 5 attempts", function() {
var res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 5, {}
const res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 5, {},
);
expect(res).toBe(-1, "Didn't give up.");
});
it("should do exponential backoff", function() {
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 1, {}
eventA, 1, {},
)).toEqual(2000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 2, {}
eventA, 2, {},
)).toEqual(4000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 3, {}
eventA, 3, {},
)).toEqual(8000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 4, {}
eventA, 4, {},
)).toEqual(16000);
});
});
+356
View File
@@ -0,0 +1,356 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
import utils from "../test-utils";
import sdk from "../..";
import expect from 'expect';
const SyncAccumulator = sdk.SyncAccumulator;
describe("SyncAccumulator", function() {
let sa;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
sa = new SyncAccumulator({
maxTimelineEntries: 10,
});
});
it("should return the same /sync response if accumulated exactly once", () => {
// technically cheating since we also cheekily pre-populate keys we
// know that the sync accumulator will pre-populate.
// It isn't 100% transitive.
const res = {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [
member("alice", "join"),
member("bob", "join"),
],
},
timeline: {
events: [msg("alice", "hi")],
prev_batch: "something",
},
},
},
},
};
sa.accumulate(res);
const output = sa.getJSON();
expect(output.nextBatch).toEqual(res.next_batch);
expect(output.roomsData).toEqual(res.rooms);
});
it("should prune the timeline to the oldest prev_batch within the limit", () => {
// maxTimelineEntries is 10 so we should get back all
// 10 timeline messages with a prev_batch of "pinned_to_1"
sa.accumulate(syncSkeleton({
state: { events: [member("alice", "join")] },
timeline: {
events: [
msg("alice", "1"),
msg("alice", "2"),
msg("alice", "3"),
msg("alice", "4"),
msg("alice", "5"),
msg("alice", "6"),
msg("alice", "7"),
],
prev_batch: "pinned_to_1",
},
}));
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "8"),
],
prev_batch: "pinned_to_8",
},
}));
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "9"),
msg("alice", "10"),
],
prev_batch: "pinned_to_10",
},
}));
let output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(10);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+1));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_1");
// accumulate more messages. Now it can't have a prev_batch of "pinned to 1"
// AND give us <= 10 messages without losing messages in-between.
// It should try to find the oldest prev_batch which still fits into 10
// messages, which is "pinned to 8".
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "11"),
msg("alice", "12"),
msg("alice", "13"),
msg("alice", "14"),
msg("alice", "15"),
msg("alice", "16"),
msg("alice", "17"),
],
prev_batch: "pinned_to_11",
},
}));
output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(10);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+8));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_8");
});
it("should remove the stored timeline on limited syncs", () => {
sa.accumulate(syncSkeleton({
state: { events: [member("alice", "join")] },
timeline: {
events: [
msg("alice", "1"),
msg("alice", "2"),
msg("alice", "3"),
],
prev_batch: "pinned_to_1",
},
}));
// some time passes and now we get a limited sync
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
limited: true,
events: [
msg("alice", "51"),
msg("alice", "52"),
msg("alice", "53"),
],
prev_batch: "pinned_to_51",
},
}));
const output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(3);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+51));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_51");
});
it("should drop typing notifications", () => {
const res = syncSkeleton({
ephemeral: {
events: [{
type: "m.typing",
content: {
user_ids: ["@alice:localhost"],
},
room_id: "!foo:bar",
}],
},
});
sa.accumulate(res);
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
).toEqual(0);
});
it("should clobber account data based on event type", () => {
const acc1 = {
type: "favourite.food",
content: {
food: "banana",
},
};
const acc2 = {
type: "favourite.food",
content: {
food: "apple",
},
};
sa.accumulate(syncSkeleton({
account_data: {
events: [acc1],
},
}));
sa.accumulate(syncSkeleton({
account_data: {
events: [acc2],
},
}));
expect(
sa.getJSON().roomsData.join["!foo:bar"].account_data.events.length,
).toEqual(1);
expect(
sa.getJSON().roomsData.join["!foo:bar"].account_data.events[0],
).toEqual(acc2);
});
it("should clobber global account data based on event type", () => {
const acc1 = {
type: "favourite.food",
content: {
food: "banana",
},
};
const acc2 = {
type: "favourite.food",
content: {
food: "apple",
},
};
sa.accumulate({
account_data: {
events: [acc1],
},
});
sa.accumulate({
account_data: {
events: [acc2],
},
});
expect(
sa.getJSON().accountData.length,
).toEqual(1);
expect(
sa.getJSON().accountData[0],
).toEqual(acc2);
});
it("should accumulate read receipts", () => {
const receipt1 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
"m.read": {
"@alice:localhost": { ts: 1 },
"@bob:localhost": { ts: 2 },
},
"some.other.receipt.type": {
"@should_be_ignored:localhost": { key: "val" },
},
},
},
};
const receipt2 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event2:localhost": {
"m.read": {
"@bob:localhost": { ts: 2 }, // clobbers event1 receipt
"@charlie:localhost": { ts: 3 },
},
},
},
};
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt1],
},
}));
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt2],
},
}));
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
).toEqual(1);
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events[0],
).toEqual({
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
"m.read": {
"@alice:localhost": { ts: 1 },
},
},
"$event2:localhost": {
"m.read": {
"@bob:localhost": { ts: 2 },
"@charlie:localhost": { ts: 3 },
},
},
},
});
});
});
function syncSkeleton(joinObj) {
joinObj = joinObj || {};
return {
next_batch: "abc",
rooms: {
join: {
"!foo:bar": joinObj,
},
},
};
}
function msg(localpart, text) {
return {
content: {
body: text,
},
origin_server_ts: 123456789,
sender: "@" + localpart + ":localhost",
type: "m.room.message",
};
}
function member(localpart, membership) {
return {
content: {
membership: membership,
},
origin_server_ts: 123456789,
state_key: "@" + localpart + ":localhost",
sender: "@" + localpart + ":localhost",
type: "m.room.member",
};
}
+125 -108
View File
@@ -1,24 +1,36 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var TimelineWindow = sdk.TimelineWindow;
var TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const TimelineWindow = sdk.TimelineWindow;
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
var utils = require("../test-utils");
const utils = require("../test-utils");
import expect from 'expect';
var ROOM_ID = "roomId";
var USER_ID = "userId";
const ROOM_ID = "roomId";
const USER_ID = "userId";
/*
* create a timeline with a bunch (default 3) events.
* baseIndex is 1 by default.
*/
function createTimeline(numEvents, baseIndex) {
if (numEvents === undefined) { numEvents = 3; }
if (baseIndex === undefined) { baseIndex = 1; }
if (numEvents === undefined) {
numEvents = 3;
}
if (baseIndex === undefined) {
baseIndex = 1;
}
var timeline = new EventTimeline(ROOM_ID);
// XXX: this is a horrid hack
const timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() {
return timelineSet;
};
const timeline = new EventTimeline(timelineSet);
// add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false);
@@ -31,12 +43,12 @@ function createTimeline(numEvents, baseIndex) {
}
function addEventsToTimeline(timeline, numEvents, atStart) {
for (var i = 0; i < numEvents; i++) {
for (let i = 0; i < numEvents; i++) {
timeline.addEvent(
utils.mkMessage({
room: ROOM_ID, user: USER_ID,
event: true,
}), atStart
}), atStart,
);
}
}
@@ -46,8 +58,8 @@ function addEventsToTimeline(timeline, numEvents, atStart) {
* create a pair of linked timelines
*/
function createLinkedTimelines() {
var tl1 = createTimeline();
var tl2 = createTimeline();
const tl1 = createTimeline();
const tl2 = createTimeline();
tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS);
tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS);
return [tl1, tl2];
@@ -56,46 +68,47 @@ function createLinkedTimelines() {
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("minIndex", function() {
it("should return the min index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.minIndex()).toEqual(-1);
});
});
describe("maxIndex", function() {
it("should return the max index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.maxIndex()).toEqual(2);
});
});
describe("advance", function() {
it("should advance up to the end of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(3);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
const result = timelineIndex.advance(3);
expect(result).toEqual(2);
expect(timelineIndex.index).toEqual(2);
});
it("should retreat back to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(-2);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
const result = timelineIndex.advance(-2);
expect(result).toEqual(-1);
expect(timelineIndex.index).toEqual(-1);
});
it("should advance into the next timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
const timelines = createLinkedTimelines();
const tl1 = timelines[0];
const tl2 = timelines[1];
// initialise the index pointing at the end of the first timeline
var timelineIndex = new TimelineIndex(tl1, 2);
const timelineIndex = new TimelineIndex(tl1, 2);
var result = timelineIndex.advance(1);
const result = timelineIndex.advance(1);
expect(result).toEqual(1);
expect(timelineIndex.timeline).toBe(tl2);
@@ -106,14 +119,15 @@ describe("TimelineIndex", function() {
});
it("should retreat into the previous timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
const timelines = createLinkedTimelines();
const tl1 = timelines[0];
const tl2 = timelines[1];
// initialise the index pointing at the start of the second
// timeline
var timelineIndex = new TimelineIndex(tl2, -1);
const timelineIndex = new TimelineIndex(tl2, -1);
var result = timelineIndex.advance(-1);
const result = timelineIndex.advance(-1);
expect(result).toEqual(-1);
expect(timelineIndex.timeline).toBe(tl1);
expect(timelineIndex.index).toEqual(1);
@@ -122,8 +136,8 @@ describe("TimelineIndex", function() {
describe("retreat", function() {
it("should retreat up to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.retreat(2);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
const result = timelineIndex.retreat(2);
expect(result).toEqual(1);
expect(timelineIndex.index).toEqual(-1);
});
@@ -133,98 +147,101 @@ 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;
let timelineSet;
let client;
function createWindow(timeline, opts) {
room = {};
timelineSet = {};
client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
return q(timeline);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
return Promise.resolve(timeline);
};
return new TimelineWindow(client, room, opts);
return new TimelineWindow(client, timelineSet, opts);
}
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("load", function() {
it("should initialise from the live timeline", function(done) {
var liveTimeline = createTimeline();
var room = {};
room.getLiveTimeline = function() { return liveTimeline; };
const liveTimeline = createTimeline();
const room = {};
room.getLiveTimeline = function() {
return liveTimeline;
};
var timelineWindow = new TimelineWindow(undefined, room);
const timelineWindow = new TimelineWindow(undefined, room);
timelineWindow.load(undefined, 2).then(function() {
var expectedEvents = liveTimeline.getEvents().slice(1);
const expectedEvents = liveTimeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should initialise from a specific event", function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
const timeline = createTimeline();
const eventId = timeline.getEvents()[1].getId();
var room = {};
var client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
const timelineSet = {};
const client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
return Promise.resolve(timeline);
};
var timelineWindow = new TimelineWindow(client, room);
const timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("canPaginate should return false until load has returned",
function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
var eventId = timeline.getEvents()[1].getId();
const eventId = timeline.getEvents()[1].getId();
var room = {};
var client = {};
const timelineSet = {};
const client = {};
var timelineWindow = new TimelineWindow(client, room);
const 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))
.toBe(false);
return q(timeline);
return Promise.resolve(timeline);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
describe("pagination", function() {
it("should be able to advance across the initial timeline",
function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var timelineWindow = createWindow(timeline);
const timeline = createTimeline();
const eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline);
timelineWindow.load(eventId, 1).then(function() {
var expectedEvents = [timeline.getEvents()[1]];
const expectedEvents = [timeline.getEvents()[1]];
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -235,7 +252,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1);
const expectedEvents = timeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -250,7 +267,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -260,16 +277,16 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should advance into next timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[0].getEvents()[1].getId();
var timelineWindow = createWindow(tls[0], {windowLimit: 5});
const tls = createLinkedTimelines();
const eventId = tls[0].getEvents()[1].getId();
const timelineWindow = createWindow(tls[0], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[0].getEvents();
const expectedEvents = tls[0].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -280,7 +297,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents()
const expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -294,7 +311,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[0]
var expectedEvents = tls[0].getEvents().slice(1)
const expectedEvents = tls[0].getEvents().slice(1)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -305,16 +322,16 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should retreat into previous timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[1].getEvents()[1].getId();
var timelineWindow = createWindow(tls[1], {windowLimit: 5});
const tls = createLinkedTimelines();
const eventId = tls[1].getEvents()[1].getId();
const timelineWindow = createWindow(tls[1], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[1].getEvents();
const expectedEvents = tls[1].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -325,7 +342,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents().slice(1, 3)
const expectedEvents = tls[0].getEvents().slice(1, 3)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -339,7 +356,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[1]
var expectedEvents = tls[0].getEvents()
const expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -350,15 +367,15 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should make forward pagination requests", function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
@@ -366,11 +383,11 @@ describe("TimelineWindow", function() {
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, false);
return q(true);
return Promise.resolve(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -380,18 +397,18 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(0, 5);
const expectedEvents = timeline.getEvents().slice(0, 5);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should make backward pagination requests", function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
@@ -399,11 +416,11 @@ describe("TimelineWindow", function() {
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, true);
return q(true);
return Promise.resolve(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -413,30 +430,30 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1, 6);
const expectedEvents = timeline.getEvents().slice(1, 6);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should limit the number of unsuccessful pagination requests",
function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
var paginateCount = 0;
let paginateCount = 0;
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
paginateCount += 1;
return q(true);
return Promise.resolve(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -447,14 +464,14 @@ describe("TimelineWindow", function() {
}).then(function(success) {
expect(success).toBe(false);
expect(paginateCount).toEqual(3);
var expectedEvents = timeline.getEvents().slice(0, 3);
const expectedEvents = timeline.getEvents().slice(0, 3);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
});
+15 -12
View File
@@ -1,30 +1,33 @@
"use strict";
var sdk = require("../..");
var User = sdk.User;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const User = sdk.User;
const utils = require("../test-utils");
import expect from 'expect';
describe("User", function() {
var userId = "@alice:bar";
var user;
const userId = "@alice:bar";
let user;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
user = new User(userId);
});
describe("setPresenceEvent", function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.presence", content: {
presence: "online",
user_id: userId,
displayname: "Alice",
last_active_ago: 1085,
avatar_url: "mxc://foo/bar"
}, event: true
avatar_url: "mxc://foo/bar",
}, event: true,
});
it("should emit 'User.displayName' if the display name changes", function() {
var emitCount = 0;
let emitCount = 0;
user.on("User.displayName", function(ev, usr) {
emitCount += 1;
});
@@ -35,7 +38,7 @@ describe("User", function() {
});
it("should emit 'User.avatarUrl' if the avatar URL changes", function() {
var emitCount = 0;
let emitCount = 0;
user.on("User.avatarUrl", function(ev, usr) {
emitCount += 1;
});
@@ -46,7 +49,7 @@ describe("User", function() {
});
it("should emit 'User.presence' if the presence changes", function() {
var emitCount = 0;
let emitCount = 0;
user.on("User.presence", function(ev, usr) {
emitCount += 1;
});
+86 -55
View File
@@ -1,40 +1,43 @@
"use strict";
var utils = require("../../lib/utils");
var testUtils = require("../test-utils");
import 'source-map-support/register';
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
import expect from 'expect';
describe("utils", function() {
beforeEach(function() {
testUtils.beforeEach(this);
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("encodeParams", function() {
it("should url encode and concat with &s", function() {
var params = {
const params = {
foo: "bar",
baz: "beer@"
baz: "beer@",
};
expect(utils.encodeParams(params)).toEqual(
"foo=bar&baz=beer%40"
"foo=bar&baz=beer%40",
);
});
});
describe("encodeUri", function() {
it("should replace based on object keys and url encode", function() {
var path = "foo/bar/%something/%here";
var vals = {
const path = "foo/bar/%something/%here";
const vals = {
"%something": "baz",
"%here": "beer@"
"%here": "beer@",
};
expect(utils.encodeUri(path, vals)).toEqual(
"foo/bar/baz/beer%40"
"foo/bar/baz/beer%40",
);
});
});
describe("forEach", function() {
it("should be invoked for each element", function() {
var arr = [];
const arr = [];
utils.forEach([55, 66, 77], function(element) {
arr.push(element);
});
@@ -44,38 +47,50 @@ describe("utils", function() {
describe("findElement", function() {
it("should find only 1 element if there is a match", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toEqual(55);
});
it("should be able to find in reverse order", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn, true)).toEqual(77);
});
it("should find nothing if the function never returns true", function() {
var matchFn = function() { return false; };
var arr = [55, 66, 77];
const matchFn = function() {
return false;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toBeFalsy();
});
});
describe("removeElement", function() {
it("should remove only 1 element if there is a match", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual([66, 77]);
});
it("should be able to remove in reverse order", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn, true);
expect(arr).toEqual([55, 66]);
});
it("should remove nothing if the function never returns true", function() {
var matchFn = function() { return false; };
var arr = [55, 66, 77];
const matchFn = function() {
return false;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual(arr);
});
@@ -92,7 +107,7 @@ describe("utils", function() {
expect(utils.isFunction(555)).toBe(false);
expect(utils.isFunction(function() {})).toBe(true);
var s = { foo: function() {} };
const s = { foo: function() {} };
expect(utils.isFunction(s.foo)).toBe(true);
});
});
@@ -113,30 +128,42 @@ describe("utils", function() {
describe("checkObjectHasKeys", function() {
it("should throw for missing keys", function() {
expect(function() { utils.checkObjectHasKeys({}, ["foo"]); }).toThrow();
expect(function() { utils.checkObjectHasKeys({
foo: "bar"
}, ["foo"]); }).not.toThrow();
expect(function() {
utils.checkObjectHasKeys({}, ["foo"]);
}).toThrow();
expect(function() {
utils.checkObjectHasKeys({
foo: "bar",
}, ["foo"]);
}).toNotThrow();
});
});
describe("checkObjectHasNoAdditionalKeys", function() {
it("should throw for extra keys", function() {
expect(function() { utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
baz: 4
}, ["foo"]); }).toThrow();
expect(function() {
utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
baz: 4,
}, ["foo"]);
}).toThrow();
expect(function() { utils.checkObjectHasNoAdditionalKeys({
foo: "bar"
}, ["foo"]); }).not.toThrow();
expect(function() {
utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
}, ["foo"]);
}).toNotThrow();
});
});
describe("deepCompare", function() {
var assert = {
isTrue: function(x) { expect(x).toBe(true); },
isFalse: function(x) { expect(x).toBe(false); },
const assert = {
isTrue: function(x) {
expect(x).toBe(true);
},
isFalse: function(x) {
expect(x).toBe(false);
},
};
it("should handle primitives", function() {
@@ -150,7 +177,7 @@ describe("utils", function() {
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
var r = /abc/;
const r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
@@ -192,8 +219,12 @@ describe("utils", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
var func = function(x) { return true; };
var func2 = function(x) { return true; };
const func = function(x) {
return true;
};
const func2 = function(x) {
return true;
};
assert.isTrue(utils.deepCompare(func, func));
assert.isFalse(utils.deepCompare(func, func2));
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
@@ -203,58 +234,58 @@ describe("utils", function() {
describe("extend", function() {
var SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
var target = {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
const sourceOrig = 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);
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should ignore null", function() {
var target = {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
const sourceOrig = 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);
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should handle properties created with defineProperties", function() {
var source = Object.defineProperties({}, {
const source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true
enumerable: true,
},
"nonenumerableProp": {
get: function() {
return true;
}
}
},
},
});
var target = {};
const target = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
-580
View File
@@ -1,580 +0,0 @@
"use strict";
var sdk = require("../..");
var WebStorageStore = sdk.WebStorageStore;
var Room = sdk.Room;
var User = sdk.User;
var utils = require("../test-utils");
var MockStorageApi = require("../MockStorageApi");
describe("WebStorageStore", function() {
var store, room;
var roomId = "!foo:bar";
var userId = "@alice:bar";
var mockStorageApi;
var batchNum = 3;
// web storage api keys
var prefix = "room_" + roomId + "_timeline_";
var stateKeyName = "room_" + roomId + "_state";
// stored state events
var stateEventMap = {
"m.room.member": {},
"m.room.name": {}
};
stateEventMap["m.room.member"][userId] = utils.mkMembership(
{user: userId, room: roomId, mship: "join"}
);
stateEventMap["m.room.name"][""] = utils.mkEvent(
{user: userId, room: roomId, type: "m.room.name",
content: {
name: "foo"
}}
);
beforeEach(function() {
utils.beforeEach(this);
mockStorageApi = new MockStorageApi();
store = new WebStorageStore(mockStorageApi, batchNum);
room = new Room(roomId);
});
describe("constructor", function() {
it("should throw if the WebStorage API functions are missing", function() {
expect(function() {
store = new WebStorageStore({}, 5);
}).toThrow();
expect(function() {
mockStorageApi.length = undefined;
store = new WebStorageStore(mockStorageApi, 5);
}).toThrow();
});
});
describe("syncToken", function() {
it("get: should return the token from the store", function() {
var token = "flibble";
store.setSyncToken(token);
expect(store.getSyncToken()).toEqual(token);
expect(mockStorageApi.length).toEqual(1);
});
it("get: should return null if the token does not exist", function() {
expect(store.getSyncToken()).toEqual(null);
expect(mockStorageApi.length).toEqual(0);
});
});
describe("storeRoom", function() {
it("should persist the room state correctly", function() {
var stateEvents = [
utils.mkEvent({
event: true, type: "m.room.create", user: userId, room: roomId,
content: {
creator: userId
}
}),
utils.mkMembership({
event: true, user: userId, room: roomId, mship: "join"
})
];
room.currentState.setStateEvents(stateEvents);
store.storeRoom(room);
var storedEvents = getItem(mockStorageApi,
"room_" + roomId + "_state"
).events;
expect(storedEvents["m.room.create"][""]).toEqual(stateEvents[0].event);
});
it("should persist timeline events correctly", function() {
var timelineEvents = [];
var entries = batchNum + batchNum - 1;
var i = 0;
for (i = 0; i < entries; i++) {
timelineEvents.push(
utils.mkMessage({room: roomId, user: userId, event: true})
);
}
room.timeline = timelineEvents;
store.storeRoom(room);
expect(getItem(mockStorageApi, prefix + "-1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "2")).toBe(null);
expect(getItem(mockStorageApi, prefix + "live")).toBe(null);
var timeline0 = getItem(mockStorageApi, prefix + "0");
var timeline1 = getItem(mockStorageApi, prefix + "1");
expect(timeline0.length).toEqual(batchNum);
expect(timeline1.length).toEqual(batchNum - 1);
for (i = 0; i < batchNum; i++) {
expect(timeline0[i]).toEqual(timelineEvents[i].event);
if ((i + batchNum) < timelineEvents.length) {
expect(timeline1[i]).toEqual(timelineEvents[i + batchNum].event);
}
}
});
it("should persist timeline events in one bucket if batchNum=0", function() {
store = new WebStorageStore(mockStorageApi, 0);
var timelineEvents = [];
var entries = batchNum + batchNum - 1;
var i = 0;
for (i = 0; i < entries; i++) {
timelineEvents.push(
utils.mkMessage({room: roomId, user: userId, event: true})
);
}
room.timeline = timelineEvents;
store.storeRoom(room);
expect(getItem(mockStorageApi, prefix + "-1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "live")).toBe(null);
var timeline = getItem(mockStorageApi, prefix + "0");
expect(timeline.length).toEqual(timelineEvents.length);
for (i = 0; i < timeline.length; i++) {
expect(timeline[i]).toEqual(
timelineEvents[i].event
);
}
});
});
describe("getRoom", function() {
// stored timeline events
var timeline0, timeline1, i;
beforeEach(function() {
timeline0 = [];
timeline1 = [];
for (i = 0; i < batchNum; i++) {
timeline1[i] = utils.mkMessage({user: userId, room: roomId});
if (i !== (batchNum - 1)) { // miss last one
timeline0[i] = utils.mkMessage({user: userId, room: roomId});
}
}
});
it("should reconstruct room state", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
var storedRoom = store.getRoom(roomId);
expect(
storedRoom.currentState.getStateEvents("m.room.name", "").event
).toEqual(stateEventMap["m.room.name"][""]);
expect(
storedRoom.currentState.getStateEvents("m.room.member", userId).event
).toEqual(stateEventMap["m.room.member"][userId]);
});
it("should reconstruct old room state", function() {
var inviteEvent = utils.mkMembership({
user: userId, room: roomId, mship: "invite"
});
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", [inviteEvent]);
var storedRoom = store.getRoom(roomId);
expect(
storedRoom.currentState.getStateEvents("m.room.member", userId).event
).toEqual(stateEventMap["m.room.member"][userId]);
expect(
storedRoom.oldState.getStateEvents("m.room.member", userId).event
).toEqual(inviteEvent);
});
it("should reconstruct the room timeline", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events
expect(storedRoom.timeline.length).toEqual(batchNum);
var timeline = timeline0.concat(timeline1);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[batchNum - 1 - i].event).toEqual(
timeline[timeline.length - 1 - i]
);
}
});
it("should sync the timeline for 'live' events " +
"(full hi batch; 1+bit live batches)", function() {
// 1 and a bit events go into _live
var timelineLive = [];
timelineLive.push(utils.mkMessage({user: userId, room: roomId}));
for (i = 0; i < batchNum; i++) {
timelineLive.push(
utils.mkMessage({user: userId, room: roomId})
);
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
setItem(mockStorageApi,
// deep copy the timeline via parse/stringify else items will
// be shift()ed from timelineLive and we can't compare!
prefix + "live", JSON.parse(JSON.stringify(timelineLive))
);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events (highest
// index of timelineLive is the newest message)
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[i].event).toEqual(
timelineLive[i + 1]
);
}
});
it("should sync the timeline for 'live' events " +
"(no low batch; 1 live batches)", function() {
var timelineLive = [];
for (i = 0; i < batchNum; i++) {
timelineLive.push(
utils.mkMessage({user: userId, room: roomId})
);
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", []);
setItem(mockStorageApi,
// deep copy the timeline via parse/stringify else items will
// be shift()ed from timelineLive and we can't compare!
prefix + "live", JSON.parse(JSON.stringify(timelineLive))
);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events (highest
// index of timelineLive is the newest message)
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[i].event).toEqual(
timelineLive[i]
);
}
});
it("should be able to reconstruct the timeline with negative indices",
function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "-5", timeline0);
setItem(mockStorageApi, prefix + "-4", timeline1);
var timeline = timeline0.concat(timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[batchNum - 1 - i].event).toEqual(
timeline[timeline.length - 1 - i]
);
}
});
it("should return null if the room doesn't exist", function() {
expect(store.getRoom("nothing")).toEqual(null);
});
it("should assign a storageToken to the Room", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom.storageToken).toBeDefined();
});
});
describe("scrollback", function() {
// stored timeline events
var timeline0, timeline1, timeline2;
beforeEach(function() {
// batch size is 3
store = new WebStorageStore(mockStorageApi, 3);
timeline0 = [
// _
utils.mkMessage({user: userId, room: roomId}), // 1 OLDEST
utils.mkMessage({user: userId, room: roomId}) // 2
];
timeline1 = [
utils.mkMessage({user: userId, room: roomId}), // 3
utils.mkMessage({user: userId, room: roomId}), // 4
utils.mkMessage({user: userId, room: roomId}) // 5
];
timeline2 = [
utils.mkMessage({user: userId, room: roomId}), // 6
utils.mkMessage({user: userId, room: roomId}), // 7
utils.mkMessage({user: userId, room: roomId}) // 8 NEWEST
];
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
setItem(mockStorageApi, prefix + "2", timeline2);
});
it("should scroll back locally giving 'limit' events", function() {
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
var events = store.scrollback(storedRoom, 3);
expect(events.length).toEqual(3);
expect(events.reverse()).toEqual(timeline1);
});
it("should give less than 'limit' events near the end of the stored timeline",
function() {
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
var events = store.scrollback(storedRoom, 7);
expect(events.length).toEqual(5);
expect(events.reverse()).toEqual(timeline0.concat(timeline1));
});
it("should progressively give older messages the more times scrollback is called",
function() {
var events;
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
events = store.scrollback(storedRoom, 2);
expect(events.reverse()).toEqual([timeline1[1], timeline1[2]]);
expect(storedRoom.timeline.length).toEqual(5);
events = store.scrollback(storedRoom, 2);
expect(events.reverse()).toEqual([timeline0[1], timeline1[0]]);
expect(storedRoom.timeline.length).toEqual(7);
events = store.scrollback(storedRoom, 2);
expect(events).toEqual([timeline0[0]]);
expect(storedRoom.timeline.length).toEqual(8);
events = store.scrollback(storedRoom, 2);
expect(events).toEqual([]);
expect(storedRoom.timeline.length).toEqual(8);
});
it("should give 0 events if there is no token on the room", function() {
var r = new Room(roomId);
expect(store.scrollback(r, 3)).toEqual([]);
});
it("should give 0 events for unknown rooms", function() {
var r = new Room("!unknown:room");
r.storageToken = "foo";
expect(store.scrollback(r, 3)).toEqual([]);
});
it("should give 0 events if the boundary event is the last in the timeline",
function() {
var events;
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
// go up to the boundary (8 messages total)
events = store.scrollback(storedRoom, 5);
expect(events.length).toEqual(5);
events = store.scrollback(storedRoom, 5);
expect(events.length).toEqual(0);
});
});
describe("storeEvents", function() {
var timeline0, i;
beforeEach(function() {
timeline0 = [];
for (i = 0; i < batchNum; i++) {
timeline0.push(utils.mkMessage({user: userId, room: roomId}));
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
});
it("should add to the live batch", function() {
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(2);
expect(liveEvents[0]).toEqual(events[0].event);
expect(liveEvents[1]).toEqual(events[1].event);
});
it("should preserve existing live events in the store", function() {
var existingEvent = utils.mkMessage({user: userId, room: roomId});
setItem(mockStorageApi, prefix + "live", [existingEvent]);
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(3);
expect(liveEvents[0]).toEqual(existingEvent);
expect(liveEvents[1]).toEqual(events[0].event);
expect(liveEvents[2]).toEqual(events[1].event);
});
it("should add to the lowest batch index if toStart=true", function() {
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken", true);
var timelineNeg1 = getItem(mockStorageApi, prefix + "-1");
expect(timelineNeg1.length).toEqual(2);
expect(timelineNeg1[0]).toEqual(events[1].event);
expect(timelineNeg1[1]).toEqual(events[0].event);
});
it("should add multiple batches to the lowest batch index if toStart=true",
function() {
var timelineNeg1 = [];
var timelineNeg2 = [];
for (i = 0; i < batchNum; i++) {
timelineNeg1.push(
utils.mkMessage({user: userId, room: roomId, event: true})
);
timelineNeg2.push(
utils.mkMessage({user: userId, room: roomId, event: true})
);
}
var events = timelineNeg2.concat(timelineNeg1).reverse();
store.storeEvents(room, events, "atoken", true);
var storedNeg1 = getItem(mockStorageApi, prefix + "-1");
var storedNeg2 = getItem(mockStorageApi, prefix + "-2");
expect(timelineNeg1.length).toEqual(storedNeg1.length);
expect(timelineNeg2.length).toEqual(storedNeg2.length);
for (i = 0; i < timelineNeg1.length; i++) {
expect(timelineNeg1[i].event).toEqual(storedNeg1[i]);
expect(timelineNeg2[i].event).toEqual(storedNeg2[i]);
}
});
it("should update stored state if state events exist", function() {
var events = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", event: true,
content: {
name: "Room Name Here for updates"
}
})
];
room.currentState.setStateEvents(events);
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(1);
expect(liveEvents[0]).toEqual(events[0].event);
var stateEvents = getItem(mockStorageApi, stateKeyName);
expect(stateEvents.events["m.room.name"][""]).toEqual(events[0].event);
});
});
describe("getRooms", function() {
var mkState = function(id) {
return [
utils.mkEvent({
event: true, type: "m.room.create", user: userId, room: id,
content: {
creator: userId
}
}),
utils.mkMembership({
event: true, user: userId, room: id, mship: "join"
})
];
};
it("should get all rooms in the store", function() {
var roomIds = [
"!alpha:bet", "!beta:fet"
];
// store 2 dynamically
var roomA = new Room(roomIds[0]);
roomA.currentState.setStateEvents(mkState(roomIds[0]));
var roomB = new Room(roomIds[1]);
roomB.currentState.setStateEvents(mkState(roomIds[1]));
store.storeRoom(roomA);
store.storeRoom(roomB);
var rooms = store.getRooms();
expect(rooms.length).toEqual(2);
for (var i = 0; i < rooms.length; i++) {
var index = roomIds.indexOf(rooms[i].roomId);
expect(index).not.toEqual(
-1, "Unknown room"
);
roomIds.splice(index, 1);
}
});
});
describe("getUser", function() {
it("should be able to retrieve a stored user", function() {
var user = new User(userId);
store.storeUser(user);
var result = store.getUser(userId);
expect(result).toBeDefined();
expect(result.userId).toEqual(userId);
});
it("should be able to retrieve a stored user with name data", function() {
var presence = utils.mkEvent({
type: "m.presence", event: true, content: {
user_id: userId,
displayname: "Flibble"
}
});
var user = new User(userId);
user.setPresenceEvent(presence);
store.storeUser(user);
var result = store.getUser(userId);
console.log(result);
expect(result.events.presence).toEqual(presence);
});
});
});
function getItem(store, key) {
return JSON.parse(store.getItem(key));
}
function setItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
+46
View File
@@ -0,0 +1,46 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module
*/
export default class Reemitter {
constructor(target) {
this.target = target;
// We keep one bound event handler for each event name so we know
// what event is arriving
this.boundHandlers = {};
}
_handleEvent(eventName, ...args) {
this.target.emit(eventName, ...args);
}
reEmit(source, eventNames) {
for (const eventName of eventNames) {
if (this.boundHandlers[eventName] === undefined) {
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
}
const boundHandler = this.boundHandlers[eventName];
source.on(eventName, boundHandler);
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17 -13
View File
@@ -16,7 +16,7 @@ limitations under the License.
/**
* @module content-repo
*/
var utils = require("./utils");
const utils = require("./utils");
/** Content Repo utility functions */
module.exports = {
@@ -46,9 +46,9 @@ module.exports = {
return '';
}
}
var serverAndMediaId = mxc.slice(6); // strips mxc://
var prefix = "/_matrix/media/v1/download/";
var params = {};
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/v1/download/";
const params = {};
if (width) {
params.width = width;
@@ -65,8 +65,8 @@ module.exports = {
prefix = "/_matrix/media/v1/thumbnail/";
}
var fragmentOffset = serverAndMediaId.indexOf("#"),
fragment = "";
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
@@ -88,18 +88,22 @@ module.exports = {
if (!identiconString) {
return null;
}
if (!width) { width = 96; }
if (!height) { height = 96; }
var params = {
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height
height: height,
};
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
}
},
};
+660
View File
@@ -0,0 +1,660 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/DeviceList
*
* Manages the list of other users' devices
*/
import Promise from 'bluebird';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
/* State transition diagram for DeviceList._deviceTrackingStatus
*
* |
* stopTrackingDeviceList V
* +---------------------> NOT_TRACKED
* | |
* +<--------------------+ | startTrackingDeviceList
* | | V
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
* | | ^ | | |
* | | restart download | | start download | | invalidateUserDeviceList
* | | client failed | | | |
* | | | V | |
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
* | | | |
* +<-------------------+ | download successful |
* ^ V |
* +----------------------- UP_TO_DATE ------------------------+
*/
// constants for DeviceList._deviceTrackingStatus
const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) {
this._sessionStore = sessionStore;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, sessionStore, olmDevice,
);
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = sessionStore.getEndToEndDeviceTrackingStatus() || {};
for (const u of Object.keys(this._deviceTrackingStatus)) {
// if a download was in progress when we got shut down, it isn't any more.
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
// userId -> promise
this._keyDownloadsInProgressByUser = {};
this.lastKnownSyncToken = null;
}
/**
* Download the keys for a list of users and stores the keys in the session
* store.
* @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached.
*
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto/deviceinfo|DeviceInfo}.
*/
downloadKeys(userIds, forceDownload) {
const usersToDownload = [];
const promises = [];
userIds.forEach((u) => {
const trackingStatus = this._deviceTrackingStatus[u];
if (this._keyDownloadsInProgressByUser[u]) {
// already a key download in progress/queued for this user; its results
// will be good enough for us.
console.log(
`downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`,
);
promises.push(this._keyDownloadsInProgressByUser[u]);
} else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) {
usersToDownload.push(u);
}
});
if (usersToDownload.length != 0) {
console.log("downloadKeys: downloading for", usersToDownload);
const downloadPromise = this._doKeyDownload(usersToDownload);
promises.push(downloadPromise);
}
if (promises.length === 0) {
console.log("downloadKeys: already have all necessary keys");
}
return Promise.all(promises).then(() => {
return this._getDevicesFromStore(userIds);
});
}
/**
* Get the stored device keys for a list of user ids
*
* @param {string[]} userIds the list of users to list keys for.
*
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
*/
_getDevicesFromStore(userIds) {
const stored = {};
const self = this;
userIds.map(function(u) {
stored[u] = {};
const devices = self.getStoredDevicesForUser(u) || [];
devices.map(function(dev) {
stored[u][dev.deviceId] = dev;
});
});
return stored;
}
/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't
* managed to get a list of devices for this user yet.
*/
getStoredDevicesForUser(userId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs) {
return null;
}
const res = [];
for (const deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
}
}
return res;
}
/**
* Get the stored keys for a single device
*
* @param {string} userId
* @param {string} deviceId
*
* @return {module:crypto/deviceinfo?} device, or undefined
* if we don't know about this device
*/
getStoredDevice(userId, deviceId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs || !devs[deviceId]) {
return undefined;
}
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
}
/**
* Find a device by curve25519 identity key
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(userId, algorithm, senderKey) {
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devices) {
return null;
}
for (const deviceId in devices) {
if (!devices.hasOwnProperty(deviceId)) {
continue;
}
const device = devices[deviceId];
for (const keyId in device.keys) {
if (!device.keys.hasOwnProperty(keyId)) {
continue;
}
if (keyId.indexOf("curve25519:") !== 0) {
continue;
}
const deviceKey = device.keys[keyId];
if (deviceKey == senderKey) {
return DeviceInfo.fromStorage(device, deviceId);
}
}
}
// doesn't match a known device
return null;
}
/**
* flag the given user for device-list tracking, if they are not already.
*
* This will mean that a subsequent call to refreshOutdatedDeviceLists()
* will download the device list for the user, and that subsequent calls to
* invalidateUserDeviceList will trigger more updates.
*
* @param {String} userId
*/
startTrackingDeviceList(userId) {
// sanity-check the userId. This is mostly paranoia, but if synapse
// can't parse the userId we give it as an mxid, it 500s the whole
// request and we can never update the device lists again (because
// the broken userId is always 'invalid' and always included in any
// refresh request).
// By checking it is at least a string, we can eliminate a class of
// silly errors.
if (typeof userId !== 'string') {
throw new Error('userId must be a string; was '+userId);
}
if (!this._deviceTrackingStatus[userId]) {
console.log('Now tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Mark the given user as no longer being tracked for device-list updates.
*
* This won't affect any in-progress downloads, which will still go on to
* complete; it will just mean that we don't think that we have an up-to-date
* list for future calls to downloadKeys.
*
* @param {String} userId
*/
stopTrackingDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Mark the cached device list for the given user outdated.
*
* If we are not tracking this user's devices, we'll do nothing. Otherwise
* we flag the user as needing an update.
*
* This doesn't actually set off an update, so that several users can be
* batched together. Call refreshOutdatedDeviceLists() for that.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log("Marking device list outdated for", userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Mark all tracked device lists as outdated.
*
* This will flag each user whose devices we are tracking as in need of an
* update.
*/
invalidateAllDeviceLists() {
for (const userId of Object.keys(this._deviceTrackingStatus)) {
this.invalidateUserDeviceList(userId);
}
}
/**
* If we have users who have outdated device lists, start key downloads for them
*
* @returns {Promise} which completes when the download completes; normally there
* is no need to wait for this (it's mostly for the unit tests).
*/
refreshOutdatedDeviceLists() {
const usersToDownload = [];
for (const userId of Object.keys(this._deviceTrackingStatus)) {
const stat = this._deviceTrackingStatus[userId];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
usersToDownload.push(userId);
}
}
// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
this._persistDeviceTrackingStatus();
return this._doKeyDownload(usersToDownload);
}
/**
* Fire off download update requests for the given users, and update the
* device list tracking status for them, and the
* _keyDownloadsInProgressByUser map for them.
*
* @param {String[]} users list of userIds
*
* @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
_doKeyDownload(users) {
if (users.length === 0) {
// nothing to do
return Promise.resolve();
}
const prom = this._serialiser.updateDevicesForUsers(
users, this.lastKnownSyncToken,
).then(() => {
finished(true);
}, (e) => {
console.error(
'Error downloading keys for ' + users + ":", e,
);
finished(false);
throw e;
});
users.forEach((u) => {
this._keyDownloadsInProgressByUser[u] = prom;
const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS;
}
});
const finished = (success) => {
users.forEach((u) => {
// we may have queued up another download request for this user
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) {
console.log('Another update in the queue for', u,
'- not marking up-to-date');
return;
}
delete this._keyDownloadsInProgressByUser[u];
const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
if (success) {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
console.log("Device list for", u, "now up to date");
} else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
});
this._persistDeviceTrackingStatus();
};
return prom;
}
_persistDeviceTrackingStatus() {
this._sessionStore.storeEndToEndDeviceTrackingStatus(this._deviceTrackingStatus);
}
}
/**
* Serialises updates to device lists
*
* Ensures that results from /keys/query are not overwritten if a second call
* completes *before* an earlier one.
*
* It currently does this by ensuring only one call to /keys/query happens at a
* time (and queuing other requests up).
*/
class DeviceListUpdateSerialiser {
constructor(baseApis, sessionStore, olmDevice) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
this._downloadInProgress = false;
// users which are queued for download
// userId -> true
this._keyDownloadsQueuedByUser = {};
// deferred which is resolved when the queued users are downloaded.
//
// non-null indicates that we have users queued for download.
this._queuedQueryDeferred = null;
// sync token to be used for the next query: essentially the
// most recent one we know about
this._nextSyncToken = null;
}
/**
* Make a key query request for the given users
*
* @param {String[]} users list of user ids
*
* @param {String} syncToken sync token to pass in the query request, to
* help the HS give the most recent results
*
* @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
updateDevicesForUsers(users, syncToken) {
users.forEach((u) => {
this._keyDownloadsQueuedByUser[u] = true;
});
this._nextSyncToken = syncToken;
if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = Promise.defer();
}
if (this._downloadInProgress) {
// just queue up these users
console.log('Queued key download for', users);
return this._queuedQueryDeferred.promise;
}
// start a new download.
return this._doQueuedQueries();
}
_doQueuedQueries() {
if (this._downloadInProgress) {
throw new Error(
"DeviceListUpdateSerialiser._doQueuedQueries called with request active",
);
}
const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser);
this._keyDownloadsQueuedByUser = {};
const deferred = this._queuedQueryDeferred;
this._queuedQueryDeferred = null;
console.log('Starting key download for', downloadUsers);
this._downloadInProgress = true;
const opts = {};
if (this._nextSyncToken) {
opts.token = this._nextSyncToken;
}
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
const dk = res.device_keys || {};
// do each user in a separate promise, to avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158)
//
// of course we ought to do this in a web worker or similar, but
// this serves as an easy solution for now.
let prom = Promise.resolve();
for (const userId of downloadUsers) {
prom = prom.delay(5).then(() => {
return this._processQueryResponseForUser(userId, dk[userId]);
});
}
return prom;
}).done(() => {
console.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false;
deferred.resolve();
// if we have queued users, fire off another request.
if (this._queuedQueryDeferred) {
this._doQueuedQueries();
}
}, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgressInProgress = false;
deferred.reject(e);
});
return deferred.promise;
}
async _processQueryResponseForUser(userId, response) {
console.log('got keys for ' + userId + ':', response);
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
});
}
await _updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, response || {},
);
// update the session store
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
});
this._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
}
}
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) {
let updated = false;
// remove any devices in the store which aren't in the response
for (const deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
if (!(deviceId in userResult)) {
console.log("Device " + userId + ":" + deviceId +
" has been removed");
delete userStore[deviceId];
updated = true;
}
}
for (const deviceId in userResult) {
if (!userResult.hasOwnProperty(deviceId)) {
continue;
}
const deviceResult = userResult[deviceId];
// check that the user_id and device_id in the response object are
// correct
if (deviceResult.user_id !== userId) {
console.warn("Mismatched user_id " + deviceResult.user_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
if (deviceResult.device_id !== deviceId) {
console.warn("Mismatched device_id " + deviceResult.device_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
if (await _storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
updated = true;
}
}
return updated;
}
/*
* Process a device in a /query response, and add it to the userStore
*
* returns (a promise for) true if a change was made, else false
*/
async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
if (!deviceResult.keys) {
// no keys?
return false;
}
const deviceId = deviceResult.device_id;
const userId = deviceResult.user_id;
const signKeyId = "ed25519:" + deviceId;
const signKey = deviceResult.keys[signKeyId];
if (!signKey) {
console.warn("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
const unsigned = deviceResult.unsigned || {};
try {
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.warn("Unable to verify signature on device " +
userId + ":" + deviceId + ":" + e);
return false;
}
// DeviceInfo
let deviceStore;
if (deviceId in userStore) {
// already have this device.
deviceStore = userStore[deviceId];
if (deviceStore.getFingerprint() != signKey) {
// this should only happen if the list has been MITMed; we are
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
console.warn("Ed25519 key for device " + userId + ":" +
deviceId + " has changed");
return false;
}
} else {
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
}
deviceStore.keys = deviceResult.keys || {};
deviceStore.algorithms = deviceResult.algorithms || [];
deviceStore.unsigned = unsigned;
return true;
}
+974
View File
@@ -0,0 +1,974 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
const Olm = global.Olm;
if (!Olm) {
throw new Error("global.Olm is not defined");
}
const utils = require("../utils");
// The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
function checkPayloadLength(payloadString) {
if (payloadString === undefined) {
throw new Error("payloadString undefined");
}
if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
// might as well fail early here rather than letting the olm library throw
// a cryptic memory allocation error.
//
// Note that even if we manage to do the encryption, the message send may fail,
// because by the time we've wrapped the ciphertext in the event object, it may
// exceed 65K. But at least we won't just fail with "abort()" in that case.
throw new Error("Message too long (" + payloadString.length + " bytes). " +
"The maximum for an encrypted message is " +
MAX_PLAINTEXT_LENGTH + " bytes.");
}
}
/**
* The type of object we use for importing and exporting megolm session data.
*
* @typedef {Object} module:crypto/OlmDevice.MegolmSessionData
* @property {String} sender_key Sender's Curve25519 device key
* @property {String[]} forwarding_curve25519_key_chain Devices which forwarded
* this session to us (normally empty).
* @property {Object<string, string>} sender_claimed_keys Other keys the sender claims.
* @property {String} room_id Room this session is used in
* @property {String} session_id Unique id for the session
* @property {String} session_key Base64'ed key data
*/
/**
* 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";
// don't know these until we load the account from storage in init()
this.deviceCurve25519Key = null;
this.deviceEd25519Key = null;
this._maxOneTimeKeys = null;
// 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 = {};
}
/**
* Initialise the OlmAccount. This must be called before any other operations
* on the OlmDevice.
*
* Attempts to load the OlmAccount from localStorage, or creates one if none is
* found.
*
* Reads the device keys from the OlmAccount object.
*/
OlmDevice.prototype.init = async function() {
let e2eKeys;
const account = new Olm.Account();
try {
_initialise_account(this._sessionStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
} finally {
account.free();
}
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
};
function _initialise_account(sessionStore, pickleKey, account) {
const e2eAccount = sessionStore.getEndToEndAccount();
if (e2eAccount !== null) {
account.unpickle(pickleKey, e2eAccount);
return;
}
account.create();
const 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) {
const account = new Olm.Account();
try {
const 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) {
const 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) {
const sessions = this._sessionStore.getEndToEndSessions(deviceKey);
const pickledSession = sessions[sessionId];
const 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) {
const 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) {
const 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 {Promise<string>} base64-encoded signature
*/
OlmDevice.prototype.sign = async 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 = async 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._maxOneTimeKeys;
};
/**
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = async function() {
const 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 = async function(numKeys) {
const 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 = async function(
theirIdentityKey, theirOneTimeKey,
) {
const self = this;
return this._getAccount(function(account) {
const 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 = async function(
theirDeviceIdentityKey, message_type, ciphertext,
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
}
const self = this;
return this._getAccount(function(account) {
const session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
self._saveAccount(account);
const 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 {Promise<string[]>} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
const 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 {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
const sessionIds = await 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 = async function(deviceIdentityKey) {
const sessionIds = await this.getSessionIdsForDevice(deviceIdentityKey);
sessionIds.sort();
const info = [];
function getSessionInfo(session) {
return {
hasReceivedMessage: session.has_received_message(),
};
}
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
const 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 {Promise<string>} ciphertext
*/
OlmDevice.prototype.encryptMessage = async function(
theirDeviceIdentityKey, sessionId, payloadString,
) {
const self = this;
checkPayloadLength(payloadString);
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
const 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 {Promise<string>} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = async function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
) {
const self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
const 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 {Promise<boolean>} true if the received message is a prekey message which matches
* the given session.
*/
OlmDevice.prototype.matchesSession = async 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) {
const 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) {
const pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
throw new Error("Unknown outbound group session " + sessionId);
}
const 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() {
const 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) {
const self = this;
checkPayloadLength(payloadString);
return this._getOutboundGroupSession(sessionId, function(session) {
const 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
// =====================
/**
* data stored in the session store about an inbound group session
*
* @typedef {Object} InboundGroupSessionData
* @property {string} room_Id
* @property {string} session pickled Olm.InboundGroupSession
* @property {Object<string, string>} keysClaimed
* @property {Array<string>} forwardingCurve25519KeyChain Devices involved in forwarding
* this session to us (normally empty).
*/
/**
* store an InboundGroupSession in the session store
*
* @param {string} senderCurve25519Key
* @param {string} sessionId
* @param {InboundGroupSessionData} sessionData
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
senderCurve25519Key, sessionId, sessionData,
) {
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(sessionData),
);
};
/**
* 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, InboundGroupSessionData): T} func
* function to call.
*
* @return {null} the sessionId is unknown
*
* @return {T} result of func
*
* @private
* @template {T}
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func,
) {
let 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 + ")",
);
}
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return func(session, r);
} 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 {Array<string>} forwardingCurve25519KeyChain Devices involved in forwarding
* this session to us.
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
* @param {boolean} exportFormat true if the megolm keys are in export format
* (ie, they lack an ed25519 signature)
*/
OlmDevice.prototype.addInboundGroupSession = async function(
roomId, senderKey, forwardingCurve25519KeyChain,
sessionId, sessionKey, keysClaimed,
exportFormat,
) {
const self = this;
/* if we already have this session, consider updating it */
function updateSession(session, sessionData) {
console.log("Update for megolm session " + senderKey + "/" + sessionId);
// for now we just ignore updates. TODO: implement something here
return true;
}
const r = this._getInboundGroupSession(
roomId, senderKey, sessionId, updateSession,
);
if (r !== null) {
return;
}
// new session.
const session = new Olm.InboundGroupSession();
try {
if (exportFormat) {
session.import_session(sessionKey);
} else {
session.create(sessionKey);
}
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + senderKey,
);
}
const sessionData = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
};
self._saveInboundGroupSession(
senderKey, sessionId, sessionData,
);
} finally {
session.free();
}
};
/**
* Add a previously-exported inbound group session to the session store
*
* @param {module:crypto/OlmDevice.MegolmSessionData} data session data
*/
OlmDevice.prototype.importInboundGroupSession = async function(data) {
/* if we already have this session, consider updating it */
function updateSession(session, sessionData) {
console.log("Update for megolm session " + data.sender_key + "|" +
data.session_id);
// for now we just ignore updates. TODO: implement something here
return true;
}
const r = this._getInboundGroupSession(
data.room_id, data.sender_key, data.session_id, updateSession,
);
if (r !== null) {
return;
}
// new session.
const session = new Olm.InboundGroupSession();
try {
session.import_session(data.session_key);
if (data.session_id != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + data.sender_key,
);
}
const sessionData = {
room_id: data.room_id,
session: session.pickle(this._pickleKey),
keysClaimed: data.sender_claimed_keys,
forwardingCurve25519KeyChain: data.forwarding_curve25519_key_chain,
};
this._saveInboundGroupSession(
data.sender_key, data.session_id, sessionData,
);
} 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 {Promise<{result: string, senderKey: string,
* forwardingCurve25519KeyChain: Array<string>,
* keysClaimed: Object<string, string>}>}
*/
OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body,
) {
const self = this;
function decrypt(session, sessionData) {
const res = session.decrypt(body);
let 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.
const 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;
}
sessionData.session = session.pickle(self._pickleKey);
self._saveInboundGroupSession(
senderKey, sessionId, sessionData,
);
return {
result: plaintext,
keysClaimed: sessionData.keysClaimed || {},
senderKey: senderKey,
forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [],
};
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, decrypt,
);
};
/**
* Determine if we have the keys for a given megolm session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {sring} sessionId session identifier
*
* @returns {Promise<boolean>} true if we have the keys to this session
*/
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
const s = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId,
);
if (s === null) {
return false;
}
const r = JSON.parse(s);
if (roomId !== r.room_id) {
console.warn(
`requested keys for inbound group session ${senderKey}|` +
`${sessionId}, with incorrect room_id (expected ${r.room_id}, ` +
`was ${roomId})`,
);
return false;
}
return true;
};
/**
* Extract the keys to a given megolm session, for sharing
*
* @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
*
* @returns {Promise<{chain_index: number, key: string,
* forwarding_curve25519_key_chain: Array<string>,
* sender_claimed_ed25519_key: string
* }>}
* details of the session key. The key is a base64-encoded megolm key in
* export format.
*/
OlmDevice.prototype.getInboundGroupSessionKey = async function(
roomId, senderKey, sessionId,
) {
function getKey(session, sessionData) {
const messageIndex = session.first_known_index();
const claimedKeys = sessionData.keysClaimed || {};
const senderEd25519Key = claimedKeys.ed25519 || null;
return {
"chain_index": messageIndex,
"key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain":
sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key,
};
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, getKey,
);
};
/**
* Export an inbound group session
*
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @return {Promise<module:crypto/OlmDevice.MegolmSessionData>} exported session data
*/
OlmDevice.prototype.exportInboundGroupSession = async function(senderKey, sessionId) {
const s = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId,
);
if (s === null) {
throw new Error("Unknown inbound group session [" + senderKey + "," +
sessionId + "]");
}
const r = JSON.parse(s);
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
const messageIndex = session.first_known_index();
return {
"sender_key": senderKey,
"sender_claimed_keys": r.keysClaimed,
"room_id": r.room_id,
"session_id": sessionId,
"session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain":
session.forwardingCurve25519KeyChain || [],
};
} finally {
session.free();
}
};
// 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;

Some files were not shown because too many files have changed in this diff Show More